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 <tsorgy@gmail.com>
 * @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 {{[key: string]: T}} KeyMap
 */

/**
 * 索引字典
 * @template T
 * @typedef {{[index: 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] - 列为下拉框类型时在下拉列表展开时触发的事件
 * @property {Function} [onDropCollapsed] - 列为下拉框类型时在下拉列表关闭时触发的事件
 * @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
 */
/**
 * 列为下拉框类型时在下拉列表关闭时触发的事件
 * @name onDropCollapsed
 * @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 包裹元素
         * @type {HTMLDivElement}
         * @private
         */
        container: 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-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 {string}
     * @default "en"
     * @ignore
     */
    lgid = 'en';
    /**
     * 行数大于等于该值则启用虚模式
     * @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 {string} [lgid=en] - 区域字符串
     * @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.reduce((array, item) => {
            array.push({
                __level: 0,
                values: item
            });
            if (Array.isArray(item.__children)) {
                array.push(...item.__children.map(c => ({
                    __level: 1,
                    values: c
                })));
            }
            return array;
        }, []);
        list = list.map((it, index) => {
            return {
                __index: index,
                ...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;
            this._var.el.style.overflow = '';
        } else {
            this._var.refs.loading.style.visibility = 'visible';
            this._var.refs.loading.style.opacity = 1;
            this._var.el.style.overflow = 'hidden';
        }
    }

    /**
     * 获取或设置 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 c = createElement('div', 'ui-grid-container');
        this._var.container = c;
        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', async e => {
            if (e.target === this._var.el) {
                if (e.offsetX < 0 || e.offsetX > e.target.clientWidth || e.offsetY < 0 || e.offsetY > e.target.clientHeight) {
                    // except scroll bars
                    return;
                }
                if (typeof this.willSelect === 'function') {
                    let result = this.willSelect(-1, -1);
                    if (result instanceof Promise) {
                        result = await result;
                    }
                    if (!result) {
                        return;
                    }
                }
                // 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);
                }
            });
        }
        c.appendChild(grid);
        container.replaceChildren(c);
        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 ui-grid-hover ui-tooltip-color',
                // createElement('div', 'ui-grid-hover-pointer ui-grid-hover ui-tooltip-color'),
                createElement('div', 'ui-grid-hover-curtain ui-grid-hover ui-tooltip-color'),
                createElement('div', 'ui-grid-hover-content ui-grid-hover')
            );
            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);
                }
                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-loading',
            createElement('div')
        );
        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
        const lgid = this.lgid;
        columnSource.sort((a, b) => String(a.caption).localeCompare(b.caption, lgid));
        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;
        this._var.colAttrs.__filtered = false;
        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);
            }
        }
        this._var.colAttrs.__filtered = false;
        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);
            }
        }
        this._var.colAttrs.__filtered = false;
        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;
            let path = ScriptPath;
            if (nullOrEmpty(path) && typeof consts !== 'undefined') {
                path = consts.modulePath;
            }
            if (typeof module === 'string') {
                url = `${path}${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 });
        });
    }

    /**
     * 展开/收折所有行
     * @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 lgid = this.lgid;
            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();
                        return a.localeCompare(b, lgid);
                    }
                } 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.localeCompare(b, lgid);
                    }
                }
                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) {
                let check;
                if (typeof type.createEdit === 'function') {
                    check = type.createEdit(e => this._onColumnAllChecked(col, e.target.checked), col);
                } else {
                    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.container);
            }
            // 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');
            }
            if (vals.__level !== 0) {
                row.classList.add('ui-grid-row-level');
                row.classList.add(`level-${vals.__level}`);
            } else {
                row.classList.remove('ui-grid-row-level');
            }
            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, !this.readonly && 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" | "filterSearched")} 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" | "filterSearched")} 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 /^(i|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 = '';
        }

        this._set(col.key, 'filterSearched', false);
        // 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;
                const lgid = this.lgid;
                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.localeCompare(b, lgid);
                        }
                    }
                    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 = array.find(i => !i.__checked) == null;
        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._set(col.key, 'filterSearched', items.length !== array.length);
                this._fillFilterList(col, itemlist, items, itemall);
                this._set(col.key, 'filterTop', -1);
                itemlist.dispatchEvent(new Event('scroll'));
            });
        }
        // function
        const functions = createElement('div', 'filter-function');
        functions.append(
            createElement('span', ok => {
                ok.className = 'button';
                ok.innerText = this.langs.ok;
                ok.addEventListener('click', () => {
                    const filterSource = this._get(col.key, 'filterSource');
                    const filterSearched = this._get(col.key, 'filterSearched');
                    if (!filterSearched && filterSource.find(i => i.__checked === false) == null) {
                        // all checked, equals to 'Reset'
                        delete col.filterValues;
                        this._var.colAttrs.__filtered = this.columns.some(c => c.filterValues != null);
                        filter.replaceChildren(createIcon('fa-solid', this.filterIcon));
                        filter.classList.remove('active');
                    } else {
                        const array = 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;
                        filter.replaceChildren(createIcon('fa-solid', this.filteredIcon));
                        filter.classList.add('active');
                    }
                    this._refreshSource();
                    if (typeof col.onFiltered === 'function') {
                        col.onFiltered.call(this, col);
                    }
                    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[]} source 
     * @param {HTMLDivElement} all 
     */
    _fillFilterList(col, list, source, all) {
        list.querySelector('.filter-holder')?.remove();
        list.querySelector('.filter-content')?.remove();
        const rowHeight = this.filterRowHeight;
        const height = source.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', source);
        const propKey = GridColumnTypeEnum.isAlwaysEditing(col.type) ? 'Value' : 'DisplayValue';
        const nullValue = col.filterAllowNull ? null : '';
        const allSelected = !Array.isArray(col.filterValues);
        for (let item of source) {
            if (item.__checked == null) {
                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);
            }
        }
        let array;
        if (source.length > 12) {
            array = source.slice(0, 12);
        } else {
            array = source;
        }
        this._doFillFilterList(col, content, array, source, all);
        list.append(holder, content);
    }

    /**
     * @private
     * @param {GridColumnDefinition} col 
     * @param {HTMLDivElement} content 
     * @param {ValueItem[]} array 
     * @param {ValueItem[]} source 
     * @param {HTMLDivElement} all 
     */
    _doFillFilterList(col, content, array, source, 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 = source.find(i => !i.__checked) == null;
                }
            }));
            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 source = this._get(col.key, 'filterSource');
            let array;
            if (startIndex + 12 < source.length) {
                array = source.slice(startIndex, startIndex + 12);
            } else {
                array = source.slice(-12);
            }
            const content = list.querySelector('.filter-content');
            content.replaceChildren();
            this._doFillFilterList(col, content, array, source, 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 (typeof 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 (typeof 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')) {
            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 key = this.columns[col]?.key ?? col;
        const type = this._var.colTypes[key];
        const virtualRow = this._var.virtualRows[row];
        if (type?.canEdit && virtualRow?.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.querySelector('.ui-grid-hover-content').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
            if (top > this.rowHeight * 2) {
                holder.classList.remove('ui-grid-hover-down');
            } else {
                holder.classList.add('ui-grid-hover-down');
            }
            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 
     */
    _afterRowChanged(e, index, colIndex) {
        const startIndex = this._var.startIndex;
        const selectedIndex = startIndex + index;
        // 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 
     * @param {number} index 
     * @param {number} colIndex 
     */
    async _onRowClicked(e, index, colIndex) {
        if (typeof this.willSelect === 'function') {
            const selectedIndex = this._var.startIndex + index;
            let result = this.willSelect(selectedIndex, colIndex);
            if (result instanceof Promise) {
                result = await result;
            }
            if (!result) {
                return;
            }
        }
        this._afterRowChanged(e, index, colIndex);
    }

    /**
     * @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();
    }
}