import '../css/grid.scss'; import { global, isPositive, isMobile, throttle, 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 } from "../popup"; import { convertCssStyle } from "../extension"; import { GridColumn, GridInputColumn, GridTextColumn, GridDropdownColumn, GridCheckboxColumn, GridIconColumn, GridDateColumn } from "./column"; 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; function getClientX(e) { if (e == null) { return null; } const cx = e.touches && e.touches[0]?.clientX; return cx ?? e.clientX; } 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 }; let r = lang; /** * 行数据接口 * @typedef {object} GridItem * @property {any} Value - 值 * @property {string} DisplayValue - 显示值 */ /** * 行数据包装接口 * @typedef GridItemWrapper * @property {object} values - 真实数据对象 * @property {(GridItem | any)} values.{key} 数据属性 * @property {object} source - 下拉数据源缓存对象 * @property {GridSourceItem[]} source.{key} 数据源 */ /** * 下拉框数据源接口 * @typedef {object} GridSourceItem * @property {string} value - 值 * @property {string} text - 显示文本 */ /** * 行数据可用性回调函数 * @callback GridItemBooleanCallback * @param {GridItem} item - 行数据对象 * @returns {boolean} 返回是否可用 * @this GridColumnDefinition */ /** * 行数据过滤回调函数 * @callback GridItemFilterCallback * @param {GridItem} item - 行数据对象 * @param {boolean} editing - 是否处于编辑状态 * @param {HTMLElement} [body] - Grid 控件的 `` 部分 * @returns {any} - 返回过滤后的显示或编辑值 * @this GridColumnDefinition */ /** * 行数据处理回调函数 * @callback GridItemObjectCallback * @param {GridItem} item - 行数据对象 * @returns {object} 返回任意对象 * @this GridColumnDefinition */ /** * 行数据字符串回调函数 * @callback GridItemStringCallback * @param {GridItem} item - 行数据对象 * @returns {string} 返回字符串 * @this GridColumnDefinition */ /** * 列过滤器数据源回调函数 * @callback GridColumnFilterSourceCallback * @param {GridColumnDefinition} col - 列定义对象 * @returns {GridItem[]} 返回过滤器的数据数组 * @this Grid */ /** * 行数据排序回调函数 * @callback GridItemSortCallback * @param {GridItem} a - 对比行数据1 * @param {GridItem} b - 对比行数据2 * @returns {number} 返回大小对比结果 */ /** * 下拉列表数据源回调函数 * @callback GridDropdownSourceCallback * @param {GridItem} 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] - 父元素,默认添加到头元素之后 */ /** * 自定义日期格式化回调函数 * @callback DateFormatterCallback * @param {Date} date - 日期值 * @returns {any} 返回格式化后的结果 */ /** * 列定义接口 * @class * @property {string} key - 列关键字,默认以该关键字从行数据中提取单元格值,行数据的关键字属性值里包含 DisplayValue 则优先显示此值 * @property {(GridColumnTypeEnum | GridColumn)} [type=Grid.ColumnTypes.Common] - 列的类型,可以为 {@linkcode GridColumn} 的子类,或者如下内置类型 {@linkcode Grid.ColumnTypes} * @property {number} type.Common=0 - 通用列(只读) * @property {number} type.Input=1 - 单行文本列 * @property {number} type.Dropdown=2 - 下拉选择列 * @property {number} type.Checkbox=3 - 复选框列 * @property {number} type.Icon=4 - 图标列 * @property {number} type.Text=5 - 多行文本列 * @property {number} type.Date=6 - 日期选择列 * @property {string} [caption] - 列标题文本 * @property {object} [captionStyle] - 列标题的元素样式 * @property {number} [width] - 大于 0 则设置为该宽度,否则根据列内容自动调整列宽 * @property {("left" |"center" | "right")} [align=left] 列对齐方式 * @property {(boolean | string | GridItemBooleanCallback)} [enabled] - 列是否可用(可编辑),允许以下类型 * * * `boolean` 则直接使用该值 * * `string` 则以该值为关键字从行数据中取值作为判断条件 * * `GridItemBooleanCallback` 则调用如下回调,以返回值作为判断条件 * @property {GridColumnDefinition} enabled.{this} - 上下文为列定义对象 * @property {GridItem} enabled.item - 行数据对象 * @property {boolean} enabled.{returns} - 返回是否启用 * @property {GridItemFilterCallback} [filter] - 单元格取值采用该函数返回的值 * @property {GridColumnDefinition} filter.{this} - 上下文为列定义对象 * @property {GridItem} filter.item - 行数据对象 * @property {boolean} filter.editing - 是否处于编辑状态 * @property {HTMLElement} [filter.body] - Grid 控件的 `` 部分 * @property {any} filter.{returns} - 返回过滤后的显示或编辑值 * @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 {object} [css] - 单元格css样式对象(仅在重建行元素时读取) * @property {(object | GridItemObjectCallback)} [style] - 单元格样式(填充行列数据时读取),支持直接返回样式对象或调用函数返回(若赋值则忽略 [styleFilter)]{@linkcode GridColumnDefinition#styleFilter}) * @property {GridColumnDefinition} style.{this} - 上下文为列定义对象 * @property {GridItem} style.item - 行数据对象 * @property {object} style.{returns} - 返回样式对象 * @property {(@deprecated)} [styleFilter] - **已过时**
_根据返回值填充单元格样式(填充行列数据时读取)_ * @property {(string | GridItemStringCallback)} [background] - 设置单元格背景色(填充行列数据时读取),支持直接设置颜色字符串或调用函数返回(若赋值则忽略 [bgFilter]{@linkcode GridColumnDefinition#bgFilter}) * @property {GridColumnDefinition} background.{this} - 上下文为列定义对象 * @property {GridItem} background.item - 行数据对象 * @property {string} background.{returns} - 返回单元格背景色字符串 * @property {(@deprecated)} [bgFilter] - **已过时**
_根据返回值设置单元格背景色_ * @property {object} [events] - 给单元格元素附加事件(事件函数上下文为数据行对象) * @property {Function} events.{event} - 事件回调函数 * @property {(object | GridItemObjectCallback)} [attrs] - 根据返回值设置单元格元素的附加属性,允许直接设置对象也支持调用如下函数返回对象 * @property {GridColumnDefinition} attrs.{this} - 上下文为列定义对象 * @property {GridItem} attrs.item - 行数据对象 * @property {object} attrs.{returns} - 返回附加属性对象 * @property {boolean} [allowFilter=false] - 是否允许进行列头过滤 * @property {(GridItem[] | GridColumnFilterSourceCallback)} [filterSource] - 自定义列过滤器的数据源,允许调用如下函数 * @property {Grid} filterSource.{this} - 上下文为 Grid * @property {GridColumnDefinition} filterSource.col - 列定义对象 * @property {GridItem[]} filterSource.{returns} - 返回过滤器的数据数组 * @property {GridItemSortCallback} [sortFilter] - 自定义列排序函数 * @property {GridItem} sortFilter.a - 对比行数据1 * @property {GridItem} sortFilter.b - 对比行数据2 * @property {number} sortFilter.{returns} - 返回大小对比结果 * @property {DropdownOptions} [dropOptions] - 列为下拉列表类型时以该值设置下拉框的参数 * @property {(GridSourceItem[] | Promise | GridDropdownSourceCallback)} [source] - 列为下拉列表类型时以该值设置下拉列表数据源,支持返回异步对象,也支持如下函数返回 * @property {GridItem} source.item - 行数据对象 * @property {GridSourceItem[]} source.{returns} - 返回行下拉列表数据源 * @property {boolean} [sourceCache=false] - 下拉列表数据源是否缓存结果(即行数据未发生变化时仅从source属性获取一次值) * @property {("fa-light" | "fa-regular" | "fa-solid")} [iconType=fa-light] - 列为图标类型时以该值设置图标样式 * @property {string} [dateMin] - 列为日期类型时以该值作为最小可选日期值 * @property {string} [dateMax] - 列为日期类型时以该值作为最大可选日期值 * @property {DateFormatterCallback} [dateValueFormatter] - 列为日期类型时自定义日期格式化函数 * @property {Date} dateValueFormatter.date - 日期值 * @property {any} dateValueFormatter.{returns} - 返回格式化后的结果 * @property {(string | GridItemStringCallback)} [tooltip] - 额外设置单元格的 tooltip,支持直接使用字符串或者调用如下函数 * @property {GridColumnDefinition} tooltip.{this} - 上下文为列定义对象 * @property {GridItem} tooltip.item - 行数据对象 * @property {string} tooltip.{returns} - 返回额外 tooltip 字符串 */ class GridColumnDefinition { /** * 列头复选框改变时触发 * @type {Function} * @event * @param {GridColumnDefinition} col - 列定义对象 * @param {boolean} flag - 是否选中 * @this Grid */ onAllChecked; /** * 单元格发生变化时触发 * @event * @param {GridItem} item - 行数据对象 * @param {(boolean | string | number)} value - 修改后的值 * @param {(boolean | string | number)} oldValue - 修改前的值 * @param {any} [e] - 列修改事件传递过来的任意对象 * @this Grid */ onChanged; /** * 文本单元格在输入完成时触发的事件 * @event * @param {GridColumnDefinition} col - 列定义对象 * @param {string} value - 修改后的文本框值 * @this Grid */ onInputEnded; /** * 列过滤点击 `OK` 时触发的事件 * @event * @param {GridColumnDefinition} col - 列定义对象 * @param {GridItem[]} selected - 选中的过滤项 * @this Grid */ onFilterOk; /** * 列过滤后触发的事件 * @event * @param {GridColumnDefinition} col - 列定义对象 * @this Grid */ onFiltered; /** * 列为下拉框类型时在下拉列表展开时触发的事件 * @event * @param {GridItem} item - 行数据对象 * @param {Dropdown} drop - 拉框对象 * @this GridColumnDefinition */ onDropExpanded; } /** * 判断复选框列的回调函数 * @callback ColumnTypesEnumIsCheckbox * @param {number} 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, /** * 判断列是否为复选框列 * @type {ColumnTypesEnumIsCheckbox} */ isCheckbox(type) { return type === 3 } }; /** * 列排序枚举 * @enum {number} */ const GridColumnDirection = { /** -1 - 倒序 */ Descending: -1, /** 1 - 升序 */ Ascending: 1 }; /** * 列排序定义接口 * @typedef GridColumnSortDefinition * @property {string} column - 排序列的关键字 * @property {("asc" | "desc")} order - 升序或降序 */ /** * 扩展行对象接口 * @typedef GridExpandableObject * @property {HTMLElement} element - 扩展行元素 */ /** * 扩展行生成回调函数 * @callback GridExpandableObjectCallback * @param {GridItem} item - 行数据对象 * @returns {GridExpandableObject} 返回扩展行对象 * @this Grid */ /** * Grid 控件基础类 * * Grid Sample * @class * @example 基础示例 *
* @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 {number} * @private */ selectedColumnIndex: -1, /** * 当前选中的行索引数组 * @type {number[]} * @private */ selectedIndexes: null, /** * 虚模式头部索引 * @type {number} * @private */ startIndex: 0, /** * 旧选择索引数组 * @type {number[]} * @private */ oldSelectedIndexes: null, /** * 旧虚模式头部索引 * @type {number} * @private */ oldIndex: null, /** * 当前滚动上边距 * @private * @type {number} */ scrollTop: 0, /** * 当前滚动左边距 * @private * @type {number} */ scrollLeft: 0, /** * 一页高度可显示的行数 * @private * @type {number} */ rowCount: -1, /** * 列类型缓存字典 * @private * @property {GridColumn} {key} - 关键字对应列的类型缓存对象 */ colTypes: {}, /** * 列属性字典 * @private * @property {object} {key} - 关键字对应列的拖拽、调整大小暂存对象 * @property {boolean} {key}.dragging - 列正在拖拽 * @property {number} {key}.offset - 列拖拽偏移 * @property {Function} {key}.mousemove - 拖拽或调整大小移动回调函数 * @property {Function} {key}.mouseup - 拖拽或调整大小鼠标释放回调函数 * @property {number} {key}.resizing - 列临时大小 * @property {boolean} {key}.sizing - 列已进入修改大小的状态 * @property {boolean} {key}.autoResize - 列需要自动调整大小 * @property {object} {key}.style - 列样式对象 * @property {GridItem[]} {key}.filterSource - 列过滤面板数据源 * @property {number} {key}.filterHeight - 列过滤面板高度 * @property {number} {key}.filterTop - 列过滤面板滚动头部间距 */ 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 */ bodyClientWidth: null, /** * 是否需要 resize * @type {boolean} * @private */ needResize: 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, /** * 加载状态元素引用 - 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[]} */ columns = []; /** * 多语言资源对象 * @type {object} * @property {string} [all=( All )] * @property {string} [ok=OK] * @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} [asc=Ascending] * @property {string} [desc=Descending] * @property {string} [column=Column] * @property {string} [order=Order] * @property {string} [sort=Sort] * @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.] */ langs = {}; /** * 行数大于等于该值则启用虚模式 * @type {number} * @default 100 */ virtualCount = 100; /** * 表格行高 * @type {number} * @default 36 */ rowHeight = 36; /** * 文本行高 * @type {number} * @default 24 */ lineHeight = 24; /** * 列表底部留出额外的空白行 * @type {number} * @default 0 */ extraRows = 0; /** * 过滤条件列表的行高 * @type {number} * @default 30 */ filterRowHeight = 30; /** * 列表高度值,为 0 时列表始终显示全部内容(自增高),为非数字或者小于 0 则根据容器高度来确定虚模式的渲染行数 * @type {number | null} */ height; /** * 列表是否为只读 * @type {boolean} */ readonly; /** * 是否允许多选 * @type {boolean} * @default false */ multiSelect = false; /** * 为 `false` 时只有点击在单元格内才会选中行 * @type {boolean} * @default true */ fullrowClick = true; /** * 单元格 tooltip 是否禁用 * @type {boolean} * @default false */ tooltipDisabled = false; /** * 列头是否显示 * @type {boolean} * @default true */ headerVisible = true; /** * 监听事件的窗口载体 * @type {(Window | HTMLElement)} * @default window */ window = global; /** * 排序列的索引 * @type {number} * @default -1 */ sortIndex = -1; /** * 排序方式,正数升序,负数倒序 * @type {GridColumnDirection} * @default GridColumnDirection.Ascending */ sortDirection = GridColumnDirection.Ascending; /** * 排序列数组 * @type {GridColumnSortDefinition[]} * @default null * @property {string} column - 排序列的关键字 * @property {("asc" | "desc")} order - 升序或降序 */ sortArray = null; /** * 是否支持点击扩展 * @type {boolean} * @default false */ expandable; /** * 扩展行生成器 * @type {GridExpandableObjectCallback} * @property {Grid} {this} - 上下文为 Grid * @property {GridItem} item - 行数据对象 * @property {GridExpandableObject} {returns} - 返回扩展行对象 * @property {number} {returns}.index - 行索引 * @property {HTMLElement} {returns}.element - 扩展行元素 */ 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 - 滚动事件对象 * @this Grid */ onBodyScrolled; /** * 扩展行展开时触发的事件 * @event * @param {GridItem} item - 行数据对象 * @param {GridExpandableObject} expandableObject - 由 [expandableGenerator]{@linkcode Grid#expandableGenerator} 产生的行扩展对象 * @param {HTMLElement} expandableObject.element - 扩展行元素 * @this Grid */ onRowExpanded; /** * 扩展行收缩时触发的事件 * @event * @param {GridItem} item - 行数据对象 * @param {GridExpandableObject} expandableObject - 由 [expandableGenerator]{@linkcode Grid#expandableGenerator} 产生的行扩展对象 * @param {HTMLElement} expandableObject.element - 扩展行元素 * @this Grid */ onRowCollapsed; /** * 列类型枚举 * @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 - 日期选择列 */ static get ColumnTypes() { return GridColumnTypeEnum } /** * Grid 控件构造函数 * @param {(string | HTMLElement)?} container Grid 控件所在的父容器,可以是 string 表示选择器,也可以是 HTMLElement 对象
_**构造时可以不进行赋值,但是调用 init 函数时则必须进行赋值**_ * @param {Function} [getText] 获取多语言文本的函数代理 * @param {string} getText.id - 资源 ID * @param {string} [getText.def] - 默认资源 * @param {string} getText.{returns} 返回的多语言 */ constructor(container, getText) { this._var.parent = typeof container === 'string' ? document.querySelector(container) : container; if (typeof getText === 'function') { r = getText; } this.langs = { all: r('allItem', '( All )'), ok: r('ok', 'OK'), 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'), asc: r('asc', 'Ascending'), desc: r('desc', 'Descending'), column: r('column', 'Column'), order: r('order', 'Order'), sort: r('sort', 'Sort'), 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.') }; } /** * 获取 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 {GridItem[]} */ get allSource() { return this._var.source?.map(s => s.values) } /** * 获取已过滤的数据数组,或者设置数据并刷新列表 * @type {GridItem[]} * @property {any} Value - 值 * @property {string} DisplayValue - 显示值 */ get source() { return this._var.currentSource?.map(s => s.values) } set source(list) { if (this._var.el == null) { throw new Error('grid has not been initialized.') } 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._refreshSource(list); } /** * 获取已过滤的数据中的扩展对象数组 * @type {GridExpandableObject[]} * @property {HTMLElement} element - 扩展行元素 */ get sourceExpandable() { return this._var.currentSource?.map(s => s.__expandable_object) } /** * 设置单行数据 * @param {number} index - 行索引 * @param {GridItem} item - 待设置的行数据对象 */ setItem(index, item) { if (this._var.currentSource == null) { throw new Error('no source'); } const it = this._var.currentSource[index]; // clear dropdown source cache delete it.source; it.values = item; if (this.sortIndex >= 0) { this.sortColumn(); } else if (this.sortArray?.length > 0) { this.sort(); } else { this.refresh(); } } /** * 添加行数据 * @param {GridItem} item - 待添加的行数据值 * @param {number} [index] - 待添加的行索引 * @returns {GridItem} 返回已添加的行数据 */ 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.sortIndex >= 0) { this.sortColumn(true); } else if (this.sortArray?.length > 0) { this.sort(true); } else { this.reload(); } return item; } /** * 批量添加行数据 * @param {GridItem[]} array - 待添加的行数据数组 * @param {number} [index] - 待添加的行索引 * @returns {GridItem[]} 返回已添加的行数据数组 */ 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.sortIndex >= 0) { this.sortColumn(true); } else if (this.sortArray?.length > 0) { this.sort(true); } else { this.reload(); } return array; } /** * 删除行数据 * @param {number} index - 待删除的行索引 * @returns {GridItem} 返回已删除的行数据 */ 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(); return it.values; } /** * 批量删除行数据 * @param {number[]} [indexes] - 待删除的行索引数组,未传值时删除所有行 * @returns {GridItem[]} 返回已删除的行数据数组 */ 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 < 1) { this._var.selectedIndexes = [index - 1]; } else { this._var.selectedIndexes = []; } this.reload(); return array; } _refreshSource(list) { list ??= this._var.source; if (this._var.colAttrs.__filtered === true) { this._var.currentSource = list.filter(it => { for (let col of this.columns) { if (Array.isArray(col.filterValues)) { const v = this._getItemProp(it.values, false, col); 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.scrollLeft = 0; this._var.rowCount = -1; this.resize(true, null, () => { if (this.sortIndex >= 0) { this.sortColumn(true); } else if (this.sortArray?.length > 0) { this.sort(true); } else { this.reload(); } }); } /** * 获取当前是否为虚模式状态 * @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; } get _tableRows() { // return [...this._var.refs.body.children]; return Array.prototype.slice.call(this._var.refs.body.querySelectorAll('&>.ui-grid-row')); } get _headerCells() { return Array.prototype.slice.call(this._var.refs.header.querySelectorAll('&>th.column')); } /** * 获取虚模式起始索引 * @readonly * @type {number} */ get startIndex() { return this._var.startIndex } /** * 获取或设置当前选中的行索引的数组,设置后会刷新列表 * @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 !== true) { this.refresh(); } else { this._tableRows.forEach((row, i) => { if (indexes.includes(startIndex + i)) { row.classList.add('selected'); } else if (row.classList.contains('selected')) { row.classList.remove('selected'); } }); } if (typeof this.onSelectedRowChanged === 'function') { this.onSelectedRowChanged(); } } /** * 获取当前选中行的索引,为 -1 则当前没有选中行 * @readonly * @type {number} */ get selectedIndex() { return (this._var.selectedIndexes && this._var.selectedIndexes[0]) ?? -1 } /** * 获取或设置 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) { this._var.el = null; this._var.refs = {}; this._var.rendering = true; 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; } this._var.parent = container; 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 == null || selectedIndexes.length === 0) { return; } selectedIndexes.splice(0); if (this.readonly !== true) { this.refresh(); } else { this._tableRows.forEach(row => { row.classList.remove('selected'); }); } if (typeof this.onSelectedRowChanged === 'function') { this.onSelectedRowChanged(selectedIndex); } this._var.selectedColumnIndex = -1; return; } let [parent, target] = this._getRowTarget(e.target); if (parent == null) { return; } const rowIndex = indexOfParent(parent); let colIndex = indexOfParent(target) - (this.expandable ? 1 : 0); if (colIndex >= this.columns.length) { colIndex = -1; } this._onRowClicked(e, rowIndex, colIndex); }); 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); 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'); } return this._onRowClicked(e, row, col); }); holder.addEventListener('dblclick', e => this._onRowDblClicked(e)); wrapper.appendChild(holder); wrapper.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.sortIndex >= 0) { this.sortColumn(true); } else if (this.sortArray?.length > 0) { this.sort(true); } } } /** * 设置数据列表,该方法为 [source]{@linkcode Grid#source} 属性的语法糖 * @param {GridItem[]} 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] - 计算大小后的回调函数 * @param {Grid} callback.{this} - 上下文为 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; 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 callback === 'function') { callback.call(this); } else { this.reload(keep); } } this._var.bodyClientWidth = body.clientWidth; } /** * 重新计算需要渲染的行,并载入元素,一般需要在高度变化时调用 * @param {boolean} [keep] - 是否保持当前滚动位置 */ reload(keep) { 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.classList.add('active'); } else { ele.classList.remove('active'); } } this._refreshSource(); return; } 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.el.scrollTop = 0; // this._var.el.scrollLeft = 0; this._var.refs.table.style.top = '0px'; } const headerHeight = this._var.headerHeight || this.rowHeight; this._var.refs.wrapper.style.height = `${this._var.containerHeight + headerHeight}px`; this._adjustRows(this._var.refs.body); this.refresh(); } /** * 重新填充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); } }); } else { this._var.headerHeight = this._var.refs.table.children[0].offsetHeight; } } /** * 把所有行重置为未修改的状态 */ resetChange() { if (this._var.source == null) { return; } for (let row of this._var.source) { delete row.__changed; } } _getComparer(col, direction) { if (typeof col.sortFilter !== 'function') { if (isNaN(direction)) { direction = 1; } return (a, b) => { a = this._getItemProp(a.values, true, col); b = this._getItemProp(b.values, true, col); if (a == null && typeof b === 'number') { a = 0; } else if (typeof a === 'number' && b == null) { b = 0; } else if (a != null && b == null) { return direction; } else if (typeof a === 'string' && typeof b === 'string') { a = a.toLowerCase(); b = b.toLowerCase(); } return a === b ? 0 : (a > b ? 1 : -1) * direction; }; } return (a, b) => col.sortFilter(a.values, b.values) * direction; } /** * 根据当前排序字段进行列排序 * @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); } /** * 显示多列排序设置面板 */ showSortPanel() { const content = createElement('div', 'ui-sort-panel-content'); const buttonWrapper = createElement('div', 'ui-sort-panel-buttons'); const grid = new Grid(null, r); 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; const reload = index => { grid.selectedIndexes = [index]; grid.scrollTop = index * (grid.rowHeight + 1); rowChanged(index); } buttonWrapper.append( createElement('button', null, createIcon('fa-light', 'plus'), createElement('span', span => { span.innerText = this.langs.addLevel; span.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); }); }) ), createElement('button', 'ui-button-delete', createIcon('fa-light', 'times'), createElement('span', span => { span.innerText = this.langs.deleteLevel; span.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); }); }) ), createElement('button', 'ui-button-copy', createIcon('fa-light', 'copy'), createElement('span', span => { span.innerText = this.langs.copyLevel; span.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); }); }) ), createElement('button', button => { button.className = '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('button', button => { button.className = '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 && c.visible !== false); grid.columns = [ { key: 'column', caption: this.langs.column, width: 270, type: GridColumnTypeEnum.Dropdown, dropOptions: { textKey: 'caption', valueKey: 'key' }, source: columnSource, sortable: false, orderable: 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 } ]; 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; } 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; this.sortDirection = 1; this.sort(); } if (typeof this.onSorted === 'function') { this.onSorted(this.sortArray); } return true; } }, { text: this.langs.cancel } ], onResizeEnded: () => grid.resize() }); const source = this.sortArray || [{ column: '', order: 'asc' }]; pop.show(this._var.el).then(() => { pop.container.style.cssText += 'width: 520px; height: 400px'; grid.init(gridWrapper); grid.source = source.filter(s => s.column === '' || columnSource.find(c => c.key === s.column) != null); grid.selectedIndexes = [0]; grid.refresh(); rowChanged(0); }); } _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; 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 isCheckbox = GridColumnTypeEnum.isCheckbox(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 || typeof type.createCaption === 'function') { // col.autoResize = false; } else { this._set(col.key, 'autoResize', true); this._var.needResize = true; sizer.innerText = col.caption ?? ''; let width = sizer.offsetWidth + 22; if (!this.readonly && col.enabled !== false && col.allcheck && isCheckbox) { width += 32; } if (col.allowFilter === true) { width += 14; } if (width < MiniColumnWidth) { width = MiniColumnWidth; } col.width = width; } col.align ??= isCheckbox ? '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 (!this.readonly && col.enabled !== false && col.allcheck && isCheckbox) { const check = createCheckbox({ 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 (col.captionStyle != null) { caption.style.cssText = convertCssStyle(col.captionStyle); } wrapper.appendChild(caption); } // 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', 'filter')); 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); } // 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'))); sizer.replaceChildren(); this._var.refs.header = header; this._var.refs.dragger = dragger; this._var.refs.draggerCursor = draggerCursor; return thead; } _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; } _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 exists = content.querySelectorAll('&>.ui-grid-row').length; count -= exists; if (count > 0) { for (let i = 0; i < count; ++i) { const row = createElement('tr', 'ui-grid-row'); 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'); 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; } if (GridColumnTypeEnum.isCheckbox(col.type)) { cell.appendChild(GridCheckboxColumn.createEdit(e => this._onRowChanged(e, exists + i, col, e.target.checked, cell))); // this._var.colTypes[col.key] = GridCheckboxColumn; } else { 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; } const element = type.create(col, i, this); if (typeof col.class === 'string') { type.setClass(element, col.class); } cell.appendChild(element); } } 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 = content.querySelectorAll('&>.ui-grid-row')[exists + count]; while (last != null) { const next = last.nextElementSibling; last.remove(); last = next; } } } _fillRows(rows, cols, widths) { const startIndex = this._var.startIndex; const selectedIndexes = this._var.selectedIndexes; const stateChanged = this._var.oldIndex !== startIndex || selectedIndexes == null || this._var.oldSelectedIndexes?.length !== selectedIndexes.length || this._var.oldSelectedIndexes.find((s, i) => s !== selectedIndexes[i]) != null; if (stateChanged) { this._var.oldIndex = startIndex; if (selectedIndexes != null) { this._var.oldSelectedIndexes = selectedIndexes.slice(); } } const offset = this.expandable ? 1 : 0; [...rows].forEach((row, i) => { const vals = this._var.currentSource[startIndex + i]; if (vals == null) { return; } if (!isPositive(row.children.length)) { return; } 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'); } // 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; } let val; if (col.text != null) { val = col.text; } else if (typeof col.filter === 'function') { val = col.filter(item, selected, this._var.refs.body); } else { val = item[col.key]; if (val?.DisplayValue != null) { val = val.DisplayValue; } } val ??= ''; // fill let bg = col.background; if (bg != null) { if (typeof bg === 'function') { bg = col.background(item); } cell.style.backgroundColor = bg ?? ''; } else if (typeof col.bgFilter === 'function') { const bgColor = col.bgFilter(item); cell.style.backgroundColor = bgColor ?? ''; } const isCheckbox = GridColumnTypeEnum.isCheckbox(col.type); const type = isCheckbox ? GridCheckboxColumn : this._var.colTypes[col.key] ?? GridColumn; let element; if (!isCheckbox && typeof type.createEdit === 'function') { if (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, true); } } if (stateChanged) { element = selected ? type.createEdit(e => this._onRowChanged(e, i, col, type.getValue(e, col), cell), col, this._var.el, vals) : type.create(col, i, this); if (typeof col.class === 'string') { type.setClass(element, col.class); } cell.replaceChildren(element); } else { element = cell.children[0]; } } else { element = cell.children[0]; } let enabled; if (this.readonly) { enabled = false; } else { enabled = col.enabled; if (typeof enabled === 'function') { enabled = enabled.call(col, item); } else if (typeof enabled === 'string') { enabled = item[enabled]; } } type.setValue(element, val, vals, col, this); let tip = col.tooltip; if (typeof tip === 'function') { tip = tip.call(col, item); } if (nullOrEmpty(tip)) { element.querySelector('.ui-tooltip-wrapper')?.remove(); } else { setTooltip(element, tip, false, this.element); } if (typeof type.setEnabled === 'function') { type.setEnabled(element, enabled); } // 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; } } let style = col.style; if (style != null) { if (typeof style === 'function') { style = col.style(item); } if (style != null) { type.setStyle(element, style); } else { element.style.cssText = ''; } } else if (typeof col.styleFilter === 'function') { const style = col.styleFilter(item); if (style != null) { type.setStyle(element, style); } else { element.style.cssText = ''; } } if (col.events != null) { for (let ev of Object.entries(col.events)) { element[ev[0]] = ev[1].bind(item); } } if (col.attrs != null) { let attrs = col.attrs; if (typeof attrs === 'function') { attrs = attrs(item); } for (let attr of Object.entries(attrs)) { element.setAttribute(attr[0], attr[1]); } } }); if (vals.__editing != null) { delete vals.__editing; } }); } _changeColumnWidth(index, width) { 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; } } } } } this._var.headerHeight = this._var.refs.table.children[0].offsetHeight; } _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`; } } _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); } } } _scrollToTop(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; } } if (this._var.scrollTop !== top) { this._var.scrollTop = top; if (this.virtual) { this._var.startIndex = top / rowHeight; } this.refresh(); if (this.virtual) { this._var.refs.table.style.top = `${top}px`; } } else if (reload) { this.refresh(); } return top; } _get(key, name) { const attr = this._var.colAttrs[key]; if (attr == null) { return null; } return attr[name]; } _set(key, name, value) { const attr = this._var.colAttrs[key]; if (attr == null) { this._var.colAttrs[key] = { [name]: value }; } else { attr[name] = value; } } _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; } _getRowTarget(target) { let parent; while ((parent = target.parentElement) != null && !parent.classList.contains('ui-grid-row')) { target = parent; } return [parent, target]; } _getParentElement(element) { while (element != null && element.className !== 'ui-grid') { element = element.parentElement; } return element; } _notHeader(tagName) { return /^(input|label|layer|svg|use)$/i.test(tagName); } _onHeaderClicked(e, col, force) { if (!force && (this._get(col.key, 'resizing') || this._get(col.key, 'dragging'))) { return; } if (!this._notHeader(e.target.tagName)) { 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); } } } _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; } _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`; // 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 = this.langs.null; } if (!Object.hasOwnProperty.call(dict, displayValue)) { const val = this._getItemProp(item.values, true, col); dict[displayValue] = { Value: val, DisplayValue: displayValue }; } } array = Object.values(dict) .sort((a, b) => { if (a == null && b == null) { return 0; } if (a == null && b != null) { return -1; } if (a != null && b == null) { return 1; } if (Object.prototype.hasOwnProperty.call(a, 'Value')) { a = a.Value; } if (Object.prototype.hasOwnProperty.call(b, 'Value')) { b = b.Value; } 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('button', ok => { 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.isCheckbox(col.type)) { col.filterValues = array.map(a => a.Value); } else { col.filterValues = array.map(a => a.DisplayValue); } } this._var.colAttrs.__filtered = true; this._refreshSource(); if (typeof col.onFiltered === 'function') { col.onFiltered.call(this, col); } filter.classList.add('active'); this._onCloseFilter(); }); }), createElement('button', reset => { 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.classList.remove('active'); this._onCloseFilter(); }); }) ); panel.appendChild(functions); this._var.el.appendChild(panel); setTimeout(() => panel.classList.add('active'), 0); this._var.colAttrs.__filtering = filter; filter.classList.add('hover'); } _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.isCheckbox(col.type) ? 'Value' : 'DisplayValue'; for (let item of array) { const v = Object.prototype.hasOwnProperty.call(item, propKey) ? item[propKey] : item; item.__checked = !Array.isArray(col.filterValues) || col.filterValues.includes(v); } if (array.length > 12) { array = array.slice(0, 12); } this._doFillFilterList(content, array, all); list.append(holder, content); } _doFillFilterList(content, array, all) { for (let item of array) { const div = createElement('div', 'filter-item'); div.appendChild(createCheckbox({ checked: item.__checked, label: Object.prototype.hasOwnProperty.call(item, 'DisplayValue') ? item.DisplayValue : item, onchange: e => { item.__checked = e.target.checked; all.querySelector('input').checked = ![...content.querySelectorAll('input')].some(i => !i.checked); } })); content.appendChild(div); } } _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(content, array, list.querySelector('.filter-all')); content.style.top = `${top + rowHeight}px`; } } _onDragStart(e, col) { if (this._notHeader(e.target.tagName)) { return; } if (e.currentTarget.classList.contains('sticky')) { return; } const index = indexOfParent(e.currentTarget) - (this.expandable ? 1 : 0); const cx = getClientX(e); 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; // const gridScrollLeft = this._var.el.scrollLeft; 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.el.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 { setTimeout(() => { delete attr.dragging; delete attr.offset; }); this._changeColumnOrder(index); } }; ['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event])); } _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) { setTimeout(() => 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])); } _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); } } } _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) { item[key] = flag; row.__changed = true; if (typeof col.onChanged === 'function') { col.onChanged.call(this, item, flag); } } } this.refresh(); } } _onScroll(e) { if (this._var.colAttrs.__filtering != null) { this._onCloseFilter(); } if (this.onBodyScrolled === 'function') { this.onBodyScrolled(e); } if (!this.virtual) { return; } const top = e.target.scrollTop; this._scrollToTop(top); } _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'); } return; } if (this._getParentElement(parent) !== this._var.el) { // sub ui-grid return; } const element = target.children[0]; if (element?.tagName !== 'SPAN') { if (holder.classList.contains('active')) { delete holder.dataset.row; delete holder.dataset.col; holder.classList.remove('active'); } return; } const row = target.dataset.row; const col = target.dataset.col; if (holder.dataset.row === row && holder.dataset.col === col) { return; } if (element.scrollWidth > element.offsetWidth) { holder.dataset.row = row; holder.dataset.col = col; holder.innerText = element.innerText; const top = target.offsetTop + this._var.refs.table.offsetTop; let left = target.offsetLeft; let width = holder.offsetWidth; if (width > this._var.bodyClientWidth) { width = this._var.bodyClientWidth; } const maxleft = this._var.bodyClientWidth + 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.bodyClientWidth}px; 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'); } } _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 !== true) { this.refresh(); } else { this._tableRows.forEach((row, i) => { if (selectedIndexes.includes(startIndex + i)) { row.classList.add('selected'); } else if (row.classList.contains('selected')) { row.classList.remove('selected'); } }); } 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(); } } } _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); } } } _onRowChanged(e, index, col, value, cell, blur) { if (this._var.currentSource == null) { return; } const row = this._var.currentSource[this._var.startIndex + index]; delete row.source; const item = row.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) { const val = item[col.key]; let oldValue; if (val != null && Object.prototype.hasOwnProperty.call(val, 'Value')) { oldValue = val.Value; val.Value = value; } else { oldValue = val; item[col.key] = value; } 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); } row.__changed = true; if (blur) { if (typeof col.onInputEnded === 'function') { col.onInputEnded.call(this, item, value); } } else { if (typeof col.onChanged === 'function') { col.onChanged.call(this, item, value, oldValue, e); } } } } _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(); } }