import '../css/grid.scss';
import { global, isPositive, isMobile, throttle, debounce, truncate } from "../../utility";
import { r as lang } from "../../utility/lgres";
import { nullOrEmpty } from "../../utility/strings";
import { createElement } from "../../functions";
import { createIcon } from "../icon";
import { createCheckbox } from "../checkbox";
import { setTooltip } from "../tooltip";
import { Popup, showAlert, showConfirm } from "../popup";
import { convertCssStyle } from "../extension";
import { GridColumn, GridInputColumn, GridTextColumn, GridDropdownColumn, GridCheckboxColumn, GridRadioboxColumn, GridIconColumn, GridDateColumn } from "./column";
import { requestAnimationFrame } from '../../ui';
/**
* @author Tsanie Lily <[email protected]>
* @license MIT
* @version 1.0.7
*/
const ScriptPath = (self.document == null ? self.location.href : self.document.currentScript?.src ?? '').replace(/ui\.min\.js\?.+$/, '');
const Encoder = new TextEncoder('utf-8');
const ColumnChangedType = {
Reorder: 'reorder',
Resize: 'resize',
Sort: 'sort'
};
const RefreshInterval = isMobile() ? 32 : 10;
const HoverInternal = 200;
const RedumCount = 4;
const MiniDragOffset = 10;
const MiniColumnWidth = 50;
const FilterPanelWidth = 200;
const ExpandableWidth = 24;
/**
* @private
* @param {Event} e
* @returns {number}
*/
function getClientX(e) {
if (e == null) {
return null;
}
const cx = e.touches && e.touches[0]?.clientX;
return cx ?? e.clientX;
}
/**
* @private
* @param {HTMLElement} target
* @returns {number}
*/
function indexOfParent(target) {
// return [...target.parentElement.children].indexOf(target);
return Array.prototype.indexOf.call(target.parentElement.children, target);
}
const ColumnTypeDefs = {
0: GridColumn,
1: GridInputColumn,
2: GridDropdownColumn,
3: GridCheckboxColumn,
4: GridIconColumn,
5: GridTextColumn,
6: GridDateColumn,
7: GridRadioboxColumn
};
let r = lang;
/**
* 键值字典
* @template T
* @typedef {Map<string, T>} KeyMap
*/
/**
* 索引字典
* @template T
* @typedef {Map<number, T>} IndexMap
*/
/**
* 数据项
* @typedef ValueItem
* @property {any} Value - 值
* @property {string} DisplayValue - 显示值
* @property {boolean} [__checked] - 已选中
* @interface
*/
/**
* 行数据对象
* @typedef GridRowItem
* @type {KeyMap<ValueItem>}
* @property {(ValueItem | any)} {key} - 数据项
* @extends {KeyMap<ValueItem>}
* @interface
*/
/**
* 行数据包装接口
* @typedef GridItemWrapper
* @property {GridRowItem} values - 真实数据对象
* @property {KeyMap<GridSourceItem[]>} source - 下拉数据源缓存对象
* @property {number} __index - 行索引
* @property {number} __offset - 批量删除时暂存的索引偏移量
* @property {KeyMap<any>} __editing - 正在编辑的列的原始值字典
* @property {boolean} __changed - 行数据是否发生改变
* @property {boolean} __expanded - 行是否已展开
* @property {GridExpandableObject} __expandable_object - 行扩展对象
* @interface
*/
/**
* 下拉框数据源接口
* @typedef GridSourceItem
* @property {string} value - 值
* @property {string} text - 显示文本
* @interface
*/
/**
* 行数据可用性回调函数
* @callback GridItemBooleanCallback
* @param {GridRowItem} item - 行数据对象
* @returns {boolean} 返回是否可用
* @this GridColumnDefinition
*/
/**
* 行数据过滤回调函数
* @callback GridItemFilterCallback
* @param {GridRowItem} item - 行数据对象
* @param {boolean} editing - 是否处于编辑状态
* @param {HTMLElement} [body] - Grid 控件的 `<tbody>` 部分
* @param {number} [index] - 所在行索引(不可依赖此值,除了呈现时,其他时候该值不会传递)
* @returns {ValueItem} - 返回过滤后的显示或编辑值
* @this GridColumnDefinition
*/
/**
* 行数据处理回调函数
* @callback GridItemObjectCallback
* @param {GridRowItem} item - 行数据对象
* @returns {any} 返回任意对象
* @this GridColumnDefinition
*/
/**
* 行数据过滤项模板回调函数
* @callback GridItemHtmlCallback
* @param {ValueItem} item - 行数据对象
* @returns {HTMLElement} 返回过滤项元素对象
* @this GridColumnDefinition
*/
/**
* 行数据字符串回调函数
* @callback GridItemStringCallback
* @param {GridRowItem} item - 行数据对象
* @returns {string} 返回字符串
* @this GridColumnDefinition
*/
/**
* 列过滤器数据源回调函数
* @callback GridColumnFilterSourceCallback
* @param {GridColumnDefinition} col - 列定义对象
* @returns {ValueItem[]} 返回过滤器的数据数组
* @this Grid
*/
/**
* 行数据排序回调函数
* @callback GridItemSortCallback
* @param {GridRowItem} a - 对比行数据1
* @param {GridRowItem} b - 对比行数据2
* @returns {number} 返回大小对比结果
*/
/**
* 下拉列表数据源回调函数
* @callback GridDropdownSourceCallback
* @param {GridRowItem} item - 行数据对象
* @returns {GridSourceItem[]} 行下拉列表数据源
*/
/**
* 下拉列表参数对象
* @typedef DropdownOptions
* @property {string} [textKey=text] - 文本关键字
* @property {string} [valueKey=value] - 值关键字
* @property {string} [htmlKey=html] - 源码显示的关键字
* @property {number} [maxLength=500] - 最大输入长度
* @property {boolean} [multiSelect] - 是否允许多选
* @property {string} [selected] - 选中值
* @property {string[]} [selectedList] - 选中的数组
* @property {boolean} [disabled] - 是否禁用
* @property {boolean} [input] - 是否支持输入
* @property {boolean} [search] - 是否支持搜索
* @property {string[]} [searchKeys] - 搜索的关键字数组
* @property {string} [searchPlaceholder] - 搜索提示文本,默认值取语言资源 `searchHolder` "Search..."
* @property {number} [tabIndex] - 焦点索引
* @property {string} [placeholder] - 输入框的提示文本
* @property {boolean} [slideFixed] - 是否固定为向下展开
* @property {HTMLElement} [wrapper] - 父元素,默认添加到头元素之后
* @interface
*/
/**
* 自定义日期格式化回调函数
* @callback DateFormatterCallback
* @param {Date} date - 日期值
* @returns {any} 返回格式化后的结果
*/
/**
* 列定义接口
*
* <img src="./assets/column-types.png" alt="Column Types"/><br/>
* 代码参考页面下方的示例
* @typedef GridColumnDefinition
* @property {string} key - 列关键字,默认以该关键字从行数据中提取单元格值,行数据的关键字属性值里包含 DisplayValue 则优先显示此值
* @property {(GridColumnTypeEnum | GridColumn)} [type=Grid.ColumnTypes.Common] - 列的类型,可以为 {@linkcode GridColumn} 的子类,或者如下内置类型 {@linkcode Grid.ColumnTypes}
* * Grid.ColumnTypes.Common - 0: 通用列(只读)
* * Grid.ColumnTypes.Input - 1: 单行文本列
* * Grid.ColumnTypes.Dropdown - 2: 下拉选择列
* * Grid.ColumnTypes.Checkbox - 3: 复选框列
* * Grid.ColumnTypes.Icon - 4: 图标列
* * Grid.ColumnTypes.Text - 5: 多行文本列
* * Grid.ColumnTypes.Date - 6: 日期选择列
* * Grid.ColumnTypes.Radio - 7: 单选框列
* @property {string} [caption] - 列标题文本
* @property {any} [captionStyle] - 列标题的元素样式
* @property {string} [captionTooltip] - 列标题的帮助文本
* @property {number} [width] - 大于 0 则设置为该宽度,否则根据列内容自动调整列宽
* @property {("left" |"center" | "right")} [align=left] 列对齐方式
* @property {(boolean | string | GridItemBooleanCallback)} [enabled] - 列是否可用(可编辑),允许以下类型
*
* * `boolean` 则直接使用该值
* * `string` 则以该值为关键字从行数据中取值作为判断条件
* * `GridItemBooleanCallback` 则调用回调,以返回值作为判断条件
* @property {GridItemFilterCallback} [filter] - 单元格取值采用该函数返回的值
* @property {string} [text] - 单元格以该值填充内容,忽略filter与关键字属性
* @property {boolean} [visible=true] - 列是否可见
* @property {boolean} [resizable=true] - 列是否允许调整宽度
* @property {boolean} [sortable=true] - 列是否允许排序
* @property {boolean} [orderable=true] - 列是否允许重排顺序
* @property {boolean} [allcheck=false] - 列为复选框类型时是否在列头增加全选复选框
* @property {boolean} [shrink=false] - 列为收缩列,禁用自动调整大小
* @property {string} [class] - 单元格元素的额外样式类型字符串(仅在重建行元素时设置)
* @property {boolean} [contentWrap=false] - 单元格文本是否换行(仅在重建行元素时设置)
* @property {number} [maxLines=0] - 大于 0 时限制显示最大行数
* @property {any} [css] - 单元格css样式对象(仅在重建行元素时读取)
* @property {any} [totalCss] - 合计行样式(仅在重建合计行元素时读取)
* @property {(any | GridItemObjectCallback)} [style] - 单元格样式(填充行列数据时读取),支持直接返回样式对象或调用函数返回(若赋值则忽略 [styleFilter]{@linkcode GridColumnDefinition#styleFilter})
* @property {GridItemObjectCallback} [styleFilter] - **已过时**<br/>_根据返回值填充单元格样式(填充行列数据时读取)_
* @property {(string | GridItemStringCallback)} [background] - 设置单元格背景色(填充行列数据时读取),支持直接设置颜色字符串或调用函数返回(若赋值则忽略 [bgFilter]{@linkcode GridColumnDefinition#bgFilter})
* @property {GridItemStringCallback} [bgFilter] - **已过时**<br/>_根据返回值设置单元格背景色_
* @property {boolean} [switch=false] - 复选框为 `ui-switch` 样式 *@since* 1.0.6
* @property {(any | GridItemObjectCallback)} [attrs] - 根据返回值设置单元格元素的附加属性,允许直接设置对象也支持调用函数返回对象
* @property {KeyMap<Function>} [events] - 给单元格元素附加事件(事件函数上下文为数据行对象)
* @property {boolean} [allowFilter=false] - 是否允许进行列头过滤
* @property {any[]} [filterValues] - 过滤值的数组
* @property {boolean} [filterAllowNull=false] - 是否区分 `null` 与空字符串
* @property {(ValueItem[] | GridColumnFilterSourceCallback)} [filterSource] - 自定义列过滤器的数据源,支持调用函数返回数据源
* @property {boolean} [filterAsValue=false] - 列头过滤强制使用 `Value` 字段
* @property {GridItemHtmlCallback} [filterTemplate] - 列头过滤项的模板函数
* @property {GridItemSortCallback} [sortFilter] - 自定义列排序函数
* @property {boolean} [sortAsText=false] - 按照 `DisplayValue` 排序
* @property {DropdownOptions} [dropOptions] - 列为下拉列表类型时以该值设置下拉框的参数
* @property {boolean} [dropRestrictCase=false] - 下拉列表是否区分大小写
* @property {(GridSourceItem[] | Promise<GridSourceItem[]> | GridDropdownSourceCallback)} [source] - 列为下拉列表类型时以该值设置下拉列表数据源,支持返回异步对象,也支持调用函数返回
* @property {boolean} [sourceCache=false] - 下拉列表数据源是否缓存结果(即行数据未发生变化时仅从source属性获取一次值)
* @property {("fa-light" | "fa-regular" | "fa-solid")} [iconType=fa-light] - 列为图标类型时以该值设置图标样式
* @property {string} [dateMin] - 列为日期类型时以该值作为最小可选日期值
* @property {string} [dateMax] - 列为日期类型时以该值作为最大可选日期值
* @property {string} [dateDisplayFormatter] - 列为日期类型时日期显示的格式化字符串
* @property {(string | DateFormatterCallback)} [dateValueFormatter] - 列为日期类型时自定义日期格式化字符串或函数
* @property {(string | GridItemStringCallback)} [tooltip] - 额外设置单元格的 tooltip,支持直接使用字符串或者使用函数返回的字符串
* @property {Function} [onAllChecked] - 列头复选框改变时触发事件
* @property {Function} [onChanged] - 单元格变化时触发事件
* @property {Function} [onFilterOk] - 列过滤点击 `OK` 时触发的事件
* @property {Function} [onFiltered] - 列过滤后触发的事件
* @property {Function} [onDropExpanded] - 列为下拉框类型时在下拉列表展开时触发的事件
* @interface
* @example
* [
* {
* key: 'name',
* // type: Grid.ColumnTypes.Common,
* caption: 'Name',
* captionStyle: {
* 'font-style': 'italic'
* },
* width: 150,
* allowFilter: true
* },
* {
* key: 'birthday',
* type: Grid.ColumnTypes.Date,
* caption: 'Birthday',
* width: 120,
* dateMin: '1900-01-01',
* dateMax: '2025-01-01',
* dateValueFormatter: toDateValue
* },
* {
* key: 'age',
* type: Grid.ColumnTypes.Input,
* caption: 'Age',
* enabled: false,
* align: 'right',
* filter: item => {
* const ms = new Date() - new Date(item.birthday);
* const age = Math.floor(ms / 1000 / 60 / 60 / 24 / 365);
* return String(age);
* }
* },
* {
* key: 'sex',
* type: Grid.ColumnTypes.Dropdown,
* caption: 'Sex',
* source: [
* { value: 'male', text: 'Male' },
* { value: 'female', text: 'Female' },
* { value: 'other', text: 'Other' }
* ]
* },
* {
* key: 'active',
* type: Grid.ColumnTypes.Checkbox,
* caption: 'Active'
* },
* {
* key: 'remove',
* type: Grid.ColumnTypes.Icon,
* text: 'times',
* resizable: false,
* sortable: false,
* orderable: false,
* tooltip: 'Remove',
* events: {
* onclick: () => {
* showConfirm('Remove', 'Are you sure you want to remove this person?', [
* {
* key: 'yes',
* text: 'Yes',
* trigger: () => {
* console.log('yes');
* return true;
* }
* },
* {
* key: 'no',
* text: 'No'
* }
* ], 'question')
* }
* }
* }
* ]
*/
/**
* 列头复选框改变时触发的事件
* @name onAllChecked
* @event
* @param {GridColumnDefinition} col - 列定义对象
* @param {boolean} flag - 是否选中
* @this Grid
* @memberof GridColumnDefinition
*/
/**
* 单元格发生变化时触发的事件
* @name onChanged
* @event
* @param {GridRowItem} item - 行数据对象
* @param {(boolean | string | number)} value - 修改后的值
* @param {(boolean | string | number)} oldValue - 修改前的值
* @param {any} [e] - 列修改事件传递过来的任意对象
* @param {GridExpandableObject} [expandableObject] - 列展开元素对象
* @this Grid
* @memberof GridColumnDefinition
*/
/**
* 列过滤点击 `OK` 时触发的事件
* @name onFilterOk
* @event
* @param {GridColumnDefinition} col - 列定义对象
* @param {ValueItem[]} selected - 选中的过滤项
* @this Grid
* @memberof GridColumnDefinition
*/
/**
* 列过滤后触发的事件
* @name onFiltered
* @event
* @param {GridColumnDefinition} col - 列定义对象
* @this Grid
* @memberof GridColumnDefinition
*/
/**
* 列为下拉框类型时在下拉列表展开时触发的事件
* @name onDropExpanded
* @event
* @param {GridRowItem} item - 行数据对象
* @param {Dropdown} drop - 下拉框对象
* @this GridColumnDefinition
* @memberof GridColumnDefinition
*/
/**
* 判断列是否始终编辑的回调函数
* @callback ColumnTypesEnumIsAlwaysEditing
* @param {number | GridColumn} type - 列类型
* @returns {boolean} 返回是否始终编辑
*/
/**
* 列类型枚举
* @enum {number}
*/
const GridColumnTypeEnum = {
/** 0 - 通用列(只读) */
Common: 0,
/** 1 - 单行文本列 */
Input: 1,
/** 2 - 下拉选择列 */
Dropdown: 2,
/** 3 - 复选框列 */
Checkbox: 3,
/** 4 - 图标列 */
Icon: 4,
/** 5 - 多行文本列 */
Text: 5,
/** 6 - 日期选择列 */
Date: 6,
/** 7 - 单选框列 */
Radio: 7,
/**
* 判断列是否为复选框列
* @type {ColumnTypesEnumIsAlwaysEditing}
*/
isAlwaysEditing(type) {
return type?.headerEditing || type === 3 || type === 7;
}
};
/**
* 列排序枚举
* @enum {number}
*/
const GridColumnDirection = {
/** -1 - 倒序 */
Descending: -1,
/** 1 - 升序 */
Ascending: 1
};
/**
* 多语言资源接口
* @typedef GridLanguages
* @property {string} [all] - ( All )
* @property {string} [ok] - OK
* @property {string} [yes] - Yes
* @property {string} [reset] - Reset
* @property {string} [cancel] - Cancel
* @property {string} [null] - ( Null )
* @property {string} [addLevel] - Add Level
* @property {string} [deleteLevel] - Delete Level
* @property {string} [copyLevel] - Copy Level
* @property {string} [sortBy] - Sort by
* @property {string} [thenBy] - Then by
* @property {string} [updateLayout] - Update Layout
* @property {string} [asc] - Ascending
* @property {string} [desc] - Descending
* @property {string} [column] - Column
* @property {string} [order] - Order
* @property {string} [sort] - Sort
* @property {string} [sortArrayExists] - This will remove the current tiered sort. Do you wish to continue?
* @property {string} [requirePrompt] - All sort criteria must have a column specified. Check the selected sort criteria and try again.
* @property {string} [duplicatePrompt] - {column} is being sorted more than once. Delete the duplicate sort criteria and try again.
* @interface
*/
/**
* Grid 数据导出对象
* @typedef GridExportData
* @property {Uint8Array} data - 导出数据
* @property {string} [type] - 导出类型,`"compressed"` 意为压缩数据
* @property {string} [error] - 压缩中的异常信息提示
* @interface
*/
/**
* 列排序定义接口
* @typedef GridColumnSortDefinition
* @property {string} column - 排序列的关键字
* @property {("asc" | "desc")} order - 升序或降序
* @interface
*/
/**
* 扩展行对象接口
* @typedef GridExpandableObject
* @property {HTMLElement} element - 扩展行元素
* @interface
*/
/**
* 扩展行生成回调函数
* @callback GridExpandableObjectCallback
* @param {GridRowItem} item - 行数据对象
* @returns {GridExpandableObject} 返回扩展行对象
* @this Grid
*/
/**
* @typedef GridVirtualRow
* @property {boolean} editing - 行处于编辑状态
* @property {KeyMap<GridVirtualCell>} cells - 虚拟单元格数组
* @private
*/
/**
* @typedef GridVirtualCell
* @property {string} background - 单元格背景色
* @property {string} value - 单元格值
* @property {string} tooltip - 单元格提示文本
* @property {boolean} enabled - 单元格是否可用
* @property {string} style - 单元格样式字符串
* @property {string} attrs - 单元格附加属性字符串
* @private
*/
/**
* @typedef GridColumnAttr
* @property {boolean} dragging - 列正在拖拽
* @property {number} offset - 列拖拽偏移
* @property {Function} mousemove - 拖拽或调整大小移动回调函数
* @property {Function} mouseup - 拖拽或调整大小鼠标释放回调函数
* @property {number} resizing - 列临时大小
* @property {boolean} sizing - 列已进入修改大小的状态
* @property {boolean} autoResize - 列需要自动调整大小
* @property {any} style - 列样式对象
* @property {ValueItem[]} filterSource - 列过滤面板数据源
* @property {number} filterHeight - 列过滤面板高度
* @property {number} filterTop - 列过滤面板滚动头部间距
* @private
*/
/**
* Grid 控件基础类
*
* <img src="./assets/grid-sample.png" alt="Grid Sample"/><br/>
* 函数调用流程图<br/>
* <img src="./assets/grid.jpg" alt="Grid"/>
* @class
* @example <caption>基础示例</caption>
* <div id="grid"></div>
* @example
* #grid>.ui-grid {
* width: 600px;
* height: 400px;
* }
* @example
* const grid = new Grid('#grid', GetTextByKey);
* grid.columns = [
* {
* key: 'name',
* caption: 'Name',
* width: 140,
* allowFilter: true
* },
* {
* key: 'age',
* caption: 'Age',
* type: Grid.ColumnTypes.Input,
* width: 80
* },
* {
* key: 'study',
* caption: 'Study',
* type: Grid.ColumnTypes.Dropdown,
* width: 120,
* source: [
* { value: 'a', text: 'A' },
* { value: 'b', text: 'B' },
* { value: 'c', text: 'C' }
* ]
* }
* ];
* grid.multiSelect = true;
* grid.init();
* grid.source = [
* { name: '张三', age: '19', study: 'a' },
* { name: '李四', age: '24', study: 'a' },
* { name: '王五', age: '20', study: 'c' }
* ];
*/
export class Grid {
/**
* 内部引用变量
* @private
*/
_var = {
/**
* 父容器元素
* @type {HTMLElement}
* @private
*/
parent: null,
/**
* Grid 元素 - `div.ui-grid`
* @type {HTMLDivElement}
* @private
*/
el: null,
/**
* 全部数据数组
* @type {GridItemWrapper[]}
* @private
*/
source: null,
/**
* 当前已过滤显示的数据数组
* @type {GridItemWrapper[]}
* @private
*/
currentSource: null,
/**
* 当前是否已全部展开
* @type {boolean}
* @private
*/
expanded: false,
/**
* 列可用性的关联字典
* @type {KeyMap<string | boolean>}
* @private
*/
enabledDict: null,
/**
* 合计行数据
* @type {GridRowItem}
* @private
*/
total: null,
/**
* Grid 是否只读
* @type {boolean}
* @private
*/
readonly: false,
/**
* 当前选中的列索引
* @type {number}
* @private
*/
selectedColumnIndex: -1,
/**
* 当前选中的行索引数组
* @type {number[]}
* @private
*/
selectedIndexes: null,
/**
* 虚模式头部索引
* @type {number}
* @private
*/
startIndex: 0,
/**
* 当前滚动上边距
* @type {number}
* @private
*/
scrollTop: 0,
/**
* 当前滚动左边距
* @type {number}
* @private
*/
scrollLeft: 0,
/**
* 浏览器是否为 Firefox
* @type {boolean}
* @private
*/
isFirefox: false,
/**
* 一页高度可显示的行数
* @type {number}
* @private
*/
rowCount: -1,
/**
* 虚拟单元格字典
* @type {IndexMap<GridVirtualRow>}
* @private
*/
virtualRows: {},
/**
* 列类型缓存字典
* @type {KeyMap<GridColumn>}
* @private
*/
colTypes: {},
/**
* 列属性字典
* @type {KeyMap<GridColumnAttr>}
* @private
*/
colAttrs: {
/**
* 有已过滤的列
* @type {boolean}
* @private
*/
__filtered: false,
/**
* 过滤面板已打开
* @type {boolean}
* @private
*/
__filtering: false,
/**
* 上一个目标排序列索引
* @type {number}
* @private
*/
__orderIndex: -1,
},
/**
* 是否处于渲染中
* @type {boolean}
* @private
*/
rendering: false,
/**
* 头部高度
* @type {number}
* @private
*/
headerHeight: null,
/**
* 正文高度
* @type {number}
* @private
*/
containerHeight: null,
/**
* 合计行高度
* @type {number}
* @private
*/
footerHeight: null,
/**
* 合计行底边距偏移量
* @type {number}
* @private
*/
footerOffset: null,
/**
* 容器宽度
* @type {number}
* @private
*/
wrapClientWidth: null,
/**
* 正文宽度
* @type {number}
* @private
*/
bodyClientWidth: null,
/**
* 是否需要 resize
* @type {boolean}
* @private
*/
needResize: null,
/**
* 提示条消失的 Timer
* @type {number}
* @private
*/
tooltipTimer: null,
/**
* 页面元素引用
* @private
*/
refs: {
/**
* 表格引用 - table.ui-grid-table
* @type {HTMLTableElement}
* @private
*/
table: null,
/**
* 表格正文引用 - tbody
* @type {HTMLTableSectionElement}
* @private
*/
body: null,
/**
* 表格头部引用 - thead>tr
* @type {HTMLTableSectionElement}
* @private
*/
header: null,
/**
* 表格合计行引用 - tfooter>tr
* @type {HTMLTableSectionElement}
* @private
*/
footer: null,
/**
* 加载状态元素引用 - div.ui-grid-loading
* @type {HTMLDivElement}
* @private
*/
loading: null,
/**
* 大小计算元素引用 - span.ui-grid-sizer
* @type {HTMLSpanElement}
* @private
*/
sizer: null,
/**
* 包装元素引用 - div.ui-grid-wrapper
* @type {HTMLDivElement}
* @private
*/
wrapper: null,
/**
* 拖拽块引用 - div.dragger
* @type {HTMLDivElement}
* @private
*/
dragger: null,
/**
* 拖拽光标引用 - layer.dragger-cursor
* @type {HTMLElement}
* @private
*/
draggerCursor: null,
}
};
/**
* 列定义的数组
* @type {GridColumnDefinition[]}
* @ignore
*/
columns = [];
/**
* 多语言资源对象
* @type {GridLanguages}
* @ignore
*/
langs = {};
/**
* 行数大于等于该值则启用虚模式
* @type {number}
* @default 100
* @ignore
*/
virtualCount = 100;
/**
* 未设置宽度的列自动调整列宽
* @type {boolean}
* @default true
* @ignore
*/
autoResize = true;
/**
* 表格行高
* @type {number}
* @default 36
* @ignore
*/
rowHeight = 36;
/**
* 文本行高(多行文本列计算高度时使用)
* @type {number}
* @default 18
* @ignore
*/
lineHeight = 18;
/**
* 列头未过滤时的图标
* @type {string}
* @default "ellipsis-h"
* @ignore
*/
filterIcon = 'ellipsis-h';
/**
* 列头已过滤时的图标
* @type {string}
* @default "filter"
* @ignore
*/
filteredIcon = 'filter';
/**
* 列表底部留出额外的空白行
* @type {number}
* @default 0
* @ignore
*/
extraRows = 0;
/**
* 过滤条件列表的行高
* @type {number}
* @default 30
* @ignore
*/
filterRowHeight = 30;
/**
* 列表高度值,为 0 时列表始终显示全部内容(自增高),为非数字或者小于 0 则根据容器高度来确定虚模式的渲染行数
* @type {number | null}
* @ignore
*/
height;
/**
* 是否允许多选
* @type {boolean}
* @default false
* @ignore
*/
multiSelect = false;
/**
* 为 `false` 时只有点击在单元格内才会选中行
* @type {boolean}
* @default true
* @ignore
*/
fullrowClick = true;
/**
* 单元格 tooltip 是否禁用
* @type {boolean}
* @default false
* @ignore
*/
tooltipDisabled = false;
/**
* 列头是否显示
* @type {boolean}
* @default true
* @ignore
*/
headerVisible = true;
/**
* 列头是否允许换行
* @type {boolean}
* @default true
* @ignore
*/
headerWrap = true;
/**
* 是否允许行间拖拽
* @type {boolean}
* @default false
* @ignore
*/
rowDraggable = false;
/**
* 监听事件的窗口载体
* @type {(Window | HTMLElement)}
* @default window
* @ignore
*/
window;
/**
* 排序列的索引
* @type {number}
* @default -1
* @ignore
*/
sortIndex = -1;
/**
* 排序方式,正数升序,负数倒序
* @type {GridColumnDirection}
* @default GridColumnDirection.Ascending
* @ignore
*/
sortDirection = GridColumnDirection.Ascending;
/**
* 排序列数组
* @type {GridColumnSortDefinition[]}
* @default null
* @ignore
*/
sortArray = null;
/**
* 是否支持点击扩展
* @type {boolean}
* @default false
* @ignore
*/
expandable;
/**
* 扩展行生成器
* @type {GridExpandableObjectCallback}
* @ignore
*/
expandableGenerator;
/**
* 即将选中行时触发
* @event
* @param {number} index - 即将选中的行索引
* @param {number} colIndex - 即将选中的列索引
* @returns {boolean} 返回 `false`、`null`、`undefined`、`0` 等则取消选中动作
* @this Grid
*/
willSelect;
/**
* 单元格单击时触发,colIndex 为 -1 则表示点击的是行的空白处
* @event
* @param {number} index - 点击的行索引
* @param {number} colIndex - 点击的列索引
* @returns {boolean} 返回 false 则取消事件冒泡
* @this Grid
*/
cellClicked;
/**
* 选中行发生变化时触发的事件
* @event
* @param {number} index - 选中的行索引
* @this Grid
*/
onSelectedRowChanged;
/**
* 单元格双击时触发的事件,colIndex 为 -1 则表示点击的是行的空白处
* @event
* @param {number} index - 双击的行索引
* @param {number} colIndex - 双击的列索引
* @this Grid
*/
onCellDblClicked;
/**
* 行双击时触发的事件
* @event
* @param {number} index - 双击的行索引
* @this Grid
*/
onRowDblClicked;
/**
* 列发生变化时触发的事件
* @event
* @param {("reorder" | "resize" | "sort")} type - 事件类型
*
* * "reorder" 为发生列重排事件,此时 value 为目标列索引
* * "resize" 为发生列宽调整事件,此时 value 为列宽度值
* * "sort" 为发生列排序事件,此时 value 为 1(升序)或 -1(倒序)
* @param {number} colIndex - 发生变化事件的列索引
* @param {number | GridColumnDirection} value - 变化的值
* @this Grid
*/
onColumnChanged;
/**
* 列滚动时触发的事件
* @event
* @param {Event} [e] - 滚动事件对象
* @param {number} index - 起始行
* @param {number} count - 显示行数
* @this Grid
*/
onBodyScrolled;
/**
* 扩展行展开时触发的事件
* @event
* @param {GridRowItem} item - 行数据对象
* @param {GridExpandableObject} expandableObject - 由 [expandableGenerator]{@linkcode Grid#expandableGenerator} 产生的行扩展对象
* @param {HTMLElement} expandableObject.element - 扩展行元素
* @this Grid
*/
onRowExpanded;
/**
* 扩展行收缩时触发的事件
* @event
* @param {GridRowItem} item - 行数据对象
* @param {GridExpandableObject} expandableObject - 由 [expandableGenerator]{@linkcode Grid#expandableGenerator} 产生的行扩展对象
* @param {HTMLElement} expandableObject.element - 扩展行元素
* @this Grid
*/
onRowCollapsed;
/**
* 行发生变化时触发的事件
* @event
* @param {("update" | "add" | "remove" | "drag")} action - 变动类型
* @param {GridRowItem[]} items - 发生变动的行对象
* @param {(number | number[])} indexes - 变动的索引集合
* @this Grid
* @since 1.0.3
*/
onRowChanged;
/**
* 列类型枚举
* @readonly
* @type {GridColumnTypeEnum}
* @property {number} Common=0 - 通用列(只读)
* @property {number} Input=1 - 单行文本列
* @property {number} Dropdown=2 - 下拉选择列
* @property {number} Checkbox=3 - 复选框列
* @property {number} Icon=4 - 图标列
* @property {number} Text=5 - 多行文本列
* @property {number} Date=6 - 日期选择列
* @property {number} Radio=7 - 单选框列
*/
static get ColumnTypes() { return GridColumnTypeEnum }
/**
* Grid 控件构造函数
* @param {(string | HTMLElement)?} container Grid 控件所在的父容器,可以是 string 表示选择器,也可以是 HTMLElement 对象<br/>_**构造时可以不进行赋值,但是调用 init 函数时则必须进行赋值**_
* @param {Function} [getText] 获取多语言文本的函数代理
* @param {string} getText.id - 资源 ID
* @param {string} [getText.def] - 默认资源
* @param {string} getText.{returns} 返回的多语言
* @property {GridColumnDefinition[]} columns - 列定义的数组
* @property {GridLanguages} [langs] - 多语言资源对象
* @property {number} [virtualCount=100] - 行数大于等于该值则启用虚模式
* @property {boolean} [autoResize=true] - 未设置宽度的列自动调整列宽
* @property {number} [rowHeight=36] - 表格行高,修改后同时需要在 `.ui-grid` 所在父容器重写 `--line-height` 的值以配合显示
* @property {number} [lineHeight=18] - 文本行高(多行文本列计算高度时使用)
* @property {string} [filterIcon=ellipsis-h] - 列头未过滤时的图标
* @property {string} [filteredIcon=filter] - 列头已过滤时的图标
* @property {number} [extraRows=0] - 列表底部留出额外的空白行
* @property {number} [filterRowHeight=30] - 过滤条件列表的行高
* @property {number} [height] - 列表高度值,为 0 时列表始终显示全部内容(自增高),为非数字或者小于 0 则根据容器高度来确定虚模式的渲染行数
* @property {boolean} [multiSelect=false] - 是否允许多选
* @property {boolean} [fullrowClick=true] - 为 `false` 时只有点击在单元格内才会选中行
* @property {boolean} [tooltipDisabled=false] - 单元格 tooltip 是否禁用
* @property {boolean} [headerVisible=true] - 列头是否显示
* @property {boolean} [headerWrap=true] - 列头是否允许换行
* @property {boolean} [rowDraggable=false] - 是否允许行间拖拽 *since* 1.0.3
* @property {(Window | HTMLElement)} [window=global] - 监听事件的窗口载体
* @property {number} [sortIndex=-1] - 排序列的索引
* @property {GridColumnDirection} [sortDirection=GridColumnDirection.Ascending] - 排序方式,正数升序,负数倒序
* @property {GridColumnSortDefinition[]} [sortArray] - 排序列数组
* @property {boolean} [expandable=false] - 是否支持点击扩展
* @property {GridExpandableObjectCallback} [expandableGenerator] - 扩展行生成器
*/
constructor(container, getText) {
this._var.parent = typeof container === 'string' ? document.querySelector(container) : container;
if (typeof getText === 'function') {
r = getText;
}
}
/**
* 获取 Grid 的页面元素
* @readonly
* @type {HTMLDivElement}
*/
get element() { return this._var.el }
/**
* 获取当前 Grid 是否已发生改变
* @readonly
* @type {boolean}
*/
get changed() {
const source = this._var.source;
if (source == null) {
return false;
}
return source.find(r => r.__changed) != null;
}
/**
* 返回所有数据的数据(未过滤)
* @readonly
* @type {GridRowItem[]}
*/
get allSource() { return this._var.source?.map(s => s.values) }
/**
* 获取已过滤的数据中的扩展对象数组
* @readonly
* @type {GridExpandableObject[]}
* @property {HTMLElement} element - 扩展行元素
*/
get sourceExpandable() { return this._var.currentSource?.map(s => s.__expandable_object) }
/**
* 获取当前是否为虚模式状态
* @readonly
* @type {boolean}
*/
get virtual() { return this._var.currentSource?.length > this.virtualCount }
/**
* 获取当前排序的列关键字,为 null 则当前无排序列
* @readonly
* @type {string | null}
*/
get sortKey() {
if (this.columns == null) {
return null;
}
return this.columns[this.sortIndex]?.key;
}
/**
* 获取当前选中的行对象
* @readonly
* @type {GridRowItem | null}
* @since 1.0.7
*/
get currentItem() {
return this.source[this.selectedIndex];
}
/**
* @private
* @type {HTMLTableRowElement[]}
*/
get _tableRows() {
return Array.from(this._var.refs.body.children).filter(r => r.classList.contains('ui-grid-row'));
}
/**
* @private
* @type {HTMLTableCellElement[]}
*/
get _headerCells() {
return Array.from(this._var.refs.header.children).filter(h => h.tagName === 'TH' && h.classList.contains('column'));
}
/**
* @private
* @type {HTMLTableCellElement[]}
*/
get _footerCells() {
return Array.from(this._var.refs.footer.children).filter(f => f.classList.contains('ui-grid-cell'));
}
/**
* 获取虚模式起始索引
* @readonly
* @type {number}
*/
get startIndex() { return this._var.startIndex }
/**
* 获取当前选中行的索引,为 -1 则当前没有选中行
* @readonly
* @type {number}
*/
get selectedIndex() { return (this._var.selectedIndexes && this._var.selectedIndexes[0]) ?? -1 }
/**
* 获取或设置 Grid 是否为只读
* @type {boolean}
*/
get readonly() { return this._var.readonly === true }
set readonly(flag) {
this._var.readonly = flag;
this.refresh();
}
/**
* 获取已过滤的数据数组,或者设置数据并刷新列表
* @type {GridRowItem[]}
*/
get source() { return this._var.currentSource?.map(s => s.values) ?? [] }
set source(list) {
if (!Array.isArray(list)) {
throw new Error('source is not an Array.')
}
list = list.map((it, index) => {
return {
__index: index,
values: it
};
});
this._var.source = list;
this._var.scrollLeft = 0;
if (this._var.el != null) {
this._var.el.scrollLeft = 0;
}
this._refreshSource(list);
}
/**
* 获取或设置合计行数据
* @type {GridRowItem}
* @since 1.0.1
*/
get total() { return this._var.total }
set total(total) {
this._var.total = total;
this.reload(true);
}
/**
* 获取或设置当前选中的行索引的数组,设置后会刷新列表
* @type {number[]}
*/
get selectedIndexes() { return this._var.selectedIndexes }
set selectedIndexes(indexes) {
const startIndex = this._var.startIndex;
this._var.selectedIndexes.splice(0, this._var.selectedIndexes.length, ...indexes);
if (this.readonly) {
this._tableRows.forEach((row, i) => {
if (indexes.includes(startIndex + i)) {
row.classList.add('selected');
} else if (row.classList.contains('selected')) {
row.classList.remove('selected');
}
});
} else {
this.refresh();
}
if (typeof this.onSelectedRowChanged === 'function') {
this.onSelectedRowChanged();
}
}
/**
* 获取或设置 Grid 当前的加载状态
* @type {boolean}
*/
get loading() { return this._var.refs.loading?.style?.visibility === 'visible' }
set loading(flag) {
if (this._var.refs.loading == null) {
return;
}
if (flag === false) {
this._var.refs.loading.style.visibility = 'hidden';
this._var.refs.loading.style.opacity = 0;
} else {
this._var.refs.loading.style.visibility = 'visible';
this._var.refs.loading.style.opacity = 1;
}
}
/**
* 获取或设置 Grid 当前滚动的偏移量
* @type {number}
*/
get scrollTop() { return this._var.el?.scrollTop; }
set scrollTop(top) {
if (this._var.el == null) {
return;
}
this._var.el.scrollTop = top;
this.reload(true);
}
/**
* 初始化Grid控件
* @param {HTMLElement} [container=.ctor#container] - 父容器元素,若未传值则采用构造方法中传入的父容器元素
*/
init(container = this._var.parent) {
if (container == null) {
throw new Error('no specified parent.');
}
if (!(container instanceof HTMLElement)) {
const ele = container[0];
if (!(ele instanceof HTMLElement)) {
throw new Error(`parent type not supported. ${JSON.stringify(Object.getPrototypeOf(container))}`);
}
container = ele;
}
if (!Array.isArray(this.columns)) {
throw new Error('no specified column definitions.');
}
if (Object.keys(this.langs).length === 0) {
this.langs = {
all: r('allItem', '( All )'),
ok: r('ok', 'OK'),
yes: r('yes', 'Yes'),
reset: r('reset', 'Reset'),
cancel: r('cancel', 'Cancel'),
null: r('null', '( Null )'),
addLevel: r('addLevel', 'Add level'),
deleteLevel: r('deleteLevel', 'Delete level'),
copyLevel: r('copyLevel', 'Copy level'),
sortBy: r('sortBy', 'Sort by'),
thenBy: r('thenBy', 'Then by'),
updateLayout: r('updateLayout', 'Update Layout'),
asc: r('asc', 'Ascending'),
desc: r('desc', 'Descending'),
column: r('column', 'Column'),
order: r('order', 'Order'),
sort: r('sort', 'Sort'),
sortArrayExists: r('sortArrayExists', 'This will remove the current tiered sort. Do you wish to continue?'),
requirePrompt: r('requirePrompt', 'All sort criteria must have a column specified. Check the selected sort criteria and try again.'),
duplicatePrompt: r('duplicatePrompt', '{column} is being sorted more than once. Delete the duplicate sort criteria and try again.')
};
}
this._var.el = null;
this._var.refs = {};
this._var.rendering = true;
this._var.parent = container;
this._var.isFirefox = /Firefox\//i.test(navigator.userAgent);
this._var.enabledDict = {};
const grid = createElement('div', 'ui-grid');
grid.setAttribute('tabindex', 0);
grid.addEventListener('keydown', e => {
let index = this.selectedIndex;
let flag = false;
if (e.key === 'ArrowUp') {
// up
if (index > 0) {
flag = true;
index -= 1;
}
} else if (e.key === 'ArrowDown') {
// down
const count = this._var.currentSource?.length ?? 0;
if (index < count - 1) {
flag = true;
index += 1;
}
}
if (flag) {
this._var.selectedIndexes = [index];
this.scrollToIndex(index);
this.refresh();
if (typeof this.onSelectedRowChanged === 'function') {
this.onSelectedRowChanged(index);
}
e.stopPropagation();
}
});
grid.addEventListener('mousedown', e => {
if (e.target === this._var.el) {
// cancel selections
const selectedIndexes = this._var.selectedIndexes;
if (selectedIndexes?.length > 0) {
selectedIndexes.splice(0);
}
if (this.readonly) {
this._tableRows.forEach(row => {
row.classList.remove('selected');
});
} else {
this.refresh();
}
if (typeof this.onSelectedRowChanged === 'function') {
this.onSelectedRowChanged(-1);
}
this._var.selectedColumnIndex = -1;
return;
}
let [parent, target] = this._getRowTarget(e.target);
if (parent == null) {
return;
}
if (this._getParentElement(parent) !== this._var.el) {
// sub ui-grid
return;
}
const rowIndex = parent.classList.contains('ui-grid-total-row') ? -1 : this._tableRows.indexOf(parent);
let colIndex = indexOfParent(target) - (this.expandable ? 1 : 0);
if (colIndex >= this.columns.length) {
colIndex = -1;
}
this._onRowClicked(e, rowIndex, colIndex);
});
if (this.rowDraggable) {
grid.addEventListener('dragover', e => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = e.ctrlKey ? 'copy' : 'move';
});
grid.addEventListener('drop', e => {
e.preventDefault();
e.stopPropagation();
const src = Number(e.dataTransfer.getData('text'));
if (isNaN(src) || src < 0) {
return;
}
const target = this._var.currentSource?.length ?? 0;
if (target == null || src === target) {
return;
}
const row = e.ctrlKey ?
Object.assign({}, this._var.currentSource[src]?.values) :
this.removeItem(src);
this.addItem(row, target);
this.selectedIndexes = [e.ctrlKey ? target : target - 1];
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('drag', [row], target);
}
});
}
container.replaceChildren(grid);
const sizer = createElement('span', 'ui-grid-sizer');
grid.appendChild(sizer);
this._var.refs.sizer = sizer;
grid.addEventListener('scroll', e => throttle(this._onScroll, RefreshInterval, this, e), { passive: true });
// header & body
const wrapper = createElement('div', 'ui-grid-wrapper');
this._var.refs.wrapper = wrapper;
grid.appendChild(wrapper);
const table = createElement('table', 'ui-grid-table');
this._var.refs.table = table;
this._createHeader(table);
this._createBody(table);
this._createFooter(table);
wrapper.appendChild(table);
// tooltip
if (!this.tooltipDisabled) {
const holder = createElement('div', 'ui-grid-hover-holder');
holder.addEventListener('mousedown', e => {
const holder = e.currentTarget;
const row = Number(holder.dataset.row);
const col = Number(holder.dataset.col);
if (holder.classList.contains('active')) {
holder.classList.remove('active');
this._clearHolder(holder);
}
return this._onRowClicked(e, row, col);
});
holder.addEventListener('dblclick', e => this._onRowDblClicked(e));
wrapper.appendChild(holder);
grid.addEventListener('mousemove', e => throttle(this._onGridMouseMove, HoverInternal, this, e, holder), { passive: true });
}
// loading
const loading = createElement('div', 'ui-grid-loading',
createElement('div', null, createIcon('fa-regular', 'spinner-third'))
);
this._var.refs.loading = loading;
grid.appendChild(loading);
this._var.el = grid;
this._var.rendering = false;
if (this._var.source != null) {
if (this.sortArray?.length > 0) {
this.sort(true);
} else if (this.sortIndex >= 0) {
this.sortColumn(true);
} else {
this.resize(true);
}
}
}
/**
* 设置数据列表,该方法为 [source]{@linkcode Grid#source} 属性的语法糖
* @param {GridRowItem[]} source - 待设置的数据列表
*/
setData(source) {
this.source = source;
}
/**
* 滚动到指定行的位置
* @param {number} index - 待滚动至的行索引
*/
scrollToIndex(index) {
const top = this._scrollToTop(index * (this.rowHeight + 1), true);
this._var.el.scrollTop = top;
}
/**
* 调整 Grid 元素的大小,一般需要在宽度变化时(如页面大小发生变化时)调用
* @param {boolean} [force] - 是否强制 [reload]{@linkcode Grid#reload},默认只有待渲染的行数发生变化时才会调用
* @param {boolean} [keep] - 是否保持当前滚动位置
* @param {Function} [callback] - 计算大小后的回调函数,上下文为 Grid
*/
resize(force, keep, callback) {
if (this._var.rendering || this._var.el == null) {
return;
}
const body = this._var.refs.body;
const top = this.headerVisible === false ? 0 : (this._var.refs.header.offsetHeight || this.rowHeight);
let height = this.height;
if (height === 0) {
height = this._var.containerHeight;
} else if (isNaN(height) || height < 0) {
height = this._var.el.offsetHeight - top;
}
const count = truncate((height - 1) / (this.rowHeight + 1)) + (RedumCount * 2) + 1;
if (force || count !== this._var.rowCount) {
this._var.rowCount = count;
if (typeof this.onBodyScrolled === 'function') {
if (!this.virtual) {
const tti = this._topToIndex(this._var.el.scrollTop);
this.onBodyScrolled(null, tti.index, count);
} else {
this.onBodyScrolled(null, this._var.startIndex, count);
}
}
if (typeof callback === 'function') {
callback.call(this);
} else {
this.reload(keep);
}
}
this._var.wrapClientWidth = this._var.refs.wrapper.clientWidth;
this._var.bodyClientWidth = body.clientWidth;
}
/**
* 重新计算需要渲染的行,并载入元素,一般需要在高度变化时调用
* @param {boolean} [keep] - 是否保持当前滚动位置
*/
reload(keep) {
if (this._var.rendering || this._var.el == null) {
return;
}
const filtered = this.columns.some(c => c.filterValues != null);
if ((filtered ^ this._var.colAttrs.__filtered) === 1) {
this._var.colAttrs.__filtered = filtered;
const headers = this._headerCells;
for (let i = 0; i < this.columns.length; ++i) {
const ele = headers[i].querySelector('.filter');
if (ele == null) {
continue;
}
if (this.columns[i].filterValues != null) {
ele.replaceChildren(createIcon('fa-solid', this.filteredIcon));
ele.classList.add('active');
} else {
ele.replaceChildren(createIcon('fa-solid', this.filterIcon));
ele.classList.remove('active');
}
}
this._refreshSource();
return;
} else if (filtered) {
const headers = this._headerCells;
for (let i = 0; i < this.columns.length; ++i) {
const ele = headers[i].querySelector('.filter');
if (ele == null) {
continue;
}
if (this.columns[i].filterValues != null) {
ele.replaceChildren(createIcon('fa-solid', this.filteredIcon));
ele.classList.add('active');
} else {
ele.replaceChildren(createIcon('fa-solid', this.filterIcon));
ele.classList.remove('active');
}
}
}
let length = this._var.currentSource?.length ?? 0;
if (this.extraRows > 0) {
length += this.extraRows;
}
this._var.containerHeight = length * (this.rowHeight + 1);
if (!keep) {
this._var.scrollTop = 0;
this._var.startIndex = 0;
this._var.el.scrollTop = 0;
this._var.refs.table.style.top = '0px';
}
this._adjustRows(this._var.refs.body);
this.refresh();
// size adjustment
const headerHeight = this._var.headerHeight || this.rowHeight;
if (this.total != null) {
const footerHeight = this._var.footerHeight || this.rowHeight;
this._var.refs.wrapper.style.height = `${headerHeight + this._var.containerHeight + footerHeight}px`;
// footer position
this._var.footerOffset = this._var.refs.table.offsetHeight - this._var.el.clientHeight;
this._var.refs.footer.parentElement.style.bottom = `${this._var.refs.table.offsetTop + this._var.footerOffset - this._var.el.scrollTop}px`;
} else {
this._var.refs.wrapper.style.height = `${headerHeight + this._var.containerHeight}px`;
}
}
/**
* 重新填充Grid单元格数据
*/
refresh() {
if (this._var.refs.body == null) {
throw new Error('body has not been created.');
}
const widths = {};
this._fillRows(this._tableRows, this.columns, widths);
if (this._var.needResize && widths.flag) {
this._var.needResize = false;
this.columns.forEach((col, i) => {
if (!this._get(col.key, 'autoResize')) {
return;
}
let width = widths[i];
if (width < col.width) {
width = col.width;
}
if (width > 0) {
this._changeColumnWidth(i, width, true);
}
});
}
this._layoutHeaderFooter();
}
/**
* 把所有行重置为未修改的状态
*/
resetChange() {
if (this._var.source == null) {
return;
}
for (let row of this._var.source) {
delete row.__changed;
}
}
/**
* 根据当前排序字段进行列排序
* @param {boolean} [reload] - 为 `true` 则在列排序后调用 [reload]{@linkcode Grid#reload} 方法
*/
sortColumn(reload) {
const index = this.sortIndex;
const col = this.columns[index];
if (col == null) {
return;
}
this.sortArray = null;
const direction = this.sortDirection;
[...this._headerCells].forEach((th, i) => {
const arrow = th.querySelector('.arrow');
if (arrow == null) {
return;
}
if (i === index) {
arrow.className = `arrow ${(direction !== 1 ? 'desc' : 'asc')}`;
} else if (arrow.className !== 'arrow') {
arrow.className = 'arrow';
}
});
const comparer = this._getComparer(col, direction);
this._var.source.sort(comparer);
if (this._var.colAttrs.__filtered === true) {
this._var.currentSource.sort(comparer);
}
if (this._var.rowCount < 0) {
return;
}
if (reload) {
this.reload();
} else {
this.refresh();
}
}
/**
* 根据当前排序列数组进行多列排序
* @param {boolean} [reload] - 为 `true` 则在多列排序后调用 [reload]{@linkcode Grid#reload} 方法
*/
sort(reload) {
const sortArray = this.sortArray;
if (sortArray == null || sortArray.length === 0) {
return;
}
this.sortIndex = -1;
const comparer = (a, b) => {
for (let i = 0; i < sortArray.length; ++i) {
const s = sortArray[i];
const col = this.columns.find(c => c.key === s.column && c.visible !== false);
if (col != null) {
const result = this._getComparer(col, s.order === 'desc' ? -1 : 1)(a, b);
if (result !== 0) {
return result;
}
}
}
return 0;
};
this._var.source.sort(comparer);
if (this._var.colAttrs.__filtered === true) {
this._var.currentSource.sort(comparer);
}
if (this._var.rowCount < 0) {
return;
}
if (reload) {
this.reload();
} else {
this.refresh();
}
// arrow icon
[...this._headerCells].forEach((th, i) => {
const arrow = th.querySelector('.arrow');
if (arrow == null) {
return;
}
const col = this.columns[i];
const s = sortArray.find(s => s.column === col.key && col.visible !== false);
if (s != null) {
arrow.className = `arrow ${s.order}`;
} else if (arrow.className !== 'arrow') {
arrow.className = 'arrow';
}
});
}
/**
* 清除列头复选框的选中状态
*/
clearHeaderCheckbox() {
const boxes = this._var.refs.header.querySelectorAll('.ui-check-wrapper>input');
boxes.forEach(box => box.checked = false);
}
/**
* 显示多列排序设置面板
* @param {Function} [callback] - 更新回调函数 *@since* 1.0.3
* @param {boolean} [layout] - 是否显示更新 layout 复选框 *@since* 1.0.3
* @since 1.0.1
*/
showSortPanel(callback, layout) {
const content = createElement('div', 'ui-sort-panel-content');
const buttonWrapper = createElement('div', 'ui-sort-panel-buttons');
const grid = new Grid(null, r);
grid.rowDraggable = true;
grid.langs = this.langs;
const rowChanged = index => {
buttonWrapper.querySelector('.ui-button-delete').disabled = index < 0;
buttonWrapper.querySelector('.ui-button-copy').disabled = index < 0;
// buttonWrapper.querySelector('.ui-button-move-up').disabled = index < 1;
// buttonWrapper.querySelector('.ui-button-move-down').disabled = index >= grid.source.length - 1;
};
grid.onSelectedRowChanged = rowChanged;
grid.onRowChanged = () => {
const layout = pop.container.querySelector('.ui-sort-layout');
if (layout != null) {
layout.classList.remove('disabled');
layout.querySelector('input').disabled = false;
}
};
const reload = index => {
grid.selectedIndexes = [index];
grid.scrollTop = index * (grid.rowHeight + 1);
rowChanged(index);
grid.onRowChanged();
}
buttonWrapper.append(
createElement('span', button => {
button.className = 'button';
button.addEventListener('click', () => {
let index = grid.selectedIndex;
const n = { column: '', order: 'asc' };
if (index >= 0) {
index += 1;
grid.addItem(n, index);
} else {
grid.addItem(n);
index = grid.source.length - 1;
}
reload(index);
});
},
createIcon('fa-light', 'plus'),
createElement('span', span => {
span.innerText = this.langs.addLevel;
})
),
createElement('span', button => {
button.className = 'button ui-button-delete';
button.addEventListener('click', () => {
let index = grid.selectedIndex;
if (index < 0) {
return;
}
grid.removeItem(index);
const length = grid.source.length;
if (index >= length) {
index = length - 1;
}
reload(index);
});
},
createIcon('fa-light', 'times'),
createElement('span', span => {
span.innerText = this.langs.deleteLevel;
})
),
createElement('span', button => {
button.className = 'button ui-button-copy';
button.addEventListener('click', () => {
const index = grid.selectedIndex;
if (index < 0) {
return;
}
const item = grid.source[index];
if (item == null) {
return;
}
grid.addItem(Object.assign({}, item), index + 1);
reload(index + 1);
});
},
createIcon('fa-light', 'copy'),
createElement('span', span => {
span.innerText = this.langs.copyLevel;
})
),
/*
createElement('span', button => {
button.className = 'button ui-button-move-up';
const icon = createIcon('fa-light', 'chevron-up');
icon.addEventListener('click', () => {
const index = grid.selectedIndex;
if (index < 1) {
return;
}
const item = grid.source[index];
if (item == null) {
return;
}
const it = grid.removeItem(index);
grid.addItem(it, index - 1);
reload(index - 1);
});
button.appendChild(icon);
}),
createElement('span', button => {
button.className = 'button ui-button-move-down';
const icon = createIcon('fa-light', 'chevron-down');
icon.addEventListener('click', () => {
const index = grid.selectedIndex;
if (index >= grid.source.length - 1) {
return;
}
const item = grid.source[index];
if (item == null) {
return;
}
const it = grid.removeItem(index);
grid.addItem(it, index + 1);
reload(index + 1);
});
button.appendChild(icon);
})
//*/
);
const gridWrapper = createElement('div', 'ui-sort-panel-grid');
content.append(buttonWrapper, gridWrapper);
const columnSource = this.columns.filter(c => c.sortable !== false); // ticket 56389, && c.visible !== false
columnSource.sort((a, b) => a.caption > b.caption ? 1 : -1);
grid.columns = [
{
width: 80,
sortable: false,
orderable: false,
resizable: false,
filter: (_item, _editing, _body, index) => index === 0 ? this.langs.sortBy : this.langs.thenBy
},
{
key: 'caption',
caption: this.langs.column,
width: 270,
type: GridColumnTypeEnum.Dropdown,
dropOptions: {
textKey: 'caption',
valueKey: 'caption',
input: true
},
dropRestrictCase: true,
sourceCache: false,
source: () => columnSource.filter(c => grid.source?.find(s => s.column === c.key) == null),
sortable: false,
orderable: false,
onChanged: (item, _value, _oldValue, e) => {
item.column = e.key;
const layout = pop.container.querySelector('.ui-sort-layout');
if (layout != null) {
layout.classList.remove('disabled');
layout.querySelector('input').disabled = false;
}
}
},
{
key: 'order',
caption: this.langs.order,
width: 150,
type: GridColumnTypeEnum.Dropdown,
source: [
{ value: 'asc', text: this.langs.asc },
{ value: 'desc', text: this.langs.desc }
],
sortable: false,
orderable: false,
onChanged: () => {
const layout = pop.container.querySelector('.ui-sort-layout');
if (layout != null) {
layout.classList.remove('disabled');
layout.querySelector('input').disabled = false;
}
}
}
];
const pop = new Popup({
title: this.langs.sort,
content,
resizable: true,
buttons: [
{
text: this.langs.ok,
trigger: () => {
const source = grid.source;
if (source == null || source.length === 0) {
this.sortArray = null;
this.sortIndex = -1;
this.sortDirection = GridColumnDirection.Ascending;
// arrow icon
[...this._headerCells].forEach((th, i) => {
const arrow = th.querySelector('.arrow');
if (arrow == null) {
return;
}
arrow.className = 'arrow';
});
} else {
const dict = {};
for (let i = 0; i < source.length; ++i) {
const it = source[i];
if (it.column == null || it.column === '') {
grid.selectedIndexes = [i];
grid.refresh();
showAlert(this.langs.sort, this.langs.requirePrompt, 'warn');
return false;
}
if (Object.prototype.hasOwnProperty.call(dict, it.column)) {
grid.selectedIndexes = [i];
grid.refresh();
let name = columnSource.find(c => c.key === it.column);
if (name == null) {
name = it.column;
} else {
name = name.caption;
}
showAlert(this.langs.sort, this.langs.duplicatePrompt.replace('{column}', name), 'warn');
return false;
}
dict[it.column] = true;
}
this.sortArray = source.map(s => ({ column: s.column, order: s.order }));
this.sortDirection = 1;
this.sort();
}
if (typeof callback === 'function') {
callback.call(this, this.sortArray, pop.container.querySelector('.ui-sort-layout>input')?.checked);
}
return true;
}
},
{ text: this.langs.cancel }
],
onResizeEnded: () => grid.resize()
});
let source = this.sortArray;
if (source == null && this.sortIndex >= 0) {
const col = this.columns[this.sortIndex];
if (col != null) {
source = [{ column: col.key, order: this.sortDirection > 0 ? 'asc' : 'desc' }];
}
}
source ??= [{ column: '', order: 'asc' }];
pop.create();
pop.rect = { width: 600, height: 460 };
pop.show(this._var.el).then(() => {
if (layout) {
const footer = pop.container.querySelector('.ui-popup-footer');
footer.insertBefore(createCheckbox({
label: this.langs.updateLayout,
className: 'ui-sort-layout',
switch: true,
enabled: false
}), footer.children[0]);
}
grid.init(gridWrapper);
grid.source = source
.map(s => (s.c = columnSource.find(c => c.key === s.column), s))
.filter(s => s.column === '' || s.c != null)
.map(s => ({ column: s.column, caption: s.c?.caption ?? '', order: s.order }));
grid.selectedIndexes = [0];
grid.refresh();
rowChanged(0);
});
}
/**
* 设置单行数据
* @param {number} index - 行索引
* @param {GridRowItem} item - 待设置的行数据对象
* @since 1.0.1
*/
setItem(index, item) {
if (this._var.currentSource == null) {
throw new Error('no source');
}
const it = this._var.currentSource[index];
// clear dropdown source cache
// FIXME: 清除缓存会导致选中状态下动态数据源下拉列表显示为空
// delete it.source;
it.values = item;
if (this.sortArray?.length > 0) {
this.sort();
} else if (this.sortIndex >= 0) {
this.sortColumn();
} else {
this.refresh();
}
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('update', [item], index);
}
}
/**
* 添加行数据
* @param {GridRowItem} item - 待添加的行数据值
* @param {number} [index] - 待添加的行索引
* @returns {GridRowItem} 返回已添加的行数据
* @since 1.0.1
*/
addItem(item, index) {
if (this._var.currentSource == null) {
throw new Error('no source');
}
const it = index >= 0 ? this._var.currentSource[index] : null;
const newIt = { __index: null, values: item };
if (it != null) {
newIt.__index = it.__index;
this._var.currentSource.splice(index, 0, newIt);
if (this._var.colAttrs.__filtered === true) {
this._var.source.splice(it.__index, 0, newIt);
}
for (let i = it.__index + 1; i < this._var.source.length; ++i) {
this._var.source[i].__index += 1;
}
} else {
newIt.__index = this._var.source.length;
this._var.currentSource.push(newIt);
if (this._var.colAttrs.__filtered === true) {
this._var.source.push(newIt);
}
}
if (this.sortArray?.length > 0) {
this.sort(true);
} else if (this.sortIndex >= 0) {
this.sortColumn(true);
} else {
this.reload();
}
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('add', [item], index);
}
return item;
}
/**
* 批量添加行数据
* @param {GridRowItem[]} array - 待添加的行数据数组
* @param {number} [index] - 待添加的行索引
* @returns {GridRowItem[]} 返回已添加的行数据数组
* @since 1.0.1
*/
addItems(array, index) {
if (this._var.currentSource == null) {
throw new Error('no source');
}
if (!Array.isArray(array) || array.length <= 0) {
// throw new Error(`invalid items array: ${array}`);
return;
}
const it = index >= 0 ? this._var.currentSource[index] : null;
if (it != null) {
const items = array.map((a, i) => ({ __index: it.__index + i, values: a }));
this._var.currentSource.splice(index, 0, ...items);
if (this._var.colAttrs.__filtered === true) {
this._var.source.splice(it.__index, 0, ...items);
}
const offset = array.length;
for (let i = it.__index + offset; i < this._var.source.length; ++i) {
this._var.source[i].__index += offset;
}
} else {
const length = this._var.source.length;
const items = array.map((a, i) => ({ __index: length + i, values: a }));
this._var.currentSource.push(...items);
if (this._var.colAttrs.__filtered === true) {
this._var.source.push(...items);
}
}
if (this.sortArray?.length > 0) {
this.sort(true);
} else if (this.sortIndex >= 0) {
this.sortColumn(true);
} else {
this.reload();
}
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('add', array, index);
}
return array;
}
/**
* 删除行数据
* @param {number} index - 待删除的行索引
* @returns {GridRowItem} 返回已删除的行数据
* @since 1.0.1
*/
removeItem(index) {
if (this._var.currentSource == null) {
throw new Error('no source');
}
const it = this._var.currentSource.splice(index, 1)[0];
if (it == null) {
return null;
}
if (this._var.colAttrs.__filtered === true) {
this._var.source.splice(it.__index, 1);
}
for (let i = it.__index; i < this._var.source.length; ++i) {
this._var.source[i].__index -= 1;
}
if (index < 1) {
this._var.selectedIndexes = [index - 1];
} else {
this._var.selectedIndexes = [];
}
this.reload();
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('remove', [it.values], index);
}
return it.values;
}
/**
* 批量删除行数据
* @param {number[]} [indexes] - 待删除的行索引数组,未传值时删除所有行
* @returns {GridRowItem[]} 返回已删除的行数据数组
* @since 1.0.1
*/
removeItems(indexes) {
if (this._var.currentSource == null) {
throw new Error('no source');
}
if (Array.isArray(indexes) && indexes.length > 0) {
indexes = indexes.slice().sort();
} else {
indexes = this._var.currentSource.map(a => a.__index);
}
const array = [];
let first = 0;
for (let i = indexes.length - 1; i >= 0; --i) {
let it = this._var.currentSource.splice(indexes[i], 1)[0];
if (it == null) {
continue;
}
let next = this._var.source[it.__index];
if (next != null && next.__offset == null) {
next.__offset = i + 1;
}
if (this._var.colAttrs.__filtered === true) {
this._var.source.splice(it.__index, 1);
}
array.splice(0, 0, it.values);
first = it.__index;
}
let offset = 1;
for (let i = first; i < this._var.source.length; ++i) {
let it = this._var.source[i];
if (it.__offset > 0) {
offset = it.__offset;
delete it.__offset;
}
it.__index -= offset;
}
const index = indexes[0];
if (index > 0) {
this._var.selectedIndexes = [index - 1];
} else {
this._var.selectedIndexes = [];
}
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('remove', array, indexes);
}
this.reload();
return array;
}
/**
* 导出已压缩的数据源,结构为
* ```
* {
* columns: [],
* source: [],
* rowHeight: number,
* sortDirection: number,
* sortKey?: string,
* sortArray?: Array<{
* column: string,
* order: "asc" | "desc"
* }>
* }
* ```
* @param {string | boolean} [compressed=deflate] - 压缩编码,传入 false 则取消压缩
* @param {string} [module] - 压缩模块,默认采用 wasm_flate.deflate 编码,自定义模块需要实现以下消息事件
* * `onmessage` - 接收消息
* * `{ type: 'init', path: string}` - 初始化消息,参数 `path` 为 `ui.min.js` 脚本所在路径
* * `{ type: 'compress', data: Uint8Array }` - 压缩消息,参数 `data` 为原始数据
* * `postMessage` - 返回消息
* * `{ type: 'init', result?: 0, error?: string}` - 初始化事件中的消息反馈,没有 `error` 意为成功
* * `{ error: string }` - 压缩事件中的消息反馈,`error` 值为错误信息
* * `Uint8Array` - 反馈压缩后的数据
* @returns {Promise<GridExportData>} 返回 `Uint8Array` 数据对象
* @since 1.0.2
*/
export(compressed, module) {
const data = {
columns: this.columns.map(c => ({
key: c.key,
type: c.type?.toString(),
caption: c.caption,
width: c.width,
align: c.align,
visible: c.visible
})),
source: this.source?.map((s, i) => {
const item = Object.create(null);
for (let c of this.columns) {
if (c.key == null) {
continue;
}
let val;
if (c.text != null) {
val = c.text;
} else if (typeof c.filter === 'function') {
val = c.filter(s, false, this._var.refs.body, i);
} else {
val = s[c.key];
if (val != null) {
if (Object.prototype.hasOwnProperty.call(val, 'DisplayValue') && val.Value === val.DisplayValue) {
val = val.DisplayValue;
} else if (Array.isArray(val)) {
val = val.join(', ');
}
}
}
val ??= '';
item[c.key] = val;
}
for (let prop of Object.keys(s)) {
if (Object.prototype.hasOwnProperty.call(item, prop)) {
continue;
}
let val;
val = s[prop];
if (val != null && Object.prototype.hasOwnProperty.call(val, 'DisplayValue')) {
val = val.DisplayValue;
}
item[prop] = val;
}
return item;
}) ?? [],
total: ((total) => {
if (total == null) {
return null;
}
const item = Object.create(null);
for (let c of this.columns) {
if (c.key == null) {
continue;
}
let val = total[c.key];
if (val != null && Object.prototype.hasOwnProperty.call(val, 'DisplayValue') && val.Value === val.DisplayValue) {
val = val.DisplayValue;
}
val ??= '';
item[c.key] = val;
}
for (let prop of Object.keys(total)) {
if (Object.prototype.hasOwnProperty.call(item, prop)) {
continue;
}
let val;
val = total[prop];
if (val != null && Object.prototype.hasOwnProperty.call(val, 'DisplayValue')) {
val = val.DisplayValue;
}
item[prop] = val;
}
return item;
})(this.total),
rowHeight: this.rowHeight,
sortDirection: this.sortDirection,
sortKey: this.sortKey,
sortArray: this.sortArray
};
const json = JSON.stringify(data);
if (compressed === false) {
return Promise.resolve(Encoder.encode(json));
}
return new Promise(resolve => {
let working;
let url;
if (typeof module === 'string') {
url = `${ScriptPath}${module}`;
} else {
url = URL.createObjectURL(new Blob([`let wasm,WASM_VECTOR_LEN=0,cachegetUint8Memory0=null;function getUint8Memory0(){return null!==cachegetUint8Memory0&&cachegetUint8Memory0.buffer===wasm.memory.buffer||(cachegetUint8Memory0=new Uint8Array(wasm.memory.buffer)),cachegetUint8Memory0}let cachegetInt32Memory0=null;function getInt32Memory0(){return null!==cachegetInt32Memory0&&cachegetInt32Memory0.buffer===wasm.memory.buffer||(cachegetInt32Memory0=new Int32Array(wasm.memory.buffer)),cachegetInt32Memory0}function passArray8ToWasm0(e,t){const a=t(1*e.length);return getUint8Memory0().set(e,a/1),WASM_VECTOR_LEN=e.length,a}function getArrayU8FromWasm0(e,t){return getUint8Memory0().subarray(e/1,e/1+t)}function encode_raw(e,t){var a=passArray8ToWasm0(t,wasm.__wbindgen_malloc),r=WASM_VECTOR_LEN;wasm[e+"_encode_raw"](8,a,r);var s=getInt32Memory0()[2],n=getInt32Memory0()[3],m=getArrayU8FromWasm0(s,n).slice();return wasm.__wbindgen_free(s,1*n),m}self.addEventListener("message",e=>{const t=e.data.type;if("init"===t)if("function"==typeof WebAssembly.instantiateStreaming){const t={},a=fetch(e.data.path+"wasm_flate_bg.wasm");WebAssembly.instantiateStreaming(a,t).then(({instance:e})=>{wasm=e.exports,self.postMessage({type:"init",result:0})}).catch(e=>a.then(t=>{"application/wasm"!==t.headers.get("Content-Type")?self.postMessage({type:"init",error:"\`WebAssembly.instantiateStreaming\` failed because your server does not serve wasm with \`application/wasm\` MIME type. Original error: "+e.message}):self.postMessage({type:"init",error:e.message})}))}else self.postMessage({type:"init",error:"no \`WebAssembly.instantiateStreaming\`"});else if("compress"===t)if(null==wasm)self.postMessage({error:"no \`wasm\` instance"});else{let t=encode_raw("${compressed ?? 'deflate'}",e.data.data);self.postMessage(t,[t.buffer])}});`]));
}
const worker = new Worker(url);
/**
* @private
* @param {Function} next
* @param {any} data
*/
const terminate = (next, data) => {
working = false;
worker.terminate();
URL.revokeObjectURL(url);
next(data);
}
// 超过 30 秒则返回无压缩数据
const timer = setTimeout(() => {
if (working) {
// terminate(reject, { message: 'timeout' });
terminate(resolve, { data: Encoder.encode(json), error: 'timeout' });
}
}, 30000);
worker.addEventListener('message', e => {
if (working) {
if (e.data.error != null) {
// terminate(reject, { message: e.data.error });
terminate(resolve, { data: Encoder.encode(json), error: e.data.error });
} else {
if (e.data.type === 'init') {
const uncompressed = Encoder.encode(json);
worker.postMessage({ type: 'compress', data: uncompressed }, [uncompressed.buffer]);
} else {
clearTimeout(timer);
terminate(resolve, { type: 'compressed', data: e.data });
}
}
}
})
worker.addEventListener('error', e => {
if (working) {
clearTimeout(timer);
// terminate(reject, e);
terminate(resolve, { data: Encoder.encode(json), error: e.message });
}
})
working = true;
worker.postMessage({ type: 'init', path: ScriptPath });
});
}
/**
* 展开/收折所有行
* @param {boolean} [expanded] - 是否展开,传 null 则切换展开状态
* @returns {boolean} 展开状态
*/
expandAll(expanded) {
if (this._var.currentSource == null) {
return;
}
if (expanded == null) {
expanded = !this._var.expanded;
} else {
expanded = expanded !== false;
}
this._var.expanded = expanded;
for (let vals of this._var.currentSource) {
vals.__expanded = expanded;
}
this.refresh();
return expanded;
}
/**
* @private
* @callback PrivateGridComparerCallback
* @param {GridItemWrapper} a
* @param {GridItemWrapper} b
* @returns {number}
*/
/**
* @private
* @param {GridColumnDefinition} col
* @param {GridColumnDirection} direction
* @returns {PrivateGridComparerCallback}
*/
_getComparer(col, direction) {
if (typeof col.sortFilter !== 'function') {
if (isNaN(direction)) {
direction = 1;
}
const editing = col.sortAsText !== true;
const comparer = (a, b) => {
a = this._getItemSortProp(a, editing, col);
b = this._getItemSortProp(b, editing, col);
if (editing) {
if (typeof a === 'boolean') {
a = a ? 2 : 1;
}
if (typeof b === 'boolean') {
b = b ? 2 : 1;
}
if (a == null && typeof b === 'number') {
return (b >= 0 ? -1 : 1);
} else if (typeof a === 'number' && b == null) {
return (a >= 0 ? 1 : -1);
} else if (a == null && b != null) {
return -1;
} else if (a != null && b == null) {
return 1;
}
if (Array.isArray(a)) {
a = a.join(', ');
}
if (Array.isArray(b)) {
b = b.join(', ');
}
if (typeof a === 'string' && typeof b === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
} else {
if (a == null && b != null) {
return -1;
}
if (a != null && b == null) {
return 1;
}
if (Array.isArray(a)) {
a = a.join(', ');
}
if (Array.isArray(b)) {
b = b.join(', ');
}
if (typeof a === 'string' && typeof b === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
}
return a === b ? 0 : (a > b ? 1 : -1);
};
return (a, b) => comparer(a.values, b.values) * direction;
}
return (a, b) => col.sortFilter(a.values, b.values) * direction;
}
/**
* @private
* @param {GridItemWrapper[]} [list]
*/
_refreshSource(list) {
list ??= this._var.source;
if (this._var.colAttrs.__filtered === true) {
this._var.currentSource = list.filter(it => {
for (let col of this.columns) {
const nullValue = col.filterAllowNull ? null : '';
if (Array.isArray(col.filterValues)) {
const v = this._getItemProp(it.values, false, col) ?? nullValue;
if (Array.isArray(v)) {
if (v.every(item => col.filterValues.indexOf(item) < 0)) {
return false;
}
} else if (col.filterValues.indexOf(v) < 0) {
return false;
}
}
}
return true;
});
} else {
this._var.currentSource = list;
}
this._var.selectedColumnIndex = -1;
this._var.selectedIndexes = [];
this._var.startIndex = 0;
this._var.scrollTop = 0;
this._var.rowCount = -1;
this.resize(true, false, () => {
if (this.sortArray?.length > 0) {
this.sort(true);
} else if (this.sortIndex >= 0) {
this.sortColumn(true);
} else {
this.reload();
}
});
}
/**
* @private
* @param {HTMLTableElement} table
* @returns {HTMLTableSectionElement}
*/
_createHeader(table) {
const thead = createElement('thead');
if (this.headerVisible === false) {
thead.style.display = 'none';
}
table.appendChild(thead);
const header = createElement('tr');
thead.appendChild(header);
const sizer = this._var.refs.sizer;
let left = this.expandable ? ExpandableWidth : 0;
const readonly = this.readonly;
if (this.expandable) {
header.appendChild(createElement('th', th => {
th.className = 'ui-expandable sticky';
const w = `${ExpandableWidth}px`;
th.style.cssText = convertCssStyle({
'width': w,
'max-width': w,
'min-width': w,
'left': '0px'
});
}, createElement('div')));
}
for (let col of this.columns) {
if (col.visible === false) {
const hidden = createElement('th', 'column');
hidden.style.display = 'none';
if (col.sortable !== false) {
hidden.dataset.key = col.key;
hidden.addEventListener('click', e => this._onHeaderClicked(e, col, true));
}
header.appendChild(hidden);
continue;
}
// style
const alwaysEditing = GridColumnTypeEnum.isAlwaysEditing(col.type);
let type = this._var.colTypes[col.key];
if (type == null) {
if (isNaN(col.type)) {
type = col.type;
} else {
type = ColumnTypeDefs[col.type];
}
type ??= GridColumn;
this._var.colTypes[col.key] = type;
}
if (col.width > 0 || col.shrink || !this.autoResize || typeof type.createCaption === 'function') {
// col.autoResize = false;
if (isNaN(col.width) || col.width <= 0) {
col.width = 50;
}
} else {
this._set(col.key, 'autoResize', true);
this._var.needResize = true;
sizer.innerText = col.caption ?? '';
let width = sizer.offsetWidth + 22;
if (!readonly && col.enabled !== false && col.allcheck && alwaysEditing) {
width += 32;
}
if (col.allowFilter === true) {
width += 14;
}
if (width < MiniColumnWidth) {
width = MiniColumnWidth;
}
col.width = width;
}
col.align ??= alwaysEditing ? 'center' : 'left';
if (col.sortable !== false) {
col.sortable = true;
}
let style;
if (col.shrink) {
style = { 'text-align': col.align };
} else {
const w = `${col.width}px`;
style = {
'width': w,
'max-width': w,
'min-width': w,
'text-align': col.align
};
}
this._set(col.key, 'style', style);
// element
const th = createElement('th', 'column');
const thStyle = { ...style };
if (col.isfixed) {
th.classList.add('sticky');
thStyle.left = `${left}px`;
}
left += col.width;
th.dataset.key = col.key;
if (col.sortable) {
thStyle.cursor = 'pointer';
th.addEventListener('click', e => this._onHeaderClicked(e, col));
}
th.style.cssText = convertCssStyle(thStyle);
if (col.orderable !== false) {
col.orderable = true;
th.addEventListener('mousedown', e => this._onDragStart(e, col));
}
const wrapper = createElement('div');
if (col.align === 'right') {
wrapper.style.justifyContent = 'flex-end';
} else if (col.align === 'center') {
wrapper.style.justifyContent = 'center';
}
th.appendChild(wrapper);
if (!readonly && col.enabled !== false && col.allcheck && alwaysEditing) {
const check = createCheckbox({
switch: col.switch,
onchange: e => this._onColumnAllChecked(col, e.target.checked)
});
wrapper.appendChild(check);
}
let caption;
if (typeof type.createCaption === 'function') {
caption = type.createCaption(col);
} else {
caption = createElement('span');
caption.innerText = col.caption ?? '';
}
if (caption instanceof HTMLElement) {
if (this.headerWrap) {
caption.classList.add('wrap');
}
if (col.captionStyle != null) {
caption.style.cssText = convertCssStyle(col.captionStyle);
}
wrapper.appendChild(caption);
}
if (col.captionTooltip != null) {
const help = createIcon('fa-solid', 'question-circle');
wrapper.appendChild(help);
setTooltip(help, col.captionTooltip, false, this._var.parent);
}
// order arrow
if (col.sortable) {
th.appendChild(createElement('layer', 'arrow'));
}
// filter
if (col.allowFilter === true) {
const filter = createElement('layer', 'filter');
filter.appendChild(createIcon('fa-solid', this.filterIcon));
filter.addEventListener('mousedown', e => this._onFilter(e, col));
th.classList.add('header-filter');
th.appendChild(filter);
}
// resize spliter
if (col.resizable !== false) {
const spliter = createElement('layer', 'spliter');
spliter.addEventListener('mousedown', e => this._onResizeStart(e, col));
spliter.addEventListener('dblclick', e => this._onAutoResize(e, col));
th.appendChild(spliter);
}
// bottom border
th.appendChild(createElement('layer', 'bottom-border'));
// tooltip
// !nullOrEmpty(col.tooltip) && setTooltip(th, col.tooltip);
header.appendChild(th);
}
const dragger = createElement('div', 'dragger');
const draggerCursor = createElement('layer', 'dragger-cursor');
header.appendChild(
createElement('th', null,
dragger, draggerCursor, createElement('div'), createElement('layer', 'bottom-border')));
sizer.replaceChildren();
this._var.refs.header = header;
this._var.refs.dragger = dragger;
this._var.refs.draggerCursor = draggerCursor;
return thead;
}
/**
* @private
* @param {HTMLTableElement} table
* @returns {HTMLTableSectionElement}
*/
_createBody(table) {
const body = createElement('tbody');
table.appendChild(body);
const cols = this.columns;
let width = 1;
for (let col of cols) {
if (col.visible !== false && !isNaN(col.width)) {
width += col.width + 1;
}
}
if (this.expandable) {
width += ExpandableWidth;
}
table.style.width = `${width}px`;
// body content
body.addEventListener('dblclick', e => this._onRowDblClicked(e));
// this._adjustRows(body);
this._var.refs.body = body;
// this.refresh();
return body;
}
/**
* @private
* @param {HTMLTableElement} table
* @returns {HTMLTableSectionElement}
*/
_createFooter(table) {
const tfoot = createElement('tfoot');
tfoot.style.display = 'none';
table.appendChild(tfoot);
tfoot.addEventListener('dblclick', e => this._onRowDblClicked(e));
const footer = createElement('tr', 'ui-grid-row ui-grid-total-row');
tfoot.appendChild(footer);
let left = this.expandable ? ExpandableWidth : 0;
if (this.expandable) {
footer.appendChild(createElement('td', td => {
td.className = 'ui-expandable sticky';
const w = `${ExpandableWidth}px`;
td.style.cssText = convertCssStyle({
'width': w,
'max-width': w,
'min-width': w,
'left': '0px'
});
}, createElement('div')));
}
this.columns.forEach((col, j) => {
const cell = createElement('td', 'ui-grid-cell');
if (col.visible !== false) {
let style = this._get(col.key, 'style') ?? {};
if (col.isfixed) {
cell.classList.add('sticky');
style.left = `${left}px`;
}
left += col.width;
cell.dataset.col = String(j);
if (col.totalCss != null) {
style = { ...style, ...col.totalCss };
}
style = convertCssStyle(style);
if (style !== '') {
cell.style.cssText = style;
}
const element = GridColumn.create(col);
if (typeof col.class === 'string') {
GridColumn.setClass(element, col.class);
}
if (col.contentWrap) {
element.classList.add('wrap');
}
cell.appendChild(element);
} else {
cell.style.display = 'none';
}
footer.appendChild(cell);
})
footer.appendChild(createElement('td', td => td.innerText = '\u00a0'));
this._var.refs.footer = footer;
return tfoot;
}
/**
* @private
* @param {HTMLTableSectionElement} content
*/
_adjustRows(content) {
let count = this._var.rowCount;
if (isNaN(count) || count < 0 || !this.virtual) {
count = this._var.currentSource?.length ?? 0;
}
const cols = this.columns;
const rows = Array.from(content.children).filter(r => r.classList.contains('ui-grid-row'));
const exists = rows.length;
count -= exists;
if (count > 0) {
const readonly = this.readonly;
for (let i = 0; i < count; ++i) {
const row = createElement('tr', 'ui-grid-row');
if (this.rowDraggable) {
row.draggable = true;
row.addEventListener('dragstart', e => {
e.dataTransfer.setData('text', String(this._var.startIndex + exists + i));
});
row.addEventListener('dragover', e => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = e.ctrlKey ? 'copy' : 'move';
});
row.addEventListener('drop', e => {
e.preventDefault();
e.stopPropagation();
const src = Number(e.dataTransfer.getData('text'));
if (isNaN(src) || src < 0) {
return;
}
const target = this._var.startIndex + exists + i;
if (src === target) {
return;
}
const row = e.ctrlKey ?
Object.assign({}, this._var.currentSource[src]?.values) :
this.removeItem(src);
this.addItem(row, target);
this.selectedIndexes = [target];
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('drag', [row], target);
}
});
}
const virtualRow = { cells: {} };
this._var.virtualRows[exists + i] = virtualRow;
let left = this.expandable ? ExpandableWidth : 0;
if (this.expandable) {
const icon = createIcon('fa-solid', 'caret-right');
icon.dataset.expanded = '0';
row.appendChild(createElement('td', td => {
td.className = 'ui-expandable sticky';
td.style.cssText = 'left: 0px';
td.addEventListener('mousedown', e => {
this._onExpandable(e, exists + i, row);
e.stopPropagation();
});
},
icon
));
}
cols.forEach((col, j) => {
const cell = createElement('td', 'ui-grid-cell');
virtualRow.cells[col.key ?? j] = { style: '' };
if (col.visible !== false) {
let style = this._get(col.key, 'style') ?? {};
if (col.isfixed) {
cell.classList.add('sticky');
style.left = `${left}px`;
}
left += col.width;
cell.dataset.row = String(exists + i);
cell.dataset.col = String(j);
if (col.css != null) {
style = { ...style, ...col.css };
}
style = convertCssStyle(style);
if (style !== '') {
cell.style.cssText = style;
}
let type = this._var.colTypes[col.key];
if (type == null) {
if (isNaN(col.type)) {
type = col.type;
} else {
type = ColumnTypeDefs[col.type];
}
type ??= GridColumn;
this._var.colTypes[col.key] = type;
}
let element;
if (!readonly && GridColumnTypeEnum.isAlwaysEditing(col.type)) {
element = type.createEdit(e => this._onRowChanged(e, exists + i, col, e.target.checked, cell), col, exists + i);
} else {
element = type.create(col, exists + i, this);
if (typeof col.class === 'string') {
type.setClass(element, col.class);
}
if (col.contentWrap) {
element.classList.add('wrap');
}
}
cell.appendChild(element);
if (col.events != null) {
for (let ev of Object.entries(col.events)) {
element[ev[0]] = e => {
const item = this._var.currentSource[this._var.startIndex + exists + i].values;
ev[1].call(item, e);
};
}
}
} else {
cell.style.display = 'none';
}
row.appendChild(cell);
});
row.appendChild(createElement('td', td => td.innerText = '\u00a0'));
content.appendChild(row);
}
} else if (count < 0) {
let last = rows[exists + count];
while (last != null) {
const next = last.nextElementSibling;
last.remove();
last = next;
}
}
}
/**
* @private
* @param {HTMLTableRowElement[]} rows
* @param {GridColumnDefinition[]} cols
* @param {any} [widths]
*/
_fillRows(rows, cols, widths) {
const startIndex = this._var.startIndex;
const selectedIndexes = this._var.selectedIndexes;
const offset = this.expandable ? 1 : 0;
const readonly = this.readonly;
rows.forEach((row, i) => {
const vals = this._var.currentSource[startIndex + i];
if (vals == null) {
return;
}
if (!isPositive(row.children.length)) {
return;
}
const virtualRow = this._var.virtualRows[i];
const item = vals.values;
const selected = selectedIndexes.includes(startIndex + i);
if (selected) {
row.classList.add('selected');
} else if (row.classList.contains('selected')) {
row.classList.remove('selected');
}
const stateChanged = virtualRow.editing !== selected;
virtualRow.editing = selected;
// data
if (this.expandable) {
const expanded = vals.__expanded;
let rowExpanded = row.nextElementSibling;
if (rowExpanded?.className !== 'ui-grid-row-expanded') {
rowExpanded = null;
}
if (expanded) {
let expandableObject = vals.__expandable_object;
if (expandableObject == null && typeof this.expandableGenerator === 'function') {
expandableObject = this.expandableGenerator(item);
if (expandableObject?.element == null) {
return;
}
expandableObject.element = createElement('td', td => {
td.colSpan = cols.length + 2;
},
expandableObject.element
);
vals.__expandable_object = expandableObject;
}
if (rowExpanded == null) {
rowExpanded = createElement('tr', 'ui-grid-row-expanded');
this._var.refs.body.insertBefore(rowExpanded, row.nextElementSibling);
} else {
rowExpanded.style.display = '';
}
rowExpanded.replaceChildren(expandableObject.element);
} else {
if (rowExpanded != null) {
rowExpanded.style.display = 'none';
}
}
const iconCell = row.children[0];
if (iconCell.children[0].dataset.expanded !== (expanded ? '1' : '0')) {
const icon = createIcon('fa-solid', expanded ? 'caret-down' : 'caret-right');
icon.dataset.expanded = expanded ? '1' : '0';
iconCell.replaceChildren(icon);
if (expanded) {
if (typeof this.onRowExpanded === 'function') {
this.onRowExpanded(vals.values, vals.__expandable_object);
}
} else {
if (typeof this.onRowCollapsed === 'function') {
this.onRowCollapsed(vals.values, vals.__expandable_object);
}
}
}
}
cols.forEach((col, j) => {
if (col.visible === false) {
return;
}
const cell = row.children[j + offset];
if (cell == null) {
return;
}
const virtualCell = virtualRow.cells[col.key ?? j];
let val;
if (col.text != null) {
val = col.text;
} else if (typeof col.filter === 'function') {
val = col.filter(item, selected, this._var.refs.body, startIndex + i);
} else {
val = item[col.key];
if (val != null) {
if (Object.prototype.hasOwnProperty.call(val, 'DisplayValue')) {
val = val.DisplayValue;
} else if (Array.isArray(val)) {
val = val.join(', ');
}
}
}
val ??= '';
// fill
let bg = col.background;
if (bg != null) {
if (typeof bg === 'function') {
bg = col.background(item);
}
} else if (typeof col.bgFilter === 'function') {
bg = col.bgFilter(item);
}
bg ??= '';
if (bg !== virtualCell.background) {
virtualCell.background = bg;
cell.style.backgroundColor = bg;
}
const alwaysEditing = GridColumnTypeEnum.isAlwaysEditing(col.type);
const type = this._var.colTypes[col.key] ?? GridColumn;
let element;
if (!readonly && !alwaysEditing && typeof type.createEdit === 'function') {
const oldValue = vals.__editing?.[col.key];
if (oldValue !== undefined) {
delete vals.__editing[col.key];
if (typeof type.leaveEdit === 'function') {
type.leaveEdit(cell.children[0], this._var.el);
}
if (type.editing) {
val = type.getValue({ target: cell.children[0] }, col);
this._onRowChanged(null, i, col, val, cell, oldValue);
if (Object.prototype.hasOwnProperty.call(val, 'value')) {
val = val.value;
}
}
}
if (stateChanged) {
element = selected ?
type.createEdit(e => {
let old;
if (type.editing) {
old = vals.__editing?.[col.key];
if (old === undefined) {
return;
}
delete vals.__editing[col.key];
}
this._onRowChanged(e, i, col, type.getValue(e, col), cell, old);
}, col, this._var.el, vals) :
type.create(col, i, this);
if (typeof col.class === 'string') {
type.setClass(element, col.class);
}
if (col.contentWrap) {
element.classList.add('wrap');
}
cell.replaceChildren(element);
if (col.events != null) {
for (let ev of Object.entries(col.events)) {
element[ev[0]] = ev[1].bind(item);
}
}
} else {
element = cell.children[0];
}
} else {
element = cell.children[0];
}
if (stateChanged) {
if (typeof type.createEdit === 'function') {
delete virtualCell.attrs;
virtualCell.style = '';
delete virtualCell.value;
delete virtualCell.enabled;
}
}
if (val !== virtualCell.value) {
virtualCell.value = val;
type.setValue(element, val, vals, col, this);
}
if (typeof type.setEnabled === 'function') {
let enabled;
if (readonly) {
enabled = false;
} else {
enabled = col.enabled;
if (typeof enabled === 'function') {
this._var.enabledDict[col.key] = true;
enabled = enabled.call(col, item);
} else if (typeof enabled === 'string') {
this._var.enabledDict[col.key] = enabled;
enabled = item[enabled];
}
}
if (enabled !== virtualCell.enabled) {
virtualCell.enabled = enabled;
type.setEnabled(element, enabled, selected);
}
}
if (stateChanged && typeof type.setEditing === 'function') {
type.setEditing(element, selected);
}
let tip = col.tooltip;
if (typeof tip === 'function') {
tip = tip.call(col, item);
}
if (tip !== virtualCell.tooltip) {
virtualCell.tooltip = tip;
if (nullOrEmpty(tip)) {
element.querySelector('.ui-tooltip-wrapper')?.remove();
} else {
setTooltip(element, tip, false, this.element);
}
}
// auto resize
if (this._var.needResize && widths != null && this._get(col.key, 'autoResize')) {
const width = element.scrollWidth + 12;
if (width > 0 && (isNaN(widths[j]) || widths[j] < width)) {
widths[j] = width;
widths.flag = true;
}
}
let style = col.style;
if (style != null) {
if (typeof style === 'function') {
style = col.style(item);
}
} else if (typeof col.styleFilter === 'function') {
style = col.styleFilter(item);
}
const separateElement = typeof type.getElement === 'function';
let maxHeight;
if (col.maxLines > 0) {
maxHeight = `${col.maxLines * this.lineHeight}px`;
if (!separateElement) {
if (style == null) {
style = { 'max-height': maxHeight };
} else {
style['max-height'] = maxHeight;
}
}
}
const styleText = style != null ? convertCssStyle(style) : '';
if (styleText !== virtualCell.style) {
virtualCell.style = styleText;
if (style != null) {
type.setStyle(element, style);
} else {
element.style.cssText = '';
}
}
if (separateElement && maxHeight != null) {
const e = type.getElement(element);
if (e != null) {
e.style['max-height'] = maxHeight;
}
}
if (col.attrs != null) {
let attrs = col.attrs;
if (typeof attrs === 'function') {
attrs = attrs(item);
}
const attrsText = convertCssStyle(attrs);
if (attrsText !== virtualCell.attrs) {
virtualCell.attrs = attrsText;
for (let attr of Object.entries(attrs)) {
element.setAttribute(attr[0], attr[1]);
}
}
}
});
if (vals.__editing != null) {
delete vals.__editing;
}
});
// total
const tfoot = this._var.refs.footer.parentElement;
if (this.total != null) {
if (tfoot.style.display === 'none') {
tfoot.style.display = '';
}
const cells = this._var.refs.footer.children;
this.columns.forEach((col, j) => {
if (col.visible === false) {
return;
}
const cell = cells[j + offset];
if (cell == null) {
return;
}
let val = this.total[col.key];
if (val != null && Object.prototype.hasOwnProperty.call(val, 'DisplayValue')) {
val = val.DisplayValue;
}
val ??= '';
const element = cell.children[0];
GridColumn.setValue(element, val);
// auto resize
if (this._var.needResize && this._get(col.key, 'autoResize')) {
const width = element.scrollWidth + 12;
if (width > 0 && widths != null && (isNaN(widths[j]) || widths[j] < width)) {
widths[j] = width;
widths.flag = true;
}
}
});
} else if (tfoot.style.display === '') {
tfoot.style.display = 'none';
}
}
/**
* @private
* @param {number} index
* @param {number} width
* @param {boolean} [freeze]
*/
_changeColumnWidth(index, width, freeze) {
const col = this.columns[index];
// const oldwidth = col.width;
const w = `${width}px`;
col.width = width;
const style = this._get(col.key, 'style');
style.width = w;
style['max-width'] = w;
style['min-width'] = w;
const headerCells = this._headerCells;
let element = headerCells[index];
element.style.width = w;
element.style.maxWidth = w;
element.style.minWidth = w;
// element.style.cssText += `width: ${w}; max-width: ${w}; min-width: ${w}`;
let left = this.expandable ? ExpandableWidth : 0;
if (col.isfixed) {
left = element.offsetLeft + width;
let l = left;
for (let i = index + 1; i < this.columns.length; ++i) {
if (this.columns[i].isfixed) {
headerCells[i].style.left = `${l}px`;
l += this.columns[i].width;
} else {
break;
}
}
}
const offset = this.expandable ? 1 : 0;
for (let row of this._tableRows) {
element = row.children[index + offset];
if (element != null) {
element.style.width = w;
element.style.maxWidth = w;
element.style.minWidth = w;
// element.style.cssText += `width: ${w}; max-width: ${w}; min-width: ${w}`;
if (col.isfixed) {
let l = left;
for (let i = index + offset + 1; i < this.columns.length; ++i) {
if (this.columns[i].isfixed) {
row.children[i].style.left = `${l}px`;
l += this.columns[i].width;
} else {
break;
}
}
}
}
}
// footer
if (this.total != null) {
const footerCells = this._footerCells;
element = footerCells[index];
element.style.width = w;
element.style.maxWidth = w;
element.style.minWidth = w;
if (col.isfixed) {
let l = left;
for (let i = index + 1; i < this.columns.length; ++i) {
if (this.columns[i].isfixed) {
footerCells[i].style.left = `${l}px`;
l += this.columns[i].width;
} else {
break;
}
}
}
}
if (!freeze) {
this._layoutHeaderFooter();
}
}
/**
* @private
*/
_layoutHeaderFooter() {
const children = this._var.refs.table.children;
this._var.headerHeight = this.headerVisible === false ? 0 : (this.headerWrap ? children[0].offsetHeight : this.rowHeight);
if (this.total != null) {
this._var.footerHeight = children[2].offsetHeight;
const footerOffset = this._var.refs.table.offsetHeight - this._var.el.clientHeight;
if (this._var.footerOffset !== footerOffset) {
this._var.footerOffset = footerOffset;
this._var.refs.footer.parentElement.style.bottom = `${this._var.refs.table.offsetTop + footerOffset - this._var.el.scrollTop}px`;
}
}
}
/**
* @private
* @param {number} index
* @param {number} offset
* @param {number} mouse
* @param {number} draggerCellLeft
*/
_changingColumnOrder(index, offset, mouse, draggerCellLeft) {
const children = this._headerCells;
let element = children[index];
this._var.refs.dragger.style.cssText = `left: ${element.offsetLeft - draggerCellLeft + offset}px; width: ${element.style.width}; display: block`;
// offset = x + gridScrollLeft - element.offsetLeft; // getOffsetLeftFromWindow(element);
offset += mouse;
let idx;
const toLeft = offset < 0;
if (toLeft) {
offset = -offset;
for (let i = index - 1; i >= 0 && offset >= 0; i -= 1) {
element = children[i];
if (element == null || !element.className || element.classList.contains('sticky')) {
idx = i + 1;
break;
}
if (offset < element.offsetWidth) {
idx = (offset > element.offsetWidth / 2) ? i : i + 1;
break;
}
offset -= element.offsetWidth;
}
idx ??= 0;
} else {
const count = children.length;
for (let i = index; i < count - 1 && offset >= 0; ++i) {
element = children[i];
if (element == null || !element.className || element.classList.contains('sticky')) {
idx = i;
break;
}
if (offset < element.offsetWidth) {
idx = (offset > element.offsetWidth / 2) ? i + 1 : i;
break;
}
offset -= element.offsetWidth;
}
idx ??= count - 1;
}
if (idx !== this._var.colAttrs.__orderIndex || this._var.refs.draggerCursor.style.display !== 'block') {
element = children[idx];
if (element == null) {
return;
}
this._var.colAttrs.__orderIndex = idx;
// avoid `offsetLeft` of hidden header to be 0
let left;
if (element.style.display === 'none') {
left = 0;
while (left === 0 && (element = children[++idx]) != null) {
left = element.offsetLeft;
}
if (!toLeft && left === 0) {
left = draggerCellLeft;
}
} else {
left = element.offsetLeft;
}
// set position of dragger cursor
this._var.refs.draggerCursor.style.cssText = `left: ${left - draggerCellLeft}px; display: block`;
}
}
/**
* @private
* @param {number} index
*/
_changeColumnOrder(index) {
this._var.refs.dragger.style.display = '';
this._var.refs.draggerCursor.style.display = '';
const orderIndex = this._var.colAttrs.__orderIndex;
if (orderIndex >= 0 && orderIndex !== index) {
let targetIndex = orderIndex - index;
if (targetIndex >= 0 && targetIndex <= 1) {
return;
}
const header = this._var.refs.header;
const children = this._headerCells;
const rows = this._tableRows;
const columns = this.columns;
const offset = this.expandable ? 1 : 0;
if (targetIndex > 1) {
targetIndex = orderIndex - 1;
// const current = columns[index];
// for (let i = index; i < targetIndex; ++i) {
// columns[i] = columns[i + 1];
// }
// columns[targetIndex] = current;
const current = columns.splice(index, 1)[0];
columns.splice(targetIndex, 0, current);
header.insertBefore(children[index], children[targetIndex].nextElementSibling);
for (let row of rows) {
row.insertBefore(row.children[index + offset], row.children[targetIndex + offset].nextElementSibling);
}
} else {
targetIndex = orderIndex;
// const current = columns[index];
// for (let i = index; i > targetIndex; i -= 1) {
// columns[i] = columns[i - 1];
// }
// columns[targetIndex] = current;
const current = columns.splice(index, 1)[0];
columns.splice(targetIndex, 0, current);
header.insertBefore(children[index], children[targetIndex]);
for (let row of rows) {
row.insertBefore(row.children[index + offset], row.children[targetIndex + offset]);
}
}
if (this.sortArray == null || this.sortArray.length === 0) {
// refresh sortIndex
[...children].forEach((th, i) => {
const arrow = th.querySelector('.arrow');
if (arrow == null) {
return;
}
if (arrow.className !== 'arrow') {
this.sortIndex = i;
}
});
}
if (typeof this.onColumnChanged === 'function') {
this.onColumnChanged(ColumnChangedType.Reorder, index, targetIndex);
}
}
}
/**
* @private
* @param {number} top
* @param {boolean} [reload]
* @returns {number}
*/
_topToIndex(top, reload) {
const rowHeight = (this.rowHeight + 1);
top -= (top % (rowHeight * 2)) + (RedumCount * rowHeight);
if (top < 0) {
top = 0;
} else {
let bottomTop = this._var.containerHeight - (reload ? 0 : this._var.rowCount * rowHeight);
if (bottomTop < 0) {
bottomTop = 0;
}
if (top > bottomTop) {
top = bottomTop;
}
}
return { top, index: top / rowHeight };
}
/**
* @private
* @param {number} top
* @param {boolean} [reload]
* @returns {number}
*/
_scrollToTop(top, reload) {
const tti = this._topToIndex(top, reload);
top = tti.top;
if (this._var.scrollTop !== top) {
this._var.scrollTop = top;
if (this.virtual) {
this._var.startIndex = tti.index;
}
this._fillRows(this._tableRows, this.columns);
if (this.virtual) {
this._var.refs.table.style.top = `${top}px`;
}
} else if (reload) {
this._fillRows(this._tableRows, this.columns);
}
return top;
}
/**
* @private
* @param {string} key
* @param {("autoResize" | "style" | "resizing" | "dragging" | "filterSource" | "filterHeight" | "filterTop")} name
* @returns {any}
*/
_get(key, name) {
const attr = this._var.colAttrs[key];
if (attr == null) {
return null;
}
return attr[name];
}
/**
* @private
* @param {string} key
* @param {("autoResize" | "style" | "filterSource" | "filterHeight" | "filterTop")} name
* @param {any} value
*/
_set(key, name, value) {
const attr = this._var.colAttrs[key];
if (attr == null) {
this._var.colAttrs[key] = { [name]: value };
} else {
attr[name] = value;
}
}
/**
* @private
* @param {GridRowItem} item
* @param {boolean} editing
* @param {GridColumnDefinition} col
* @returns {any}
*/
_getItemProp(item, editing, col) {
let value;
if (typeof col?.filter === 'function') {
value = col.filter(item, editing, this._var.refs.body);
} else {
value = item[col.key];
}
if (value == null) {
return value;
}
const prop = editing ? 'Value' : 'DisplayValue';
if (Object.prototype.hasOwnProperty.call(value, prop)) {
return value[prop];
}
return value;
}
/**
* @private
* @param {GridRowItem} item
* @param {boolean} editing
* @param {GridColumnDefinition} col
* @returns {any}
*/
_getItemSortProp(item, editing, col) {
const value = item[col.key];
if (value != null && Object.prototype.hasOwnProperty.call(value, 'SortValue')) {
return value.SortValue;
}
return this._getItemProp(item, editing, col);
}
/**
* @private
* @param {HTMLElement} target
* @returns {HTMLElement[]}
*/
_getRowTarget(target) {
let parent;
while ((parent = target.parentElement) != null && !parent.classList.contains('ui-grid-row')) {
target = parent;
}
return [parent, target];
}
/**
* @private
* @param {HTMLElement} element
* @returns {HTMLElement}
*/
_getParentElement(element) {
while (element != null && element.className !== 'ui-grid') {
element = element.parentElement;
}
return element;
}
/**
* @private
* @param {HTMLElement} e
* @returns {boolean}
*/
_notHeader(e) {
if (e.parentElement.classList.contains('ui-switch')) {
return true;
}
return /^(input|label|layer|svg|use)$/i.test(e.tagName);
}
/**
* @private
* @param {MouseEvent} e
* @param {GridColumnDefinition} col
* @param {boolean} [force]
*/
_onHeaderClicked(e, col, force) {
if (!force && (this._get(col.key, 'resizing') || this._get(col.key, 'dragging'))) {
return;
}
if (!this._notHeader(e.target)) {
if (Array.isArray(this.sortArray) && this.sortArray.length > 0) {
showConfirm(this.langs.sort, this.langs.sortArrayExists, [
{
key: 'yes',
text: this.langs.yes
},
{
text: this.langs.cancel
}
]).then(r => {
if (r.result === 'yes') {
const sortCol = this.sortArray.find(c => c.column === col.key);
this.sortDirection = sortCol?.order === 'asc' ? -1 : 1;
this._onDoHeaderSort(col);
}
});
} else {
this._onDoHeaderSort(col);
}
}
}
/**
* @private
* @param {GridColumnDefinition} col
*/
_onDoHeaderSort(col) {
const index = this.columns.indexOf(col);
if (index < 0) {
return;
}
if (this.sortIndex === index) {
this.sortDirection = this.sortDirection === 1 ? -1 : 1;
} else {
this.sortIndex = index;
}
this.sortColumn();
if (typeof this.onColumnChanged === 'function') {
this.onColumnChanged(ColumnChangedType.Sort, index, this.sortDirection);
}
}
/**
* @private
* @param {MouseEvent} [e]
* @returns {boolean}
*/
_onCloseFilter(e) {
if (e != null) {
if ((e.target.tagName === 'LAYER' && e.target.classList.contains('filter')) ||
e.target.tagName === 'use') {
return false;
}
}
const panels = this._var.el.querySelectorAll('.filter-panel.active');
if (panels.length > 0) {
panels.forEach(el => el.classList.remove('active'));
setTimeout(() => this._var.el.querySelectorAll('.filter-panel').forEach(el => el.remove()), 120);
const filtering = this._var.colAttrs.__filtering;
if (filtering instanceof HTMLElement) {
filtering.classList.remove('hover');
}
delete this._var.colAttrs.__filtering;
document.removeEventListener('mousedown', this._onCloseFilter);
return true;
}
return false;
}
/**
* @private
* @param {MouseEvent} e
* @param {GridColumnDefinition} col
*/
_onFilter(e, col) {
if (this._onCloseFilter()) {
return;
}
document.addEventListener('mousedown', this._onCloseFilter.bind(this));
const panel = createElement('div', 'filter-panel');
panel.addEventListener('mousedown', e => e.stopPropagation());
const filter = e.currentTarget;
const th = filter.parentElement;
const width = th.offsetWidth;
panel.style.top = `${th.offsetHeight + this._var.el.scrollTop}px`;
const offsetLeft = th.offsetLeft;
const totalWidth = th.parentElement.offsetWidth;
const left = offsetLeft + FilterPanelWidth > totalWidth ?
totalWidth - FilterPanelWidth :
offsetLeft + (width > FilterPanelWidth ? width - FilterPanelWidth : 0);
panel.style.left = `${left}px`;
const maxHeight = this._var.el.offsetHeight - this._var.headerHeight;
if (maxHeight < 300) {
panel.style.height = `${maxHeight}px`;
} else {
panel.style.height = '';
}
// search
let searchbox;
if (col.allowSearch !== false) {
const searchholder = createElement('div', 'filter-search-holder');
searchbox = createElement('input', 'filter-search-box ui-text');
searchbox.type = 'text';
const searchicon = createIcon('fa-regular', 'search');
searchicon.addEventListener('mousedown', e => {
searchbox.focus();
e.preventDefault();
});
searchholder.append(searchbox, searchicon);
panel.append(searchholder);
}
// list
const itemlist = createElement('div', 'filter-item-list');
itemlist.addEventListener('scroll', e => throttle(this._onFilterScroll, RefreshInterval, this, col, itemlist, e.target.scrollTop), { passive: true });
// - all
const itemall = createElement('div', 'filter-item filter-all');
itemall.appendChild(createCheckbox({
label: this.langs.all,
onchange: e => {
const checked = e.target.checked;
itemlist.querySelectorAll('.filter-content input').forEach(box => box.checked = checked);
for (let it of this._get(col.key, 'filterSource')) {
it.__checked = checked;
}
}
}));
itemlist.appendChild(itemall);
// - items
let array;
if (Array.isArray(col.filterSource)) {
array = col.filterSource;
} else if (typeof col.filterSource === 'function') {
array = col.filterSource.call(this, col);
} else {
const dict = Object.create(null);
for (let item of this._var.source) {
let displayValue = this._getItemProp(item.values, false, col);
if (displayValue == null) {
displayValue = col.filterAllowNull ? this.langs.null : '';
}
if (Array.isArray(displayValue)) {
const vals = this._getItemProp(item.values, true, col);
displayValue.forEach((display, i) => {
if (!Object.hasOwnProperty.call(dict, display)) {
dict[display] = {
Value: vals[i],
DisplayValue: display,
RowItem: item.values
};
}
});
} else if (!Object.hasOwnProperty.call(dict, displayValue)) {
dict[displayValue] = {
Value: this._getItemProp(item.values, true, col),
DisplayValue: displayValue,
RowItem: item.values
};
}
}
array = Object.values(dict);
if (typeof col.sortFilter === 'function') {
array.sort((a, b) => col.sortFilter(a.RowItem, b.RowItem));
} else {
const type = this._var.colTypes[col.key];
const isDateColumn = type === GridDateColumn || type instanceof GridDateColumn;
const filterAsValue = col.filterAsValue;
array.sort((itemA, itemB) => {
let a = itemA.Value;
let b = itemB.Value;
if (a instanceof Date || b instanceof Date) {
if (a == null) {
a = 0;
} else if (b == null) {
b = 0;
}
} else if (a != null && b == null) {
return 1;
} else {
if (!filterAsValue && !isDateColumn) {
a = itemA.DisplayValue;
b = itemB.DisplayValue;
}
if (typeof a === 'string' && typeof b === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
}
return a > b ? 1 : (a < b ? -1 : 0);
});
}
}
array = array.map(i => {
if (Object.prototype.hasOwnProperty.call(i, 'Value') &&
Object.prototype.hasOwnProperty.call(i, 'DisplayValue')) {
return i;
}
return {
Value: i,
DisplayValue: i == null ? this.langs.null : i
};
});
this._fillFilterList(col, itemlist, array, itemall);
itemall.querySelector('input').checked = ![...itemlist.querySelectorAll('.filter-content input')].some(i => !i.checked);
panel.appendChild(itemlist);
if (searchbox != null) {
searchbox.addEventListener('input', e => {
const key = e.currentTarget.value.toLowerCase();
const items = key.length === 0 ? array : array.filter(i => {
let displayValue;
if (i != null && Object.prototype.hasOwnProperty.call(i, 'DisplayValue')) {
displayValue = i.DisplayValue;
} else {
displayValue = i;
}
if (displayValue == null) {
displayValue = this.langs.null;
}
return String(displayValue).toLowerCase().includes(key);
});
this._fillFilterList(col, itemlist, items, itemall);
});
}
// function
const functions = createElement('div', 'filter-function');
functions.append(
createElement('span', ok => {
ok.className = 'button';
ok.innerText = this.langs.ok;
ok.addEventListener('click', () => {
const array = this._get(col.key, 'filterSource').filter(i => i.__checked !== false);
if (typeof col.onFilterOk === 'function') {
col.onFilterOk.call(this, col, array);
} else {
if (GridColumnTypeEnum.isAlwaysEditing(col.type)) {
col.filterValues = array.map(a => a.Value);
} else {
const nullValue = col.filterAllowNull ? null : '';
col.filterValues = array.map(a => a.Value == null ? nullValue : a.DisplayValue);
}
}
this._var.colAttrs.__filtered = true;
this._refreshSource();
if (typeof col.onFiltered === 'function') {
col.onFiltered.call(this, col);
}
filter.replaceChildren(createIcon('fa-solid', this.filteredIcon));
filter.classList.add('active');
this._onCloseFilter();
});
}),
createElement('span', reset => {
reset.className = 'button';
reset.innerText = this.langs.reset;
reset.addEventListener('click', () => {
delete col.filterValues;
this._var.colAttrs.__filtered = this.columns.some(c => c.filterValues != null)
this._refreshSource();
if (typeof col.onFiltered === 'function') {
col.onFiltered.call(this, col);
}
filter.replaceChildren(createIcon('fa-solid', this.filterIcon));
filter.classList.remove('active');
this._onCloseFilter();
});
})
);
panel.appendChild(functions);
this._var.el.appendChild(panel);
requestAnimationFrame(() => panel.classList.add('active'));
this._var.colAttrs.__filtering = filter;
filter.classList.add('hover');
}
/**
* @private
* @param {GridColumnDefinition} col
* @param {HTMLDivElement} list
* @param {ValueItem[]} array
* @param {HTMLDivElement} all
*/
_fillFilterList(col, list, array, all) {
list.querySelector('.filter-holder')?.remove();
list.querySelector('.filter-content')?.remove();
const rowHeight = this.filterRowHeight;
const height = array.length * rowHeight;
this._set(col.key, 'filterHeight', height);
const holder = createElement('div', 'filter-holder');
holder.style.height = `${height}px`;
const content = createElement('div', 'filter-content');
content.style.top = `${rowHeight}px`;
this._set(col.key, 'filterSource', array);
const propKey = GridColumnTypeEnum.isAlwaysEditing(col.type) ? 'Value' : 'DisplayValue';
const nullValue = col.filterAllowNull ? null : '';
const allSelected = !Array.isArray(col.filterValues);
for (let item of array) {
let v = item.Value ?? nullValue;
if (v != null) {
v = Object.prototype.hasOwnProperty.call(item, propKey) ? item[propKey] : item;
}
item.__checked = allSelected || col.filterValues.some(it => Array.isArray(it) ? it.includes(v) : it === v);
}
if (array.length > 12) {
array = array.slice(0, 12);
}
this._doFillFilterList(col, content, array, all);
list.append(holder, content);
}
/**
* @private
* @param {GridColumnDefinition} col
* @param {HTMLDivElement} content
* @param {ValueItem[]} array
* @param {HTMLDivElement} all
*/
_doFillFilterList(col, content, array, all) {
for (let item of array) {
const div = createElement('div', 'filter-item');
const title = Object.prototype.hasOwnProperty.call(item, 'DisplayValue') ? item.DisplayValue : item;
let display;
if (typeof col.filterTemplate === 'function') {
display = col.filterTemplate(item);
}
if (display == null) {
display = title && String(title).replace(/( |\r\n|\n|<br[ \t]*\/?>)/g, '\u00a0');
}
div.appendChild(createCheckbox({
checked: item.__checked,
label: display,
title,
onchange: e => {
item.__checked = e.target.checked;
all.querySelector('input').checked = ![...content.querySelectorAll('input')].some(i => !i.checked);
}
}));
content.appendChild(div);
}
}
/**
* @private
* @param {GridColumnDefinition} col
* @param {HTMLDivElement} list
* @param {number} top
*/
_onFilterScroll(col, list, top) {
const rowHeight = this.filterRowHeight;
top -= (top % (rowHeight * 2)) + rowHeight;
if (top < 0) {
top = 0;
} else {
let bottomTop = this._get(col.key, 'filterHeight') - (12 * rowHeight);
if (bottomTop < 0) {
bottomTop = 0;
}
if (top > bottomTop) {
top = bottomTop;
}
}
if (this._get(col.key, 'filterTop') !== top) {
this._set(col.key, 'filterTop', top);
const startIndex = top / rowHeight;
let array = this._get(col.key, 'filterSource');
if (startIndex + 12 < array.length) {
array = array.slice(startIndex, startIndex + 12);
} else {
array = array.slice(-12);
}
const content = list.querySelector('.filter-content');
content.replaceChildren();
this._doFillFilterList(col, content, array, list.querySelector('.filter-all'));
content.style.top = `${top + rowHeight}px`;
}
}
/**
* @private
* @param {MouseEvent} e
* @param {GridColumnDefinition} col
*/
_onDragStart(e, col) {
if (this._notHeader(e.target)) {
return;
}
if (e.currentTarget.classList.contains('sticky')) {
return;
}
const index = indexOfParent(e.currentTarget) - (this.expandable ? 1 : 0);
const cx = getClientX(e);
const window = this.window ?? global;
const clearEvents = attr => {
for (let event of ['mousemove', 'mouseup']) {
if (Object.prototype.hasOwnProperty.call(attr, event)) {
window.removeEventListener(event, attr[event]);
delete attr[event];
}
}
};
let attr = this._var.colAttrs[col.key];
if (attr == null) {
attr = this._var.colAttrs[col.key] = {};
} else {
clearEvents(attr);
}
attr.dragging = true;
const draggerCellLeft = this._var.refs.header.querySelector('th:last-child').offsetLeft;
let p = this._var.el;
let gridLeftFromWindow = p.offsetLeft;
while ((p = p.offsetParent) != null) {
gridLeftFromWindow += p.offsetLeft + p.clientLeft;
}
const mouse = cx - e.currentTarget.offsetLeft + this._var.scrollLeft - gridLeftFromWindow;
const dragmove = e => {
const cx2 = getClientX(e);
const offset = cx2 - cx;
let pos = attr.offset;
let dragging;
if (pos == null) {
if (offset > MiniDragOffset || offset < -MiniDragOffset) {
dragging = true;
}
} else if (pos !== offset) {
dragging = true;
}
if (dragging) {
this._changingColumnOrder(index, offset, mouse, draggerCellLeft);
attr.offset = offset;
}
};
attr.mousemove = e => throttle(dragmove, RefreshInterval, this, e);
attr.mouseup = () => {
clearEvents(attr);
if (attr.offset == null) {
delete attr.dragging;
} else {
requestAnimationFrame(() => {
delete attr.dragging;
delete attr.offset;
});
this._changeColumnOrder(index);
}
};
['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event]));
}
/**
* @private
* @param {MouseEvent} e
* @param {GridColumnDefinition} col
*/
_onResizeStart(e, col) {
const cx = getClientX(e);
const width = col.width;
const index = indexOfParent(e.currentTarget.parentElement) - (this.expandable ? 1 : 0);
const window = this.window ?? global;
const clearEvents = attr => {
for (let event of ['mousemove', 'mouseup']) {
if (Object.prototype.hasOwnProperty.call(attr, event)) {
window.removeEventListener(event, attr[event]);
delete attr[event];
}
}
};
let attr = this._var.colAttrs[col.key];
if (attr == null) {
attr = this._var.colAttrs[col.key] = {};
} else {
clearEvents(attr);
}
attr.resizing = width;
const resizemove = e => {
const cx2 = getClientX(e);
const val = width + (cx2 - cx);
if (val < MiniColumnWidth) {
return;
}
attr.resizing = val;
attr.sizing = true;
this._changeColumnWidth(index, val);
};
attr.mousemove = e => throttle(resizemove, RefreshInterval, this, e);
attr.mouseup = e => {
clearEvents(attr);
const width = attr.resizing;
if (width != null) {
requestAnimationFrame(() => delete attr.resizing);
if (attr.sizing) {
delete attr.sizing;
delete attr.autoResize;
this._changeColumnWidth(index, width);
if (typeof this.onColumnChanged === 'function') {
this.onColumnChanged(ColumnChangedType.Resize, index, width);
}
}
}
e.stopPropagation();
e.preventDefault();
};
['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event]));
}
/**
* @private
* @param {MouseEvent} e
* @param {GridColumnDefinition} col
*/
_onAutoResize(e, col) {
const th = e.currentTarget.parentElement;
const index = indexOfParent(th);
const offset = this.expandable ? 1 : 0;
let width = th.querySelector('div:first-child').scrollWidth;
for (let row of this._tableRows) {
const element = row.children[index + offset].children[0];
const w = element.scrollWidth;
if (w > width) {
width = w;
}
}
if (width < MiniColumnWidth) {
width = MiniColumnWidth;
}
if (width > 0 && width !== col.width) {
width += 12;
this._changeColumnWidth(index - offset, width);
if (typeof this.onColumnChanged === 'function') {
this.onColumnChanged(ColumnChangedType.Resize, index - offset, width);
}
}
}
/**
* @private
* @param {GridColumnDefinition} col
* @param {boolean} flag
*/
_onColumnAllChecked(col, flag) {
if (this._var.currentSource == null) {
return;
}
const key = col.key;
const isFunction = typeof col.enabled === 'function';
const isString = typeof col.enabled === 'string';
if (typeof col.onAllChecked === 'function') {
col.onAllChecked.call(this, col, flag);
} else {
for (let row of this._var.currentSource) {
const item = row.values;
if (item == null) {
continue;
}
const enabled = isFunction ? col.enabled(item) : isString ? item[col.enabled] : col.enabled;
if (enabled !== false) {
const old = item[key];
item[key] = flag;
row.__changed = true;
if (typeof col.onChanged === 'function') {
col.onChanged.call(this, item, flag, old, void 0, row.__expandable_object);
}
}
}
this.refresh();
}
}
/**
* @private
* @param {Event} e
*/
_onScroll(e) {
if (this._var.colAttrs.__filtering != null) {
this._onCloseFilter();
}
this._var.scrollLeft = e.target.scrollLeft;
const top = e.target.scrollTop;
if (!this.virtual) {
if (this.total != null) {
this._var.refs.footer.parentElement.style.bottom = `${this._var.footerOffset - e.target.scrollTop}px`;
}
const tti = this._topToIndex(top);
if (this.onBodyScrolled === 'function') {
this.onBodyScrolled(e, tti.index, this._var.rowCount);
}
return;
}
this._scrollToTop(top);
if (this.total != null) {
this._var.refs.footer.parentElement.style.bottom = `${this._var.refs.table.offsetTop + this._var.footerOffset - e.target.scrollTop}px`;
}
if (this.onBodyScrolled === 'function') {
this.onBodyScrolled(e, this._var.startIndex, this._var.rowCount);
}
if (this._var.isFirefox) {
// 修复 firefox 下列头显示位置不正确的问题
debounce(this._fillRows, RefreshInterval, this, this._tableRows, this.columns);
}
}
/**
* 清除 tooltip 显示框,并延时后设置 `display: none`
* 不设置 display 的话会导致元素依然被算入滚动范围,从而影响滚动条显示
* @private
* @param {HTMLDivElement} holder
*/
_clearHolder(holder) {
if (this._var.tooltipTimer != null) {
clearTimeout(this._var.tooltipTimer);
}
this._var.tooltipTimer = setTimeout(() => {
holder.style.display = 'none';
this._var.tooltipTimer = null;
}, 120);
}
/**
* @private
* @param {MouseEvent} e
* @param {HTMLDivElement} holder
*/
_onGridMouseMove(e, holder) {
e.stopPropagation();
if (e.target.classList.contains('ui-grid-hover-holder')) {
return;
}
let [parent, target] = this._getRowTarget(e.target);
if (parent == null) {
delete holder.dataset.row;
delete holder.dataset.col;
if (holder.classList.contains('active')) {
holder.classList.remove('active');
this._clearHolder(holder);
}
return;
}
if (this._getParentElement(parent) !== this._var.el) {
// sub ui-grid
return;
}
const col = target.dataset.col;
const row = target.dataset.row;
if (holder.dataset.row === row &&
holder.dataset.col === col) {
return;
}
const type = this._var.colTypes[this.columns[col]?.key];
if (type?.canEdit && this._var.virtualRows[row]?.editing) {
delete holder.dataset.row;
delete holder.dataset.col;
if (holder.classList.contains('active')) {
holder.classList.remove('active');
this._clearHolder(holder);
}
return;
}
let element = target.children[0];
if (type != null && typeof type.getElement === 'function') {
element = type.getElement(element);
}
if (element?.tagName !== 'SPAN') {
if (holder.classList.contains('active')) {
delete holder.dataset.row;
delete holder.dataset.col;
holder.classList.remove('active');
this._clearHolder(holder);
}
return;
}
if (element.scrollWidth > element.offsetWidth ||
element.scrollHeight > element.offsetHeight) {
holder.dataset.row = row;
holder.dataset.col = col;
holder.innerText = element.innerText;
const top = (parent.classList.contains('ui-grid-total-row') ? this._var.refs.footer.parentElement.offsetTop + 1 : target.offsetTop) + this._var.refs.table.offsetTop;
let left = target.offsetLeft;
let width = holder.offsetWidth;
if (width > this._var.wrapClientWidth) {
width = this._var.wrapClientWidth;
}
const maxleft = this._var.wrapClientWidth + this._var.scrollLeft - width;
if (left > maxleft) {
left = maxleft;
}
const height = target.offsetHeight;
holder.style.cssText = `top: ${top}px; left: ${left}px; max-width: ${this._var.wrapClientWidth}px; min-height: ${height - 2}px`;
holder.classList.add('active');
} else if (holder.classList.contains('active')) {
delete holder.dataset.row;
delete holder.dataset.col;
holder.classList.remove('active');
this._clearHolder(holder);
}
}
/**
* @private
* @param {MouseEvent} e
* @param {number} index
* @param {number} colIndex
*/
_onRowClicked(e, index, colIndex) {
const startIndex = this._var.startIndex;
const selectedIndex = startIndex + index;
if (typeof this.willSelect === 'function' && !this.willSelect(selectedIndex, colIndex)) {
return;
}
// multi-select
let flag = false;
const selectedIndexes = this._var.selectedIndexes;
if (this.multiSelect) {
if (e.ctrlKey) {
const i = selectedIndexes.indexOf(selectedIndex);
if (i < 0) {
selectedIndexes.push(selectedIndex);
} else {
selectedIndexes.splice(i, 1);
}
flag = true;
} else if (e.shiftKey && selectedIndexes.length > 0) {
if (selectedIndexes.length > 1 || selectedIndexes[0] !== selectedIndex) {
let start = selectedIndexes[selectedIndexes.length - 1];
let end;
if (start > selectedIndex) {
end = start;
start = selectedIndex;
} else {
end = selectedIndex;
}
selectedIndexes.splice(0);
for (let i = start; i <= end; ++i) {
selectedIndexes.push(i);
}
flag = true;
}
}
}
if (!flag && (selectedIndexes.length !== 1 || selectedIndexes[0] !== selectedIndex)) {
selectedIndexes.splice(0, selectedIndexes.length, selectedIndex);
flag = true;
}
// apply style
if (flag) {
if (this.readonly) {
this._tableRows.forEach((row, i) => {
if (selectedIndexes.includes(startIndex + i)) {
row.classList.add('selected');
} else if (row.classList.contains('selected')) {
row.classList.remove('selected');
}
});
} else {
this.refresh();
}
if (typeof this.onSelectedRowChanged === 'function') {
this.onSelectedRowChanged(selectedIndex);
}
}
this._var.selectedColumnIndex = colIndex;
if ((this.fullrowClick || colIndex >= 0) && e.buttons === 1 && typeof this.cellClicked === 'function') {
if (this.cellClicked(selectedIndex, colIndex) === false) {
e.stopPropagation();
e.preventDefault();
}
}
}
/**
* @private
* @param {MouseEvent} e
*/
_onRowDblClicked(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'LAYER' && e.target.className === 'ui-check-inner' || e.target.tagName === 'LABEL' && (e.target.className === 'ui-drop-text' || e.target.className === 'ui-drop-caret')) {
return;
}
const index = this.selectedIndex;
if (typeof this.onRowDblClicked === 'function') {
this.onRowDblClicked(index);
}
if (typeof this.onCellDblClicked === 'function') {
const colIndex = this._var.selectedColumnIndex;
if (this.fullrowClick || colIndex >= 0) {
this.onCellDblClicked(index, colIndex);
}
}
}
/**
* @private
* @param {any} e
* @param {number} index
* @param {GridColumnDefinition} col
* @param {any} value
* @param {HTMLTableCellElement} cell
* @param {any} [oldValue]
*/
_onRowChanged(e, index, col, value, cell, oldValue) {
if (this._var.currentSource == null) {
return;
}
const vals = this._var.currentSource[this._var.startIndex + index];
// FIXME: 清除缓存会导致选中状态下动态数据源下拉列表显示为空
// delete vals.source;
const item = vals.values;
if (item == null) {
return;
}
let enabled = col.enabled;
if (typeof enabled === 'function') {
enabled = enabled.call(col, item);
} else if (typeof enabled === 'string') {
enabled = item[enabled];
}
if (enabled !== false) {
let v;
let t;
if (value != null) {
v = Object.prototype.hasOwnProperty.call(value, 'value') ? value.value : value;
t = Object.prototype.hasOwnProperty.call(value, 'text') ? value.text : value;
} else {
v = t = value;
}
const val = item[col.key];
if (val != null && Object.prototype.hasOwnProperty.call(val, 'Value')) {
oldValue ??= val.Value;
val.Value = v;
if (Object.prototype.hasOwnProperty.call(val, 'DisplayValue')) {
val.DisplayValue = t;
}
} else {
oldValue ??= val;
item[col.key] = v;
}
const virtualRow = this._var.virtualRows[index];
const virtualCell = virtualRow.cells[col.key];
if (virtualCell != null) {
virtualCell.value = v;
}
let tip = col.tooltip;
if (typeof tip === 'function') {
tip = tip.call(col, item);
}
if (nullOrEmpty(tip)) {
cell.querySelector('.ui-tooltip-wrapper')?.remove();
} else {
setTooltip(cell.children[0], tip, false, this.element);
}
// 调整其他列的可用性
const row = this._tableRows[index];
const offset = this.expandable ? 1 : 0;
this.columns.forEach((c, j) => {
const cache = this._var.enabledDict[c.key];
if (cache !== true && cache !== col.key) {
return;
}
const cell = row.children[j + offset];
if (cell == null) {
return;
}
const type = this._var.colTypes[c.key] ?? GridColumn;
if (typeof type.setEnabled === 'function') {
if (typeof c.enabled === 'function') {
enabled = c.enabled(item);
} else if (typeof c.enabled === 'string') {
enabled = item[c.enabled];
} else {
return;
}
const vCell = virtualRow.cells[c.key ?? j];
if (enabled !== vCell.enabled) {
vCell.enabled = enabled;
type.setEnabled(cell.children[0], enabled);
}
}
});
vals.__changed = true;
if (typeof col.onChanged === 'function') {
col.onChanged.call(this, item, v, oldValue, e, vals.__expandable_object);
}
}
}
/**
* @private
* @param {MouseEvent} _e
* @param {number} index
* @param {HTMLTableRowElement} _row
*/
_onExpandable(_e, index, _row) {
if (this._var.currentSource == null) {
return;
}
const vals = this._var.currentSource[this._var.startIndex + index];
vals.__expanded = !vals.__expanded
this.refresh();
}
}