import '../css/grid.scss';
import { global, isPositive, isMobile, throttle, truncate } from "../../utility";
import { r as lang } from "../../utility/lgres";
import { nullOrEmpty } from "../../utility/strings";
import { createElement } from "../../functions";
import { createIcon } from "../icon";
import { createCheckbox } from "../checkbox";
import { setTooltip } from "../tooltip";
import { Popup, showAlert } from "../popup";
import { convertCssStyle } from "../extension";
import { GridColumn, GridInputColumn, GridTextColumn, GridDropdownColumn, GridCheckboxColumn, GridIconColumn, GridDateColumn } from "./column";
const ColumnChangedType = {
Reorder: 'reorder',
Resize: 'resize',
Sort: 'sort'
};
const RefreshInterval = isMobile() ? 32 : 10;
const HoverInternal = 200;
const RedumCount = 4;
const MiniDragOffset = 10;
const MiniColumnWidth = 50;
const FilterPanelWidth = 200;
const ExpandableWidth = 24;
function getClientX(e) {
if (e == null) {
return null;
}
const cx = e.touches && e.touches[0]?.clientX;
return cx ?? e.clientX;
}
function indexOfParent(target) {
// return [...target.parentElement.children].indexOf(target);
return Array.prototype.indexOf.call(target.parentElement.children, target);
}
const ColumnTypeDefs = {
0: GridColumn,
1: GridInputColumn,
2: GridDropdownColumn,
3: GridCheckboxColumn,
4: GridIconColumn,
5: GridTextColumn,
6: GridDateColumn
};
let r = lang;
/**
* 行数据接口
* @typedef {object} GridItem
* @property {any} Value - 值
* @property {string} DisplayValue - 显示值
*/
/**
* 行数据包装接口
* @typedef GridItemWrapper
* @property {object} values - 真实数据对象
* @property {(GridItem | any)} values.{key} 数据属性
* @property {object} source - 下拉数据源缓存对象
* @property {GridSourceItem[]} source.{key} 数据源
*/
/**
* 下拉框数据源接口
* @typedef {object} GridSourceItem
* @property {string} value - 值
* @property {string} text - 显示文本
*/
/**
* 行数据可用性回调函数
* @callback GridItemBooleanCallback
* @param {GridItem} item - 行数据对象
* @returns {boolean} 返回是否可用
* @this GridColumnDefinition
*/
/**
* 行数据过滤回调函数
* @callback GridItemFilterCallback
* @param {GridItem} item - 行数据对象
* @param {boolean} editing - 是否处于编辑状态
* @param {HTMLElement} [body] - Grid 控件的 `
` 部分
* @returns {any} - 返回过滤后的显示或编辑值
* @this GridColumnDefinition
*/
/**
* 行数据处理回调函数
* @callback GridItemObjectCallback
* @param {GridItem} item - 行数据对象
* @returns {object} 返回任意对象
* @this GridColumnDefinition
*/
/**
* 行数据字符串回调函数
* @callback GridItemStringCallback
* @param {GridItem} item - 行数据对象
* @returns {string} 返回字符串
* @this GridColumnDefinition
*/
/**
* 列过滤器数据源回调函数
* @callback GridColumnFilterSourceCallback
* @param {GridColumnDefinition} col - 列定义对象
* @returns {GridItem[]} 返回过滤器的数据数组
* @this Grid
*/
/**
* 行数据排序回调函数
* @callback GridItemSortCallback
* @param {GridItem} a - 对比行数据1
* @param {GridItem} b - 对比行数据2
* @returns {number} 返回大小对比结果
*/
/**
* 下拉列表数据源回调函数
* @callback GridDropdownSourceCallback
* @param {GridItem} item - 行数据对象
* @returns {GridSourceItem[]} 行下拉列表数据源
*/
/**
* 下拉列表参数对象
* @typedef DropdownOptions
* @property {string} [textKey=text] - 文本关键字
* @property {string} [valueKey=value] - 值关键字
* @property {string} [htmlKey=html] - 源码显示的关键字
* @property {number} [maxLength=500] - 最大输入长度
* @property {boolean} [multiSelect] - 是否允许多选
* @property {string} [selected] - 选中值
* @property {string[]} [selectedList] - 选中的数组
* @property {boolean} [disabled] - 是否禁用
* @property {boolean} [input] - 是否支持输入
* @property {boolean} [search] - 是否支持搜索
* @property {string[]} [searchKeys] - 搜索的关键字数组
* @property {string} [searchPlaceholder] - 搜索提示文本,默认值取语言资源 `searchHolder` "Search..."
* @property {number} [tabIndex] - 焦点索引
* @property {string} [placeholder] - 输入框的提示文本
* @property {boolean} [slideFixed] - 是否固定为向下展开
* @property {HTMLElement} [wrapper] - 父元素,默认添加到头元素之后
*/
/**
* 自定义日期格式化回调函数
* @callback DateFormatterCallback
* @param {Date} date - 日期值
* @returns {any} 返回格式化后的结果
*/
/**
* 列定义接口
* @class
* @property {string} key - 列关键字,默认以该关键字从行数据中提取单元格值,行数据的关键字属性值里包含 DisplayValue 则优先显示此值
* @property {(GridColumnTypeEnum | GridColumn)} [type=Grid.ColumnTypes.Common] - 列的类型,可以为 {@linkcode GridColumn} 的子类,或者如下内置类型 {@linkcode Grid.ColumnTypes}
* @property {number} type.Common=0 - 通用列(只读)
* @property {number} type.Input=1 - 单行文本列
* @property {number} type.Dropdown=2 - 下拉选择列
* @property {number} type.Checkbox=3 - 复选框列
* @property {number} type.Icon=4 - 图标列
* @property {number} type.Text=5 - 多行文本列
* @property {number} type.Date=6 - 日期选择列
* @property {string} [caption] - 列标题文本
* @property {object} [captionStyle] - 列标题的元素样式
* @property {number} [width] - 大于 0 则设置为该宽度,否则根据列内容自动调整列宽
* @property {("left" |"center" | "right")} [align=left] 列对齐方式
* @property {(boolean | string | GridItemBooleanCallback)} [enabled] - 列是否可用(可编辑),允许以下类型
*
* * `boolean` 则直接使用该值
* * `string` 则以该值为关键字从行数据中取值作为判断条件
* * `GridItemBooleanCallback` 则调用如下回调,以返回值作为判断条件
* @property {GridColumnDefinition} enabled.{this} - 上下文为列定义对象
* @property {GridItem} enabled.item - 行数据对象
* @property {boolean} enabled.{returns} - 返回是否启用
* @property {GridItemFilterCallback} [filter] - 单元格取值采用该函数返回的值
* @property {GridColumnDefinition} filter.{this} - 上下文为列定义对象
* @property {GridItem} filter.item - 行数据对象
* @property {boolean} filter.editing - 是否处于编辑状态
* @property {HTMLElement} [filter.body] - Grid 控件的 `` 部分
* @property {any} filter.{returns} - 返回过滤后的显示或编辑值
* @property {string} [text] - 单元格以该值填充内容,忽略filter与关键字属性
* @property {boolean} [visible=true] - 列是否可见
* @property {boolean} [resizable=true] - 列是否允许调整宽度
* @property {boolean} [sortable=true] - 列是否允许排序
* @property {boolean} [orderable=true] - 列是否允许重排顺序
* @property {boolean} [allcheck=false] - 列为复选框类型时是否在列头增加全选复选框
* @property {boolean} [shrink=false] - 列为收缩列,禁用自动调整大小
* @property {string} [class] - 单元格元素的额外样式类型字符串(仅在重建行元素时读取)
* @property {object} [css] - 单元格css样式对象(仅在重建行元素时读取)
* @property {(object | GridItemObjectCallback)} [style] - 单元格样式(填充行列数据时读取),支持直接返回样式对象或调用函数返回(若赋值则忽略 [styleFilter)]{@linkcode GridColumnDefinition#styleFilter})
* @property {GridColumnDefinition} style.{this} - 上下文为列定义对象
* @property {GridItem} style.item - 行数据对象
* @property {object} style.{returns} - 返回样式对象
* @property {(@deprecated)} [styleFilter] - **已过时**
_根据返回值填充单元格样式(填充行列数据时读取)_
* @property {(string | GridItemStringCallback)} [background] - 设置单元格背景色(填充行列数据时读取),支持直接设置颜色字符串或调用函数返回(若赋值则忽略 [bgFilter]{@linkcode GridColumnDefinition#bgFilter})
* @property {GridColumnDefinition} background.{this} - 上下文为列定义对象
* @property {GridItem} background.item - 行数据对象
* @property {string} background.{returns} - 返回单元格背景色字符串
* @property {(@deprecated)} [bgFilter] - **已过时**
_根据返回值设置单元格背景色_
* @property {object} [events] - 给单元格元素附加事件(事件函数上下文为数据行对象)
* @property {Function} events.{event} - 事件回调函数
* @property {(object | GridItemObjectCallback)} [attrs] - 根据返回值设置单元格元素的附加属性,允许直接设置对象也支持调用如下函数返回对象
* @property {GridColumnDefinition} attrs.{this} - 上下文为列定义对象
* @property {GridItem} attrs.item - 行数据对象
* @property {object} attrs.{returns} - 返回附加属性对象
* @property {boolean} [allowFilter=false] - 是否允许进行列头过滤
* @property {(GridItem[] | GridColumnFilterSourceCallback)} [filterSource] - 自定义列过滤器的数据源,允许调用如下函数
* @property {Grid} filterSource.{this} - 上下文为 Grid
* @property {GridColumnDefinition} filterSource.col - 列定义对象
* @property {GridItem[]} filterSource.{returns} - 返回过滤器的数据数组
* @property {GridItemSortCallback} [sortFilter] - 自定义列排序函数
* @property {GridItem} sortFilter.a - 对比行数据1
* @property {GridItem} sortFilter.b - 对比行数据2
* @property {number} sortFilter.{returns} - 返回大小对比结果
* @property {DropdownOptions} [dropOptions] - 列为下拉列表类型时以该值设置下拉框的参数
* @property {(GridSourceItem[] | Promise | GridDropdownSourceCallback)} [source] - 列为下拉列表类型时以该值设置下拉列表数据源,支持返回异步对象,也支持如下函数返回
* @property {GridItem} source.item - 行数据对象
* @property {GridSourceItem[]} source.{returns} - 返回行下拉列表数据源
* @property {boolean} [sourceCache=false] - 下拉列表数据源是否缓存结果(即行数据未发生变化时仅从source属性获取一次值)
* @property {("fa-light" | "fa-regular" | "fa-solid")} [iconType=fa-light] - 列为图标类型时以该值设置图标样式
* @property {string} [dateMin] - 列为日期类型时以该值作为最小可选日期值
* @property {string} [dateMax] - 列为日期类型时以该值作为最大可选日期值
* @property {DateFormatterCallback} [dateValueFormatter] - 列为日期类型时自定义日期格式化函数
* @property {Date} dateValueFormatter.date - 日期值
* @property {any} dateValueFormatter.{returns} - 返回格式化后的结果
* @property {(string | GridItemStringCallback)} [tooltip] - 额外设置单元格的 tooltip,支持直接使用字符串或者调用如下函数
* @property {GridColumnDefinition} tooltip.{this} - 上下文为列定义对象
* @property {GridItem} tooltip.item - 行数据对象
* @property {string} tooltip.{returns} - 返回额外 tooltip 字符串
*/
class GridColumnDefinition {
/**
* 列头复选框改变时触发
* @type {Function}
* @event
* @param {GridColumnDefinition} col - 列定义对象
* @param {boolean} flag - 是否选中
* @this Grid
*/
onAllChecked;
/**
* 单元格发生变化时触发
* @event
* @param {GridItem} item - 行数据对象
* @param {(boolean | string | number)} value - 修改后的值
* @param {(boolean | string | number)} oldValue - 修改前的值
* @param {any} [e] - 列修改事件传递过来的任意对象
* @this Grid
*/
onChanged;
/**
* 文本单元格在输入完成时触发的事件
* @event
* @param {GridColumnDefinition} col - 列定义对象
* @param {string} value - 修改后的文本框值
* @this Grid
*/
onInputEnded;
/**
* 列过滤点击 `OK` 时触发的事件
* @event
* @param {GridColumnDefinition} col - 列定义对象
* @param {GridItem[]} selected - 选中的过滤项
* @this Grid
*/
onFilterOk;
/**
* 列过滤后触发的事件
* @event
* @param {GridColumnDefinition} col - 列定义对象
* @this Grid
*/
onFiltered;
/**
* 列为下拉框类型时在下拉列表展开时触发的事件
* @event
* @param {GridItem} item - 行数据对象
* @param {Dropdown} drop - 拉框对象
* @this GridColumnDefinition
*/
onDropExpanded;
}
/**
* 判断复选框列的回调函数
* @callback ColumnTypesEnumIsCheckbox
* @param {number} type - 列类型
* @returns {boolean} 返回是否为复选框列
*/
/**
* 列类型枚举
* @enum {number}
*/
const GridColumnTypeEnum = {
/** 0 - 通用列(只读) */
Common: 0,
/** 1 - 单行文本列 */
Input: 1,
/** 2 - 下拉选择列 */
Dropdown: 2,
/** 3 - 复选框列 */
Checkbox: 3,
/** 4 - 图标列 */
Icon: 4,
/** 5 - 多行文本列 */
Text: 5,
/** 6 - 日期选择列 */
Date: 6,
/**
* 判断列是否为复选框列
* @type {ColumnTypesEnumIsCheckbox}
*/
isCheckbox(type) { return type === 3 }
};
/**
* 列排序枚举
* @enum {number}
*/
const GridColumnDirection = {
/** -1 - 倒序 */
Descending: -1,
/** 1 - 升序 */
Ascending: 1
};
/**
* 列排序定义接口
* @typedef GridColumnSortDefinition
* @property {string} column - 排序列的关键字
* @property {("asc" | "desc")} order - 升序或降序
*/
/**
* 扩展行对象接口
* @typedef GridExpandableObject
* @property {HTMLElement} element - 扩展行元素
*/
/**
* 扩展行生成回调函数
* @callback GridExpandableObjectCallback
* @param {GridItem} item - 行数据对象
* @returns {GridExpandableObject} 返回扩展行对象
* @this Grid
*/
/**
* Grid 控件基础类
*
*
* @class
* @example 基础示例
*
* @example
* #grid>.ui-grid {
* width: 600px;
* height: 400px;
* }
* @example
* const grid = new Grid('#grid', GetTextByKey);
* grid.columns = [
* {
* key: 'name',
* caption: 'Name',
* width: 140,
* allowFilter: true
* },
* {
* key: 'age',
* caption: 'Age',
* type: Grid.ColumnTypes.Input,
* width: 80
* },
* {
* key: 'study',
* caption: 'Study',
* type: Grid.ColumnTypes.Dropdown,
* width: 120,
* source: [
* { value: 'a', text: 'A' },
* { value: 'b', text: 'B' },
* { value: 'c', text: 'C' }
* ]
* }
* ];
* grid.multiSelect = true;
* grid.init();
* grid.source = [
* { name: '张三', age: '19', study: 'a' },
* { name: '李四', age: '24', study: 'a' },
* { name: '王五', age: '20', study: 'c' }
* ];
*/
export class Grid {
/**
* 内部引用变量
* @private
*/
_var = {
/**
* 父容器元素
* @type {HTMLElement}
* @private
*/
parent: null,
/**
* Grid 元素 - `div.ui-grid`
* @type {HTMLDivElement}
* @private
*/
el: null,
/**
* 全部数据数组
* @type {GridItemWrapper[]}
* @private
*/
source: null,
/**
* 当前已过滤显示的数据数组
* @type {GridItemWrapper[]}
* @private
*/
currentSource: null,
/**
* 当前选中的列索引
* @type {number}
* @private
*/
selectedColumnIndex: -1,
/**
* 当前选中的行索引数组
* @type {number[]}
* @private
*/
selectedIndexes: null,
/**
* 虚模式头部索引
* @type {number}
* @private
*/
startIndex: 0,
/**
* 旧选择索引数组
* @type {number[]}
* @private
*/
oldSelectedIndexes: null,
/**
* 旧虚模式头部索引
* @type {number}
* @private
*/
oldIndex: null,
/**
* 当前滚动上边距
* @private
* @type {number}
*/
scrollTop: 0,
/**
* 当前滚动左边距
* @private
* @type {number}
*/
scrollLeft: 0,
/**
* 一页高度可显示的行数
* @private
* @type {number}
*/
rowCount: -1,
/**
* 列类型缓存字典
* @private
* @property {GridColumn} {key} - 关键字对应列的类型缓存对象
*/
colTypes: {},
/**
* 列属性字典
* @private
* @property {object} {key} - 关键字对应列的拖拽、调整大小暂存对象
* @property {boolean} {key}.dragging - 列正在拖拽
* @property {number} {key}.offset - 列拖拽偏移
* @property {Function} {key}.mousemove - 拖拽或调整大小移动回调函数
* @property {Function} {key}.mouseup - 拖拽或调整大小鼠标释放回调函数
* @property {number} {key}.resizing - 列临时大小
* @property {boolean} {key}.sizing - 列已进入修改大小的状态
* @property {boolean} {key}.autoResize - 列需要自动调整大小
* @property {object} {key}.style - 列样式对象
* @property {GridItem[]} {key}.filterSource - 列过滤面板数据源
* @property {number} {key}.filterHeight - 列过滤面板高度
* @property {number} {key}.filterTop - 列过滤面板滚动头部间距
*/
colAttrs: {
/**
* 有已过滤的列
* @type {boolean}
* @private
*/
__filtered: false,
/**
* 过滤面板已打开
* @type {boolean}
* @private
*/
__filtering: false,
/**
* 上一个目标排序列索引
* @type {number}
* @private
*/
__orderIndex: -1,
},
/**
* 是否处于渲染中
* @type {boolean}
* @private
*/
rendering: false,
/**
* 头部高度
* @type {number}
* @private
*/
headerHeight: null,
/**
* 正文高度
* @type {number}
* @private
*/
containerHeight: null,
/**
* 正文宽度
* @type {number}
* @private
*/
bodyClientWidth: null,
/**
* 是否需要 resize
* @type {boolean}
* @private
*/
needResize: null,
/**
* 页面元素引用
* @private
*/
refs: {
/**
* 表格引用 - table.ui-grid-table
* @type {HTMLTableElement}
* @private
*/
table: null,
/**
* 表格正文引用 - tbody
* @type {HTMLTableSectionElement}
* @private
*/
body: null,
/**
* 表格头部引用 - thead>tr
* @type {HTMLTableSectionElement}
* @private
*/
header: null,
/**
* 加载状态元素引用 - div.ui-grid-loading
* @type {HTMLDivElement}
* @private
*/
loading: null,
/**
* 大小计算元素引用 - span.ui-grid-sizer
* @type {HTMLSpanElement}
* @private
*/
sizer: null,
/**
* 包装元素引用 - div.ui-grid-wrapper
* @type {HTMLDivElement}
* @private
*/
wrapper: null,
/**
* 拖拽块引用 - div.dragger
* @type {HTMLDivElement}
* @private
*/
dragger: null,
/**
* 拖拽光标引用 - layer.dragger-cursor
* @type {HTMLElement}
* @private
*/
draggerCursor: null,
}
};
/**
* 列定义的数组
* @type {GridColumnDefinition[]}
*/
columns = [];
/**
* 多语言资源对象
* @type {object}
* @property {string} [all=( All )]
* @property {string} [ok=OK]
* @property {string} [reset=Reset]
* @property {string} [cancel=Cancel]
* @property {string} [null=( Null )]
* @property {string} [addLevel=Add Level]
* @property {string} [deleteLevel=Delete Level]
* @property {string} [copyLevel=Copy Level]
* @property {string} [asc=Ascending]
* @property {string} [desc=Descending]
* @property {string} [column=Column]
* @property {string} [order=Order]
* @property {string} [sort=Sort]
* @property {string} [requirePrompt=All sort criteria must have a column specified. Check the selected sort criteria and try again.]
* @property {string} [duplicatePrompt={column} is being sorted more than once. Delete the duplicate sort criteria and try again.]
*/
langs = {};
/**
* 行数大于等于该值则启用虚模式
* @type {number}
* @default 100
*/
virtualCount = 100;
/**
* 表格行高
* @type {number}
* @default 36
*/
rowHeight = 36;
/**
* 文本行高
* @type {number}
* @default 24
*/
lineHeight = 24;
/**
* 列表底部留出额外的空白行
* @type {number}
* @default 0
*/
extraRows = 0;
/**
* 过滤条件列表的行高
* @type {number}
* @default 30
*/
filterRowHeight = 30;
/**
* 列表高度值,为 0 时列表始终显示全部内容(自增高),为非数字或者小于 0 则根据容器高度来确定虚模式的渲染行数
* @type {number | null}
*/
height;
/**
* 列表是否为只读
* @type {boolean}
*/
readonly;
/**
* 是否允许多选
* @type {boolean}
* @default false
*/
multiSelect = false;
/**
* 为 `false` 时只有点击在单元格内才会选中行
* @type {boolean}
* @default true
*/
fullrowClick = true;
/**
* 单元格 tooltip 是否禁用
* @type {boolean}
* @default false
*/
tooltipDisabled = false;
/**
* 列头是否显示
* @type {boolean}
* @default true
*/
headerVisible = true;
/**
* 监听事件的窗口载体
* @type {(Window | HTMLElement)}
* @default window
*/
window = global;
/**
* 排序列的索引
* @type {number}
* @default -1
*/
sortIndex = -1;
/**
* 排序方式,正数升序,负数倒序
* @type {GridColumnDirection}
* @default GridColumnDirection.Ascending
*/
sortDirection = GridColumnDirection.Ascending;
/**
* 排序列数组
* @type {GridColumnSortDefinition[]}
* @default null
* @property {string} column - 排序列的关键字
* @property {("asc" | "desc")} order - 升序或降序
*/
sortArray = null;
/**
* 是否支持点击扩展
* @type {boolean}
* @default false
*/
expandable;
/**
* 扩展行生成器
* @type {GridExpandableObjectCallback}
* @property {Grid} {this} - 上下文为 Grid
* @property {GridItem} item - 行数据对象
* @property {GridExpandableObject} {returns} - 返回扩展行对象
* @property {number} {returns}.index - 行索引
* @property {HTMLElement} {returns}.element - 扩展行元素
*/
expandableGenerator;
/**
* 即将选中行时触发
* @event
* @param {number} index - 即将选中的行索引
* @param {number} colIndex - 即将选中的列索引
* @returns {boolean} 返回 `false`、`null`、`undefined`、`0` 等则取消选中动作
* @this Grid
*/
willSelect;
/**
* 单元格单击时触发,colIndex 为 -1 则表示点击的是行的空白处
* @event
* @param {number} index - 点击的行索引
* @param {number} colIndex - 点击的列索引
* @returns {boolean} 返回 false 则取消事件冒泡
* @this Grid
*/
cellClicked;
/**
* 选中行发生变化时触发的事件
* @event
* @param {number} index - 选中的行索引
* @this Grid
*/
onSelectedRowChanged;
/**
* 单元格双击时触发的事件,colIndex 为 -1 则表示点击的是行的空白处
* @event
* @param {number} index - 双击的行索引
* @param {number} colIndex - 双击的列索引
* @this Grid
*/
onCellDblClicked;
/**
* 行双击时触发的事件
* @event
* @param {number} index - 双击的行索引
* @this Grid
*/
onRowDblClicked;
/**
* 列发生变化时触发的事件
* @event
* @param {("reorder" | "resize" | "sort")} type - 事件类型
*
* * "reorder" 为发生列重排事件,此时 value 为目标列索引
* * "resize" 为发生列宽调整事件,此时 value 为列宽度值
* * "sort" 为发生列排序事件,此时 value 为 1(升序)或 -1(倒序)
* @param {number} colIndex - 发生变化事件的列索引
* @param {number | GridColumnDirection} value - 变化的值
* @this Grid
*/
onColumnChanged;
/**
* 列滚动时触发的事件
* @event
* @param {Event} e - 滚动事件对象
* @this Grid
*/
onBodyScrolled;
/**
* 扩展行展开时触发的事件
* @event
* @param {GridItem} item - 行数据对象
* @param {GridExpandableObject} expandableObject - 由 [expandableGenerator]{@linkcode Grid#expandableGenerator} 产生的行扩展对象
* @param {HTMLElement} expandableObject.element - 扩展行元素
* @this Grid
*/
onRowExpanded;
/**
* 扩展行收缩时触发的事件
* @event
* @param {GridItem} item - 行数据对象
* @param {GridExpandableObject} expandableObject - 由 [expandableGenerator]{@linkcode Grid#expandableGenerator} 产生的行扩展对象
* @param {HTMLElement} expandableObject.element - 扩展行元素
* @this Grid
*/
onRowCollapsed;
/**
* 列类型枚举
* @readonly
* @type {GridColumnTypeEnum}
* @property {number} Common=0 - 通用列(只读)
* @property {number} Input=1 - 单行文本列
* @property {number} Dropdown=2 - 下拉选择列
* @property {number} Checkbox=3 - 复选框列
* @property {number} Icon=4 - 图标列
* @property {number} Text=5 - 多行文本列
* @property {number} Date=6 - 日期选择列
*/
static get ColumnTypes() { return GridColumnTypeEnum }
/**
* Grid 控件构造函数
* @param {(string | HTMLElement)?} container Grid 控件所在的父容器,可以是 string 表示选择器,也可以是 HTMLElement 对象
_**构造时可以不进行赋值,但是调用 init 函数时则必须进行赋值**_
* @param {Function} [getText] 获取多语言文本的函数代理
* @param {string} getText.id - 资源 ID
* @param {string} [getText.def] - 默认资源
* @param {string} getText.{returns} 返回的多语言
*/
constructor(container, getText) {
this._var.parent = typeof container === 'string' ? document.querySelector(container) : container;
if (typeof getText === 'function') {
r = getText;
}
this.langs = {
all: r('allItem', '( All )'),
ok: r('ok', 'OK'),
reset: r('reset', 'Reset'),
cancel: r('cancel', 'Cancel'),
null: r('null', '( Null )'),
addLevel: r('addLevel', 'Add level'),
deleteLevel: r('deleteLevel', 'Delete level'),
copyLevel: r('copyLevel', 'Copy level'),
asc: r('asc', 'Ascending'),
desc: r('desc', 'Descending'),
column: r('column', 'Column'),
order: r('order', 'Order'),
sort: r('sort', 'Sort'),
requirePrompt: r('requirePrompt', 'All sort criteria must have a column specified. Check the selected sort criteria and try again.'),
duplicatePrompt: r('duplicatePrompt', '{column} is being sorted more than once. Delete the duplicate sort criteria and try again.')
};
}
/**
* 获取 Grid 的页面元素
* @readonly
* @type {HTMLDivElement}
*/
get element() { return this._var.el }
/**
* 获取当前 Grid 是否已发生改变
* @readonly
* @type {boolean}
*/
get changed() {
const source = this._var.source;
if (source == null) {
return false;
}
return source.find(r => r.__changed) != null;
}
/**
* 返回所有数据的数据(未过滤)
* @readonly
* @type {GridItem[]}
*/
get allSource() { return this._var.source?.map(s => s.values) }
/**
* 获取已过滤的数据数组,或者设置数据并刷新列表
* @type {GridItem[]}
* @property {any} Value - 值
* @property {string} DisplayValue - 显示值
*/
get source() { return this._var.currentSource?.map(s => s.values) }
set source(list) {
if (this._var.el == null) {
throw new Error('grid has not been initialized.')
}
if (!Array.isArray(list)) {
throw new Error('source is not an Array.')
}
list = list.map((it, index) => {
return {
__index: index,
values: it
}
});
this._var.source = list;
this._refreshSource(list);
}
/**
* 获取已过滤的数据中的扩展对象数组
* @type {GridExpandableObject[]}
* @property {HTMLElement} element - 扩展行元素
*/
get sourceExpandable() { return this._var.currentSource?.map(s => s.__expandable_object) }
/**
* 设置单行数据
* @param {number} index - 行索引
* @param {GridItem} item - 待设置的行数据对象
*/
setItem(index, item) {
if (this._var.currentSource == null) {
throw new Error('no source');
}
const it = this._var.currentSource[index];
// clear dropdown source cache
delete it.source;
it.values = item;
if (this.sortIndex >= 0) {
this.sortColumn();
} else if (this.sortArray?.length > 0) {
this.sort();
} else {
this.refresh();
}
}
/**
* 添加行数据
* @param {GridItem} item - 待添加的行数据值
* @param {number} [index] - 待添加的行索引
* @returns {GridItem} 返回已添加的行数据
*/
addItem(item, index) {
if (this._var.currentSource == null) {
throw new Error('no source');
}
const it = index >= 0 ? this._var.currentSource[index] : null;
const newIt = { __index: null, values: item };
if (it != null) {
newIt.__index = it.__index;
this._var.currentSource.splice(index, 0, newIt);
if (this._var.colAttrs.__filtered === true) {
this._var.source.splice(it.__index, 0, newIt);
}
for (let i = it.__index + 1; i < this._var.source.length; ++i) {
this._var.source[i].__index += 1;
}
} else {
newIt.__index = this._var.source.length;
this._var.currentSource.push(newIt);
if (this._var.colAttrs.__filtered === true) {
this._var.source.push(newIt);
}
}
if (this.sortIndex >= 0) {
this.sortColumn(true);
} else if (this.sortArray?.length > 0) {
this.sort(true);
} else {
this.reload();
}
return item;
}
/**
* 批量添加行数据
* @param {GridItem[]} array - 待添加的行数据数组
* @param {number} [index] - 待添加的行索引
* @returns {GridItem[]} 返回已添加的行数据数组
*/
addItems(array, index) {
if (this._var.currentSource == null) {
throw new Error('no source');
}
if (!Array.isArray(array) || array.length <= 0) {
// throw new Error(`invalid items array: ${array}`);
return;
}
const it = index >= 0 ? this._var.currentSource[index] : null;
if (it != null) {
const items = array.map((a, i) => ({ __index: it.__index + i, values: a }));
this._var.currentSource.splice(index, 0, ...items);
if (this._var.colAttrs.__filtered === true) {
this._var.source.splice(it.__index, 0, ...items);
}
const offset = array.length;
for (let i = it.__index + offset; i < this._var.source.length; ++i) {
this._var.source[i].__index += offset;
}
} else {
const length = this._var.source.length;
const items = array.map((a, i) => ({ __index: length + i, values: a }));
this._var.currentSource.push(...items);
if (this._var.colAttrs.__filtered === true) {
this._var.source.push(...items);
}
}
if (this.sortIndex >= 0) {
this.sortColumn(true);
} else if (this.sortArray?.length > 0) {
this.sort(true);
} else {
this.reload();
}
return array;
}
/**
* 删除行数据
* @param {number} index - 待删除的行索引
* @returns {GridItem} 返回已删除的行数据
*/
removeItem(index) {
if (this._var.currentSource == null) {
throw new Error('no source');
}
const it = this._var.currentSource.splice(index, 1)[0];
if (it == null) {
return null;
}
if (this._var.colAttrs.__filtered === true) {
this._var.source.splice(it.__index, 1);
}
for (let i = it.__index; i < this._var.source.length; ++i) {
this._var.source[i].__index -= 1;
}
if (index < 1) {
this._var.selectedIndexes = [index - 1];
} else {
this._var.selectedIndexes = [];
}
this.reload();
return it.values;
}
/**
* 批量删除行数据
* @param {number[]} [indexes] - 待删除的行索引数组,未传值时删除所有行
* @returns {GridItem[]} 返回已删除的行数据数组
*/
removeItems(indexes) {
if (this._var.currentSource == null) {
throw new Error('no source');
}
if (Array.isArray(indexes) && indexes.length > 0) {
indexes = indexes.slice().sort();
} else {
indexes = this._var.currentSource.map(a => a.__index);
}
const array = [];
let first = 0;
for (let i = indexes.length - 1; i >= 0; --i) {
let it = this._var.currentSource.splice(indexes[i], 1)[0];
if (it == null) {
continue;
}
let next = this._var.source[it.__index];
if (next != null && next.__offset == null) {
next.__offset = i + 1;
}
if (this._var.colAttrs.__filtered === true) {
this._var.source.splice(it.__index, 1);
}
array.splice(0, 0, it.values);
first = it.__index;
}
let offset = 1;
for (let i = first; i < this._var.source.length; ++i) {
let it = this._var.source[i];
if (it.__offset > 0) {
offset = it.__offset;
delete it.__offset;
}
it.__index -= offset;
}
const index = indexes[0];
if (index < 1) {
this._var.selectedIndexes = [index - 1];
} else {
this._var.selectedIndexes = [];
}
this.reload();
return array;
}
_refreshSource(list) {
list ??= this._var.source;
if (this._var.colAttrs.__filtered === true) {
this._var.currentSource = list.filter(it => {
for (let col of this.columns) {
if (Array.isArray(col.filterValues)) {
const v = this._getItemProp(it.values, false, col);
if (col.filterValues.indexOf(v) < 0) {
return false;
}
}
}
return true;
});
} else {
this._var.currentSource = list;
}
this._var.selectedColumnIndex = -1;
this._var.selectedIndexes = [];
this._var.startIndex = 0;
this._var.scrollTop = 0;
this._var.scrollLeft = 0;
this._var.rowCount = -1;
this.resize(true, null, () => {
if (this.sortIndex >= 0) {
this.sortColumn(true);
} else if (this.sortArray?.length > 0) {
this.sort(true);
} else {
this.reload();
}
});
}
/**
* 获取当前是否为虚模式状态
* @readonly
* @type {boolean}
*/
get virtual() { return this._var.currentSource?.length > this.virtualCount }
/**
* 获取当前排序的列关键字,为 null 则当前无排序列
* @readonly
* @type {string | null}
*/
get sortKey() {
if (this.columns == null) {
return null;
}
return this.columns[this.sortIndex]?.key;
}
get _tableRows() {
// return [...this._var.refs.body.children];
return Array.prototype.slice.call(this._var.refs.body.querySelectorAll('&>.ui-grid-row'));
}
get _headerCells() {
return Array.prototype.slice.call(this._var.refs.header.querySelectorAll('&>th.column'));
}
/**
* 获取虚模式起始索引
* @readonly
* @type {number}
*/
get startIndex() { return this._var.startIndex }
/**
* 获取或设置当前选中的行索引的数组,设置后会刷新列表
* @type {number[]}
*/
get selectedIndexes() { return this._var.selectedIndexes }
set selectedIndexes(indexes) {
const startIndex = this._var.startIndex;
this._var.selectedIndexes.splice(0, this._var.selectedIndexes.length, ...indexes);
if (this.readonly !== true) {
this.refresh();
} else {
this._tableRows.forEach((row, i) => {
if (indexes.includes(startIndex + i)) {
row.classList.add('selected');
} else if (row.classList.contains('selected')) {
row.classList.remove('selected');
}
});
}
if (typeof this.onSelectedRowChanged === 'function') {
this.onSelectedRowChanged();
}
}
/**
* 获取当前选中行的索引,为 -1 则当前没有选中行
* @readonly
* @type {number}
*/
get selectedIndex() { return (this._var.selectedIndexes && this._var.selectedIndexes[0]) ?? -1 }
/**
* 获取或设置 Grid 当前的加载状态
* @type {boolean}
*/
get loading() { return this._var.refs.loading?.style?.visibility === 'visible' }
set loading(flag) {
if (this._var.refs.loading == null) {
return;
}
if (flag === false) {
this._var.refs.loading.style.visibility = 'hidden';
this._var.refs.loading.style.opacity = 0;
} else {
this._var.refs.loading.style.visibility = 'visible';
this._var.refs.loading.style.opacity = 1;
}
}
/**
* 获取或设置 Grid 当前滚动的偏移量
* @type {number}
*/
get scrollTop() { return this._var.el?.scrollTop; }
set scrollTop(top) {
if (this._var.el == null) {
return;
}
this._var.el.scrollTop = top;
this.reload(true);
}
/**
* 初始化Grid控件
* @param {HTMLElement} [container=.ctor#container] - 父容器元素,若未传值则采用构造方法中传入的父容器元素
*/
init(container = this._var.parent) {
this._var.el = null;
this._var.refs = {};
this._var.rendering = true;
if (container == null) {
throw new Error('no specified parent.');
}
if (!(container instanceof HTMLElement)) {
const ele = container[0];
if (!(ele instanceof HTMLElement)) {
throw new Error(`parent type not supported. ${JSON.stringify(Object.getPrototypeOf(container))}`);
}
container = ele;
}
this._var.parent = container;
const grid = createElement('div', 'ui-grid');
grid.setAttribute('tabindex', 0);
grid.addEventListener('keydown', e => {
let index = this.selectedIndex;
let flag = false;
if (e.key === 'ArrowUp') {
// up
if (index > 0) {
flag = true;
index -= 1;
}
} else if (e.key === 'ArrowDown') {
// down
const count = this._var.currentSource?.length ?? 0;
if (index < count - 1) {
flag = true;
index += 1;
}
}
if (flag) {
this._var.selectedIndexes = [index];
this.scrollToIndex(index);
this.refresh();
if (typeof this.onSelectedRowChanged === 'function') {
this.onSelectedRowChanged(index);
}
e.stopPropagation();
}
});
grid.addEventListener('mousedown', e => {
if (e.target === this._var.el) {
// cancel selections
const selectedIndexes = this._var.selectedIndexes;
if (selectedIndexes == null || selectedIndexes.length === 0) {
return;
}
selectedIndexes.splice(0);
if (this.readonly !== true) {
this.refresh();
} else {
this._tableRows.forEach(row => {
row.classList.remove('selected');
});
}
if (typeof this.onSelectedRowChanged === 'function') {
this.onSelectedRowChanged(selectedIndex);
}
this._var.selectedColumnIndex = -1;
return;
}
let [parent, target] = this._getRowTarget(e.target);
if (parent == null) {
return;
}
const rowIndex = indexOfParent(parent);
let colIndex = indexOfParent(target) - (this.expandable ? 1 : 0);
if (colIndex >= this.columns.length) {
colIndex = -1;
}
this._onRowClicked(e, rowIndex, colIndex);
});
container.replaceChildren(grid);
const sizer = createElement('span', 'ui-grid-sizer');
grid.appendChild(sizer);
this._var.refs.sizer = sizer;
grid.addEventListener('scroll', e => throttle(this._onScroll, RefreshInterval, this, e), { passive: true });
// header & body
const wrapper = createElement('div', 'ui-grid-wrapper');
this._var.refs.wrapper = wrapper;
grid.appendChild(wrapper);
const table = createElement('table', 'ui-grid-table');
this._var.refs.table = table;
this._createHeader(table);
this._createBody(table);
wrapper.appendChild(table);
// tooltip
if (!this.tooltipDisabled) {
const holder = createElement('div', 'ui-grid-hover-holder');
holder.addEventListener('mousedown', e => {
const holder = e.currentTarget;
const row = Number(holder.dataset.row);
const col = Number(holder.dataset.col);
if (holder.classList.contains('active')) {
holder.classList.remove('active');
}
return this._onRowClicked(e, row, col);
});
holder.addEventListener('dblclick', e => this._onRowDblClicked(e));
wrapper.appendChild(holder);
wrapper.addEventListener('mousemove', e => throttle(this._onGridMouseMove, HoverInternal, this, e, holder), { passive: true });
}
// loading
const loading = createElement('div', 'ui-grid-loading',
createElement('div', null, createIcon('fa-regular', 'spinner-third'))
);
this._var.refs.loading = loading;
grid.appendChild(loading);
this._var.el = grid;
this._var.rendering = false;
if (this._var.source != null) {
if (this.sortIndex >= 0) {
this.sortColumn(true);
} else if (this.sortArray?.length > 0) {
this.sort(true);
}
}
}
/**
* 设置数据列表,该方法为 [source]{@linkcode Grid#source} 属性的语法糖
* @param {GridItem[]} source - 待设置的数据列表
*/
setData(source) {
this.source = source;
}
/**
* 滚动到指定行的位置
* @param {number} index - 待滚动至的行索引
*/
scrollToIndex(index) {
const top = this._scrollToTop(index * (this.rowHeight + 1), true);
this._var.el.scrollTop = top;
}
/**
* 调整 Grid 元素的大小,一般需要在宽度变化时(如页面大小发生变化时)调用
* @param {boolean} [force] - 是否强制 [reload]{@linkcode Grid#reload},默认只有待渲染的行数发生变化时才会调用
* @param {boolean} [keep] - 是否保持当前滚动位置
* @param {Function} [callback] - 计算大小后的回调函数
* @param {Grid} callback.{this} - 上下文为 Grid
*/
resize(force, keep, callback) {
if (this._var.rendering || this._var.el == null) {
return;
}
const body = this._var.refs.body;
const top = this.headerVisible === false ? 0 : this._var.refs.header.offsetHeight;
let height = this.height;
if (height === 0) {
height = this._var.containerHeight;
} else if (isNaN(height) || height < 0) {
height = this._var.el.offsetHeight - top;
}
const count = truncate((height - 1) / (this.rowHeight + 1)) + (RedumCount * 2) + 1;
if (force || count !== this._var.rowCount) {
this._var.rowCount = count;
if (typeof callback === 'function') {
callback.call(this);
} else {
this.reload(keep);
}
}
this._var.bodyClientWidth = body.clientWidth;
}
/**
* 重新计算需要渲染的行,并载入元素,一般需要在高度变化时调用
* @param {boolean} [keep] - 是否保持当前滚动位置
*/
reload(keep) {
const filtered = this.columns.some(c => c.filterValues != null);
if ((filtered ^ this._var.colAttrs.__filtered) === 1) {
this._var.colAttrs.__filtered = filtered;
const headers = this._headerCells;
for (let i = 0; i < this.columns.length; ++i) {
const ele = headers[i].querySelector('.filter');
if (ele == null) {
continue;
}
if (this.columns[i].filterValues != null) {
ele.classList.add('active');
} else {
ele.classList.remove('active');
}
}
this._refreshSource();
return;
}
let length = this._var.currentSource?.length ?? 0;
if (this.extraRows > 0) {
length += this.extraRows;
}
this._var.containerHeight = length * (this.rowHeight + 1);
if (!keep) {
this._var.el.scrollTop = 0;
// this._var.el.scrollLeft = 0;
this._var.refs.table.style.top = '0px';
}
const headerHeight = this._var.headerHeight || this.rowHeight;
this._var.refs.wrapper.style.height = `${this._var.containerHeight + headerHeight}px`;
this._adjustRows(this._var.refs.body);
this.refresh();
}
/**
* 重新填充Grid单元格数据
*/
refresh() {
if (this._var.refs.body == null) {
throw new Error('body has not been created.');
}
const widths = {};
this._fillRows(this._tableRows, this.columns, widths);
if (this._var.needResize && widths.flag) {
this._var.needResize = false;
this.columns.forEach((col, i) => {
if (!this._get(col.key, 'autoResize')) {
return;
}
let width = widths[i];
if (width < col.width) {
width = col.width;
}
if (width > 0) {
this._changeColumnWidth(i, width);
}
});
} else {
this._var.headerHeight = this._var.refs.table.children[0].offsetHeight;
}
}
/**
* 把所有行重置为未修改的状态
*/
resetChange() {
if (this._var.source == null) {
return;
}
for (let row of this._var.source) {
delete row.__changed;
}
}
_getComparer(col, direction) {
if (typeof col.sortFilter !== 'function') {
if (isNaN(direction)) {
direction = 1;
}
return (a, b) => {
a = this._getItemProp(a.values, true, col);
b = this._getItemProp(b.values, true, col);
if (a == null && typeof b === 'number') {
a = 0;
} else if (typeof a === 'number' && b == null) {
b = 0;
} else if (a != null && b == null) {
return direction;
} else if (typeof a === 'string' && typeof b === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
return a === b ? 0 : (a > b ? 1 : -1) * direction;
};
}
return (a, b) => col.sortFilter(a.values, b.values) * direction;
}
/**
* 根据当前排序字段进行列排序
* @param {boolean} [reload] - 为 `true` 则在列排序后调用 [reload]{@linkcode Grid#reload} 方法
*/
sortColumn(reload) {
const index = this.sortIndex;
const col = this.columns[index];
if (col == null) {
return;
}
this.sortArray = null;
const direction = this.sortDirection;
[...this._headerCells].forEach((th, i) => {
const arrow = th.querySelector('.arrow');
if (arrow == null) {
return;
}
if (i === index) {
arrow.className = `arrow ${(direction !== 1 ? 'desc' : 'asc')}`;
} else if (arrow.className !== 'arrow') {
arrow.className = 'arrow';
}
});
const comparer = this._getComparer(col, direction);
this._var.source.sort(comparer);
if (this._var.colAttrs.__filtered === true) {
this._var.currentSource.sort(comparer);
}
if (this._var.rowCount < 0) {
return;
}
if (reload) {
this.reload();
} else {
this.refresh();
}
}
/**
* 根据当前排序列数组进行多列排序
* @param {boolean} [reload] - 为 `true` 则在多列排序后调用 [reload]{@linkcode Grid#reload} 方法
*/
sort(reload) {
const sortArray = this.sortArray;
if (sortArray == null || sortArray.length === 0) {
return;
}
this.sortIndex = -1;
const comparer = (a, b) => {
for (let i = 0; i < sortArray.length; ++i) {
const s = sortArray[i];
const col = this.columns.find(c => c.key === s.column && c.visible !== false);
if (col != null) {
const result = this._getComparer(col, s.order === 'desc' ? -1 : 1)(a, b);
if (result !== 0) {
return result;
}
}
}
return 0;
};
this._var.source.sort(comparer);
if (this._var.colAttrs.__filtered === true) {
this._var.currentSource.sort(comparer);
}
if (this._var.rowCount < 0) {
return;
}
if (reload) {
this.reload();
} else {
this.refresh();
}
// arrow icon
[...this._headerCells].forEach((th, i) => {
const arrow = th.querySelector('.arrow');
if (arrow == null) {
return;
}
const col = this.columns[i];
const s = sortArray.find(s => s.column === col.key && col.visible !== false);
if (s != null) {
arrow.className = `arrow ${s.order}`;
} else if (arrow.className !== 'arrow') {
arrow.className = 'arrow';
}
});
}
/**
* 清除列头复选框的选中状态
*/
clearHeaderCheckbox() {
const boxes = this._var.refs.header.querySelectorAll('.ui-check-wrapper>input');
boxes.forEach(box => box.checked = false);
}
/**
* 显示多列排序设置面板
*/
showSortPanel() {
const content = createElement('div', 'ui-sort-panel-content');
const buttonWrapper = createElement('div', 'ui-sort-panel-buttons');
const grid = new Grid(null, r);
grid.langs = this.langs;
const rowChanged = index => {
buttonWrapper.querySelector('.ui-button-delete').disabled = index < 0;
buttonWrapper.querySelector('.ui-button-copy').disabled = index < 0;
buttonWrapper.querySelector('.ui-button-move-up').disabled = index < 1;
buttonWrapper.querySelector('.ui-button-move-down').disabled = index >= grid.source.length - 1;
};
grid.onSelectedRowChanged = rowChanged;
const reload = index => {
grid.selectedIndexes = [index];
grid.scrollTop = index * (grid.rowHeight + 1);
rowChanged(index);
}
buttonWrapper.append(
createElement('button', null,
createIcon('fa-light', 'plus'),
createElement('span', span => {
span.innerText = this.langs.addLevel;
span.addEventListener('click', () => {
let index = grid.selectedIndex;
const n = { column: '', order: 'asc' };
if (index >= 0) {
index += 1;
grid.addItem(n, index);
} else {
grid.addItem(n);
index = grid.source.length - 1;
}
reload(index);
});
})
),
createElement('button', 'ui-button-delete',
createIcon('fa-light', 'times'),
createElement('span', span => {
span.innerText = this.langs.deleteLevel;
span.addEventListener('click', () => {
let index = grid.selectedIndex;
if (index < 0) {
return;
}
grid.removeItem(index);
const length = grid.source.length;
if (index >= length) {
index = length - 1;
}
reload(index);
});
})
),
createElement('button', 'ui-button-copy',
createIcon('fa-light', 'copy'),
createElement('span', span => {
span.innerText = this.langs.copyLevel;
span.addEventListener('click', () => {
const index = grid.selectedIndex;
if (index < 0) {
return;
}
const item = grid.source[index];
if (item == null) {
return;
}
grid.addItem(Object.assign({}, item), index + 1);
reload(index + 1);
});
})
),
createElement('button', button => {
button.className = 'ui-button-move-up';
const icon = createIcon('fa-light', 'chevron-up');
icon.addEventListener('click', () => {
const index = grid.selectedIndex;
if (index < 1) {
return;
}
const item = grid.source[index];
if (item == null) {
return;
}
const it = grid.removeItem(index);
grid.addItem(it, index - 1);
reload(index - 1);
});
button.appendChild(icon);
}),
createElement('button', button => {
button.className = 'ui-button-move-down';
const icon = createIcon('fa-light', 'chevron-down');
icon.addEventListener('click', () => {
const index = grid.selectedIndex;
if (index >= grid.source.length - 1) {
return;
}
const item = grid.source[index];
if (item == null) {
return;
}
const it = grid.removeItem(index);
grid.addItem(it, index + 1);
reload(index + 1);
});
button.appendChild(icon);
})
);
const gridWrapper = createElement('div', 'ui-sort-panel-grid');
content.append(buttonWrapper, gridWrapper);
const columnSource = this.columns.filter(c => c.sortable !== false && c.visible !== false);
grid.columns = [
{
key: 'column',
caption: this.langs.column,
width: 270,
type: GridColumnTypeEnum.Dropdown,
dropOptions: {
textKey: 'caption',
valueKey: 'key'
},
source: columnSource,
sortable: false,
orderable: false
},
{
key: 'order',
caption: this.langs.order,
width: 150,
type: GridColumnTypeEnum.Dropdown,
source: [
{ value: 'asc', text: this.langs.asc },
{ value: 'desc', text: this.langs.desc }
],
sortable: false,
orderable: false
}
];
const pop = new Popup({
title: this.langs.sort,
content,
resizable: true,
buttons: [
{
text: this.langs.ok,
trigger: () => {
const source = grid.source;
if (source == null || source.length === 0) {
this.sortArray = null;
} else {
const dict = {};
for (let i = 0; i < source.length; ++i) {
const it = source[i];
if (it.column == null || it.column === '') {
grid.selectedIndexes = [i];
grid.refresh();
showAlert(this.langs.sort, this.langs.requirePrompt, 'warn');
return false;
}
if (Object.prototype.hasOwnProperty.call(dict, it.column)) {
grid.selectedIndexes = [i];
grid.refresh();
let name = columnSource.find(c => c.key === it.column);
if (name == null) {
name = it.column;
} else {
name = name.caption;
}
showAlert(this.langs.sort, this.langs.duplicatePrompt.replace('{column}', name), 'warn');
return false;
}
dict[it.column] = true;
}
this.sortArray = source;
this.sortDirection = 1;
this.sort();
}
if (typeof this.onSorted === 'function') {
this.onSorted(this.sortArray);
}
return true;
}
},
{ text: this.langs.cancel }
],
onResizeEnded: () => grid.resize()
});
const source = this.sortArray || [{ column: '', order: 'asc' }];
pop.show(this._var.el).then(() => {
pop.container.style.cssText += 'width: 520px; height: 400px';
grid.init(gridWrapper);
grid.source = source.filter(s => s.column === '' || columnSource.find(c => c.key === s.column) != null);
grid.selectedIndexes = [0];
grid.refresh();
rowChanged(0);
});
}
_createHeader(table) {
const thead = createElement('thead');
if (this.headerVisible === false) {
thead.style.display = 'none';
}
table.appendChild(thead);
const header = createElement('tr');
thead.appendChild(header);
const sizer = this._var.refs.sizer;
let left = this.expandable ? ExpandableWidth : 0;
if (this.expandable) {
header.appendChild(createElement('th', th => {
th.className = 'ui-expandable sticky';
const w = `${ExpandableWidth}px`;
th.style.cssText = convertCssStyle({
'width': w,
'max-width': w,
'min-width': w,
'left': '0px'
});
}, createElement('div')));
}
for (let col of this.columns) {
if (col.visible === false) {
const hidden = createElement('th', 'column');
hidden.style.display = 'none';
if (col.sortable !== false) {
hidden.dataset.key = col.key;
hidden.addEventListener('click', e => this._onHeaderClicked(e, col, true));
}
header.appendChild(hidden);
continue;
}
// style
const isCheckbox = GridColumnTypeEnum.isCheckbox(col.type);
let type = this._var.colTypes[col.key];
if (type == null) {
if (isNaN(col.type)) {
type = col.type;
} else {
type = ColumnTypeDefs[col.type];
}
type ??= GridColumn;
this._var.colTypes[col.key] = type;
}
if (col.width > 0 || col.shrink || typeof type.createCaption === 'function') {
// col.autoResize = false;
} else {
this._set(col.key, 'autoResize', true);
this._var.needResize = true;
sizer.innerText = col.caption ?? '';
let width = sizer.offsetWidth + 22;
if (!this.readonly && col.enabled !== false && col.allcheck && isCheckbox) {
width += 32;
}
if (col.allowFilter === true) {
width += 14;
}
if (width < MiniColumnWidth) {
width = MiniColumnWidth;
}
col.width = width;
}
col.align ??= isCheckbox ? 'center' : 'left';
if (col.sortable !== false) {
col.sortable = true;
}
let style;
if (col.shrink) {
style = { 'text-align': col.align };
} else {
const w = `${col.width}px`;
style = {
'width': w,
'max-width': w,
'min-width': w,
'text-align': col.align
};
}
this._set(col.key, 'style', style);
// element
const th = createElement('th', 'column');
const thStyle = { ...style };
if (col.isfixed) {
th.classList.add('sticky');
thStyle.left = `${left}px`;
}
left += col.width;
th.dataset.key = col.key;
if (col.sortable) {
thStyle.cursor = 'pointer';
th.addEventListener('click', e => this._onHeaderClicked(e, col));
}
th.style.cssText = convertCssStyle(thStyle);
if (col.orderable !== false) {
col.orderable = true;
th.addEventListener('mousedown', e => this._onDragStart(e, col));
}
const wrapper = createElement('div');
if (col.align === 'right') {
wrapper.style.justifyContent = 'flex-end';
} else if (col.align === 'center') {
wrapper.style.justifyContent = 'center';
}
th.appendChild(wrapper);
if (!this.readonly && col.enabled !== false && col.allcheck && isCheckbox) {
const check = createCheckbox({
onchange: e => this._onColumnAllChecked(col, e.target.checked)
});
wrapper.appendChild(check);
}
let caption;
if (typeof type.createCaption === 'function') {
caption = type.createCaption(col);
} else {
caption = createElement('span');
caption.innerText = col.caption ?? '';
}
if (caption instanceof HTMLElement) {
if (col.captionStyle != null) {
caption.style.cssText = convertCssStyle(col.captionStyle);
}
wrapper.appendChild(caption);
}
// order arrow
if (col.sortable) {
th.appendChild(createElement('layer', 'arrow'));
}
// filter
if (col.allowFilter === true) {
const filter = createElement('layer', 'filter');
filter.appendChild(createIcon('fa-solid', 'filter'));
filter.addEventListener('mousedown', e => this._onFilter(e, col));
th.classList.add('header-filter');
th.appendChild(filter);
}
// resize spliter
if (col.resizable !== false) {
const spliter = createElement('layer', 'spliter');
spliter.addEventListener('mousedown', e => this._onResizeStart(e, col));
spliter.addEventListener('dblclick', e => this._onAutoResize(e, col));
th.appendChild(spliter);
}
// tooltip
// !nullOrEmpty(col.tooltip) && setTooltip(th, col.tooltip);
header.appendChild(th);
}
const dragger = createElement('div', 'dragger');
const draggerCursor = createElement('layer', 'dragger-cursor');
header.appendChild(
createElement('th', null, dragger, draggerCursor, createElement('div')));
sizer.replaceChildren();
this._var.refs.header = header;
this._var.refs.dragger = dragger;
this._var.refs.draggerCursor = draggerCursor;
return thead;
}
_createBody(table) {
const body = createElement('tbody');
table.appendChild(body);
const cols = this.columns;
let width = 1;
for (let col of cols) {
if (col.visible !== false && !isNaN(col.width)) {
width += col.width + 1;
}
}
if (this.expandable) {
width += ExpandableWidth;
}
table.style.width = `${width}px`;
// body content
body.addEventListener('dblclick', e => this._onRowDblClicked(e));
// this._adjustRows(body);
this._var.refs.body = body;
// this.refresh();
return body;
}
_adjustRows(content) {
let count = this._var.rowCount;
if (isNaN(count) || count < 0 || !this.virtual) {
count = this._var.currentSource?.length ?? 0;
}
const cols = this.columns;
const exists = content.querySelectorAll('&>.ui-grid-row').length;
count -= exists;
if (count > 0) {
for (let i = 0; i < count; ++i) {
const row = createElement('tr', 'ui-grid-row');
let left = this.expandable ? ExpandableWidth : 0;
if (this.expandable) {
const icon = createIcon('fa-solid', 'caret-right');
icon.dataset.expanded = '0';
row.appendChild(createElement('td', td => {
td.className = 'ui-expandable sticky';
td.style.cssText = 'left: 0px';
td.addEventListener('mousedown', e => {
this._onExpandable(e, exists + i, row);
e.stopPropagation();
});
},
icon
));
}
cols.forEach((col, j) => {
const cell = createElement('td', 'ui-grid-cell');
if (col.visible !== false) {
let style = this._get(col.key, 'style') ?? {};
if (col.isfixed) {
cell.classList.add('sticky');
style.left = `${left}px`;
}
left += col.width;
cell.dataset.row = String(exists + i);
cell.dataset.col = String(j);
if (col.css != null) {
style = { ...style, ...col.css };
}
style = convertCssStyle(style);
if (style !== '') {
cell.style.cssText = style;
}
if (GridColumnTypeEnum.isCheckbox(col.type)) {
cell.appendChild(GridCheckboxColumn.createEdit(e => this._onRowChanged(e, exists + i, col, e.target.checked, cell)));
// this._var.colTypes[col.key] = GridCheckboxColumn;
} else {
let type = this._var.colTypes[col.key];
if (type == null) {
if (isNaN(col.type)) {
type = col.type;
} else {
type = ColumnTypeDefs[col.type];
}
type ??= GridColumn;
this._var.colTypes[col.key] = type;
}
const element = type.create(col, i, this);
if (typeof col.class === 'string') {
type.setClass(element, col.class);
}
cell.appendChild(element);
}
} else {
cell.style.display = 'none';
}
row.appendChild(cell);
});
row.appendChild(createElement('td', td => td.innerText = '\u00a0'));
content.appendChild(row);
}
} else if (count < 0) {
let last = content.querySelectorAll('&>.ui-grid-row')[exists + count];
while (last != null) {
const next = last.nextElementSibling;
last.remove();
last = next;
}
}
}
_fillRows(rows, cols, widths) {
const startIndex = this._var.startIndex;
const selectedIndexes = this._var.selectedIndexes;
const stateChanged =
this._var.oldIndex !== startIndex ||
selectedIndexes == null ||
this._var.oldSelectedIndexes?.length !== selectedIndexes.length ||
this._var.oldSelectedIndexes.find((s, i) => s !== selectedIndexes[i]) != null;
if (stateChanged) {
this._var.oldIndex = startIndex;
if (selectedIndexes != null) {
this._var.oldSelectedIndexes = selectedIndexes.slice();
}
}
const offset = this.expandable ? 1 : 0;
[...rows].forEach((row, i) => {
const vals = this._var.currentSource[startIndex + i];
if (vals == null) {
return;
}
if (!isPositive(row.children.length)) {
return;
}
const item = vals.values;
const selected = selectedIndexes.includes(startIndex + i);
if (selected) {
row.classList.add('selected');
} else if (row.classList.contains('selected')) {
row.classList.remove('selected');
}
// data
if (this.expandable) {
const expanded = vals.__expanded;
let rowExpanded = row.nextElementSibling;
if (rowExpanded?.className !== 'ui-grid-row-expanded') {
rowExpanded = null;
}
if (expanded) {
let expandableObject = vals.__expandable_object;
if (expandableObject == null && typeof this.expandableGenerator === 'function') {
expandableObject = this.expandableGenerator(item);
if (expandableObject?.element == null) {
return;
}
expandableObject.element = createElement('td', td => {
td.colSpan = cols.length + 2;
},
expandableObject.element
);
vals.__expandable_object = expandableObject;
}
if (rowExpanded == null) {
rowExpanded = createElement('tr', 'ui-grid-row-expanded');
this._var.refs.body.insertBefore(rowExpanded, row.nextElementSibling);
} else {
rowExpanded.style.display = '';
}
rowExpanded.replaceChildren(expandableObject.element);
} else {
if (rowExpanded != null) {
rowExpanded.style.display = 'none';
}
}
const iconCell = row.children[0];
if (iconCell.children[0].dataset.expanded !== (expanded ? '1' : '0')) {
const icon = createIcon('fa-solid', expanded ? 'caret-down' : 'caret-right');
icon.dataset.expanded = expanded ? '1' : '0';
iconCell.replaceChildren(icon);
if (expanded) {
if (typeof this.onRowExpanded === 'function') {
this.onRowExpanded(vals.values, vals.__expandable_object);
}
} else {
if (typeof this.onRowCollapsed === 'function') {
this.onRowCollapsed(vals.values, vals.__expandable_object);
}
}
}
}
cols.forEach((col, j) => {
if (col.visible === false) {
return;
}
const cell = row.children[j + offset];
if (cell == null) {
return;
}
let val;
if (col.text != null) {
val = col.text;
} else if (typeof col.filter === 'function') {
val = col.filter(item, selected, this._var.refs.body);
} else {
val = item[col.key];
if (val?.DisplayValue != null) {
val = val.DisplayValue;
}
}
val ??= '';
// fill
let bg = col.background;
if (bg != null) {
if (typeof bg === 'function') {
bg = col.background(item);
}
cell.style.backgroundColor = bg ?? '';
} else if (typeof col.bgFilter === 'function') {
const bgColor = col.bgFilter(item);
cell.style.backgroundColor = bgColor ?? '';
}
const isCheckbox = GridColumnTypeEnum.isCheckbox(col.type);
const type = isCheckbox ? GridCheckboxColumn : this._var.colTypes[col.key] ?? GridColumn;
let element;
if (!isCheckbox && typeof type.createEdit === 'function') {
if (vals.__editing?.[col.key]) {
if (typeof type.leaveEdit === 'function') {
type.leaveEdit(cell.children[0], this._var.el);
}
if (type.editing) {
val = type.getValue({ target: cell.children[0] }, col);
this._onRowChanged(null, i, col, val, cell, true);
}
}
if (stateChanged) {
element = selected ?
type.createEdit(e => this._onRowChanged(e, i, col, type.getValue(e, col), cell), col, this._var.el, vals) :
type.create(col, i, this);
if (typeof col.class === 'string') {
type.setClass(element, col.class);
}
cell.replaceChildren(element);
} else {
element = cell.children[0];
}
} else {
element = cell.children[0];
}
let enabled;
if (this.readonly) {
enabled = false;
} else {
enabled = col.enabled;
if (typeof enabled === 'function') {
enabled = enabled.call(col, item);
} else if (typeof enabled === 'string') {
enabled = item[enabled];
}
}
type.setValue(element, val, vals, col, this);
let tip = col.tooltip;
if (typeof tip === 'function') {
tip = tip.call(col, item);
}
if (nullOrEmpty(tip)) {
element.querySelector('.ui-tooltip-wrapper')?.remove();
} else {
setTooltip(element, tip, false, this.element);
}
if (typeof type.setEnabled === 'function') {
type.setEnabled(element, enabled);
}
// auto resize
if (this._var.needResize && this._get(col.key, 'autoResize')) {
const width = element.scrollWidth + 12;
if (width > 0 && widths != null && (isNaN(widths[j]) || widths[j] < width)) {
widths[j] = width;
widths.flag = true;
}
}
let style = col.style;
if (style != null) {
if (typeof style === 'function') {
style = col.style(item);
}
if (style != null) {
type.setStyle(element, style);
} else {
element.style.cssText = '';
}
} else if (typeof col.styleFilter === 'function') {
const style = col.styleFilter(item);
if (style != null) {
type.setStyle(element, style);
} else {
element.style.cssText = '';
}
}
if (col.events != null) {
for (let ev of Object.entries(col.events)) {
element[ev[0]] = ev[1].bind(item);
}
}
if (col.attrs != null) {
let attrs = col.attrs;
if (typeof attrs === 'function') {
attrs = attrs(item);
}
for (let attr of Object.entries(attrs)) {
element.setAttribute(attr[0], attr[1]);
}
}
});
if (vals.__editing != null) {
delete vals.__editing;
}
});
}
_changeColumnWidth(index, width) {
const col = this.columns[index];
// const oldwidth = col.width;
const w = `${width}px`;
col.width = width;
const style = this._get(col.key, 'style');
style.width = w;
style['max-width'] = w;
style['min-width'] = w;
const headerCells = this._headerCells;
let element = headerCells[index];
element.style.width = w;
element.style.maxWidth = w;
element.style.minWidth = w;
// element.style.cssText += `width: ${w}; max-width: ${w}; min-width: ${w}`;
let left = this.expandable ? ExpandableWidth : 0;
if (col.isfixed) {
left = element.offsetLeft + width;
let l = left;
for (let i = index + 1; i < this.columns.length; ++i) {
if (this.columns[i].isfixed) {
headerCells[i].style.left = `${l}px`;
l += this.columns[i].width;
} else {
break;
}
}
}
const offset = this.expandable ? 1 : 0;
for (let row of this._tableRows) {
element = row.children[index + offset];
if (element != null) {
element.style.width = w;
element.style.maxWidth = w;
element.style.minWidth = w;
// element.style.cssText += `width: ${w}; max-width: ${w}; min-width: ${w}`;
if (col.isfixed) {
let l = left;
for (let i = index + offset + 1; i < this.columns.length; ++i) {
if (this.columns[i].isfixed) {
row.children[i].style.left = `${l}px`;
l += this.columns[i].width;
} else {
break;
}
}
}
}
}
this._var.headerHeight = this._var.refs.table.children[0].offsetHeight;
}
_changingColumnOrder(index, offset, mouse, draggerCellLeft) {
const children = this._headerCells;
let element = children[index];
this._var.refs.dragger.style.cssText = `left: ${element.offsetLeft - draggerCellLeft + offset}px; width: ${element.style.width}; display: block`;
// offset = x + gridScrollLeft - element.offsetLeft; // getOffsetLeftFromWindow(element);
offset += mouse;
let idx;
const toLeft = offset < 0;
if (toLeft) {
offset = -offset;
for (let i = index - 1; i >= 0 && offset >= 0; i -= 1) {
element = children[i];
if (element == null || !element.className || element.classList.contains('sticky')) {
idx = i + 1;
break;
}
if (offset < element.offsetWidth) {
idx = (offset > element.offsetWidth / 2) ? i : i + 1;
break;
}
offset -= element.offsetWidth;
}
idx ??= 0;
} else {
const count = children.length;
for (let i = index; i < count - 1 && offset >= 0; ++i) {
element = children[i];
if (element == null || !element.className || element.classList.contains('sticky')) {
idx = i;
break;
}
if (offset < element.offsetWidth) {
idx = (offset > element.offsetWidth / 2) ? i + 1 : i;
break;
}
offset -= element.offsetWidth;
}
idx ??= count - 1;
}
if (idx !== this._var.colAttrs.__orderIndex || this._var.refs.draggerCursor.style.display !== 'block') {
element = children[idx];
if (element == null) {
return;
}
this._var.colAttrs.__orderIndex = idx;
// avoid `offsetLeft` of hidden header to be 0
let left;
if (element.style.display === 'none') {
left = 0;
while (left === 0 && (element = children[++idx]) != null) {
left = element.offsetLeft;
}
if (!toLeft && left === 0) {
left = draggerCellLeft;
}
} else {
left = element.offsetLeft;
}
// set position of dragger cursor
this._var.refs.draggerCursor.style.cssText = `left: ${left - draggerCellLeft}px; display: block`;
}
}
_changeColumnOrder(index) {
this._var.refs.dragger.style.display = '';
this._var.refs.draggerCursor.style.display = '';
const orderIndex = this._var.colAttrs.__orderIndex;
if (orderIndex >= 0 && orderIndex !== index) {
let targetIndex = orderIndex - index;
if (targetIndex >= 0 && targetIndex <= 1) {
return;
}
const header = this._var.refs.header;
const children = this._headerCells;
const rows = this._tableRows;
const columns = this.columns;
const offset = this.expandable ? 1 : 0;
if (targetIndex > 1) {
targetIndex = orderIndex - 1;
// const current = columns[index];
// for (let i = index; i < targetIndex; ++i) {
// columns[i] = columns[i + 1];
// }
// columns[targetIndex] = current;
const current = columns.splice(index, 1)[0];
columns.splice(targetIndex, 0, current);
header.insertBefore(children[index], children[targetIndex].nextElementSibling);
for (let row of rows) {
row.insertBefore(row.children[index + offset], row.children[targetIndex + offset].nextElementSibling);
}
} else {
targetIndex = orderIndex;
// const current = columns[index];
// for (let i = index; i > targetIndex; i -= 1) {
// columns[i] = columns[i - 1];
// }
// columns[targetIndex] = current;
const current = columns.splice(index, 1)[0];
columns.splice(targetIndex, 0, current);
header.insertBefore(children[index], children[targetIndex]);
for (let row of rows) {
row.insertBefore(row.children[index + offset], row.children[targetIndex + offset]);
}
}
if (this.sortArray == null || this.sortArray.length === 0) {
// refresh sortIndex
[...children].forEach((th, i) => {
const arrow = th.querySelector('.arrow');
if (arrow == null) {
return;
}
if (arrow.className !== 'arrow') {
this.sortIndex = i;
}
});
}
if (typeof this.onColumnChanged === 'function') {
this.onColumnChanged(ColumnChangedType.Reorder, index, targetIndex);
}
}
}
_scrollToTop(top, reload) {
const rowHeight = (this.rowHeight + 1);
top -= (top % (rowHeight * 2)) + (RedumCount * rowHeight);
if (top < 0) {
top = 0;
} else {
let bottomTop = this._var.containerHeight - (reload ? 0 : this._var.rowCount * rowHeight);
if (bottomTop < 0) {
bottomTop = 0;
}
if (top > bottomTop) {
top = bottomTop;
}
}
if (this._var.scrollTop !== top) {
this._var.scrollTop = top;
if (this.virtual) {
this._var.startIndex = top / rowHeight;
}
this.refresh();
if (this.virtual) {
this._var.refs.table.style.top = `${top}px`;
}
} else if (reload) {
this.refresh();
}
return top;
}
_get(key, name) {
const attr = this._var.colAttrs[key];
if (attr == null) {
return null;
}
return attr[name];
}
_set(key, name, value) {
const attr = this._var.colAttrs[key];
if (attr == null) {
this._var.colAttrs[key] = { [name]: value };
} else {
attr[name] = value;
}
}
_getItemProp(item, editing, col) {
let value;
if (typeof col?.filter === 'function') {
value = col.filter(item, editing, this._var.refs.body);
} else {
value = item[col.key];
}
if (value == null) {
return value;
}
const prop = editing ? 'Value' : 'DisplayValue';
if (Object.prototype.hasOwnProperty.call(value, prop)) {
return value[prop];
}
return value;
}
_getRowTarget(target) {
let parent;
while ((parent = target.parentElement) != null && !parent.classList.contains('ui-grid-row')) {
target = parent;
}
return [parent, target];
}
_getParentElement(element) {
while (element != null && element.className !== 'ui-grid') {
element = element.parentElement;
}
return element;
}
_notHeader(tagName) {
return /^(input|label|layer|svg|use)$/i.test(tagName);
}
_onHeaderClicked(e, col, force) {
if (!force && (this._get(col.key, 'resizing') || this._get(col.key, 'dragging'))) {
return;
}
if (!this._notHeader(e.target.tagName)) {
const index = this.columns.indexOf(col);
if (index < 0) {
return;
}
if (this.sortIndex === index) {
this.sortDirection = this.sortDirection === 1 ? -1 : 1;
} else {
this.sortIndex = index;
}
this.sortColumn();
if (typeof this.onColumnChanged === 'function') {
this.onColumnChanged(ColumnChangedType.Sort, index, this.sortDirection);
}
}
}
_onCloseFilter(e) {
if (e != null) {
if ((e.target.tagName === 'LAYER' && e.target.classList.contains('filter')) ||
e.target.tagName === 'use') {
return false;
}
}
const panels = this._var.el.querySelectorAll('.filter-panel.active');
if (panels.length > 0) {
panels.forEach(el => el.classList.remove('active'));
setTimeout(() => this._var.el.querySelectorAll('.filter-panel').forEach(el => el.remove()), 120);
const filtering = this._var.colAttrs.__filtering;
if (filtering instanceof HTMLElement) {
filtering.classList.remove('hover');
}
delete this._var.colAttrs.__filtering;
document.removeEventListener('mousedown', this._onCloseFilter);
return true;
}
return false;
}
_onFilter(e, col) {
if (this._onCloseFilter()) {
return;
}
document.addEventListener('mousedown', this._onCloseFilter.bind(this));
const panel = createElement('div', 'filter-panel');
panel.addEventListener('mousedown', e => e.stopPropagation());
const filter = e.currentTarget;
const th = filter.parentElement;
const width = th.offsetWidth;
panel.style.top = `${th.offsetHeight + this._var.el.scrollTop}px`;
const offsetLeft = th.offsetLeft;
const totalWidth = th.parentElement.offsetWidth;
const left = offsetLeft + FilterPanelWidth > totalWidth ?
totalWidth - FilterPanelWidth :
offsetLeft + (width > FilterPanelWidth ? width - FilterPanelWidth : 0);
panel.style.left = `${left}px`;
// search
let searchbox;
if (col.allowSearch !== false) {
const searchholder = createElement('div', 'filter-search-holder');
searchbox = createElement('input', 'filter-search-box ui-text');
searchbox.type = 'text';
const searchicon = createIcon('fa-regular', 'search');
searchicon.addEventListener('mousedown', e => {
searchbox.focus();
e.preventDefault();
});
searchholder.append(searchbox, searchicon);
panel.append(searchholder);
}
// list
const itemlist = createElement('div', 'filter-item-list');
itemlist.addEventListener('scroll', e => throttle(this._onFilterScroll, RefreshInterval, this, col, itemlist, e.target.scrollTop), { passive: true });
// - all
const itemall = createElement('div', 'filter-item filter-all');
itemall.appendChild(createCheckbox({
label: this.langs.all,
onchange: e => {
const checked = e.target.checked;
itemlist.querySelectorAll('.filter-content input').forEach(box => box.checked = checked);
for (let it of this._get(col.key, 'filterSource')) {
it.__checked = checked;
}
}
}));
itemlist.appendChild(itemall);
// - items
let array;
if (Array.isArray(col.filterSource)) {
array = col.filterSource;
} else if (typeof col.filterSource === 'function') {
array = col.filterSource.call(this, col);
} else {
const dict = Object.create(null);
for (let item of this._var.source) {
let displayValue = this._getItemProp(item.values, false, col);
if (displayValue == null) {
displayValue = this.langs.null;
}
if (!Object.hasOwnProperty.call(dict, displayValue)) {
const val = this._getItemProp(item.values, true, col);
dict[displayValue] = {
Value: val,
DisplayValue: displayValue
};
}
}
array = Object.values(dict)
.sort((a, b) => {
if (a == null && b == null) {
return 0;
}
if (a == null && b != null) {
return -1;
}
if (a != null && b == null) {
return 1;
}
if (Object.prototype.hasOwnProperty.call(a, 'Value')) {
a = a.Value;
}
if (Object.prototype.hasOwnProperty.call(b, 'Value')) {
b = b.Value;
}
return a > b ? 1 : a < b ? -1 : 0;
});
}
array = array.map(i => {
if (Object.prototype.hasOwnProperty.call(i, 'Value') &&
Object.prototype.hasOwnProperty.call(i, 'DisplayValue')) {
return i;
}
return {
Value: i,
DisplayValue: i == null ? this.langs.null : i
};
});
this._fillFilterList(col, itemlist, array, itemall);
itemall.querySelector('input').checked = ![...itemlist.querySelectorAll('.filter-content input')].some(i => !i.checked);
panel.appendChild(itemlist);
if (searchbox != null) {
searchbox.addEventListener('input', e => {
const key = e.currentTarget.value.toLowerCase();
const items = key.length === 0 ? array : array.filter(i => {
let displayValue;
if (i != null && Object.prototype.hasOwnProperty.call(i, 'DisplayValue')) {
displayValue = i.DisplayValue;
} else {
displayValue = i;
}
if (displayValue == null) {
displayValue = this.langs.null;
}
return String(displayValue).toLowerCase().includes(key);
});
this._fillFilterList(col, itemlist, items, itemall);
});
}
// function
const functions = createElement('div', 'filter-function');
functions.append(
createElement('button', ok => {
ok.innerText = this.langs.ok;
ok.addEventListener('click', () => {
const array = this._get(col.key, 'filterSource').filter(i => i.__checked !== false);
if (typeof col.onFilterOk === 'function') {
col.onFilterOk.call(this, col, array);
} else {
if (GridColumnTypeEnum.isCheckbox(col.type)) {
col.filterValues = array.map(a => a.Value);
} else {
col.filterValues = array.map(a => a.DisplayValue);
}
}
this._var.colAttrs.__filtered = true;
this._refreshSource();
if (typeof col.onFiltered === 'function') {
col.onFiltered.call(this, col);
}
filter.classList.add('active');
this._onCloseFilter();
});
}),
createElement('button', reset => {
reset.innerText = this.langs.reset;
reset.addEventListener('click', () => {
delete col.filterValues;
this._var.colAttrs.__filtered = this.columns.some(c => c.filterValues != null)
this._refreshSource();
if (typeof col.onFiltered === 'function') {
col.onFiltered.call(this, col);
}
filter.classList.remove('active');
this._onCloseFilter();
});
})
);
panel.appendChild(functions);
this._var.el.appendChild(panel);
setTimeout(() => panel.classList.add('active'), 0);
this._var.colAttrs.__filtering = filter;
filter.classList.add('hover');
}
_fillFilterList(col, list, array, all) {
list.querySelector('.filter-holder')?.remove();
list.querySelector('.filter-content')?.remove();
const rowHeight = this.filterRowHeight;
const height = array.length * rowHeight;
this._set(col.key, 'filterHeight', height);
const holder = createElement('div', 'filter-holder');
holder.style.height = `${height}px`;
const content = createElement('div', 'filter-content');
content.style.top = `${rowHeight}px`;
this._set(col.key, 'filterSource', array);
const propKey = GridColumnTypeEnum.isCheckbox(col.type) ? 'Value' : 'DisplayValue';
for (let item of array) {
const v = Object.prototype.hasOwnProperty.call(item, propKey) ? item[propKey] : item;
item.__checked = !Array.isArray(col.filterValues) || col.filterValues.includes(v);
}
if (array.length > 12) {
array = array.slice(0, 12);
}
this._doFillFilterList(content, array, all);
list.append(holder, content);
}
_doFillFilterList(content, array, all) {
for (let item of array) {
const div = createElement('div', 'filter-item');
div.appendChild(createCheckbox({
checked: item.__checked,
label: Object.prototype.hasOwnProperty.call(item, 'DisplayValue') ? item.DisplayValue : item,
onchange: e => {
item.__checked = e.target.checked;
all.querySelector('input').checked = ![...content.querySelectorAll('input')].some(i => !i.checked);
}
}));
content.appendChild(div);
}
}
_onFilterScroll(col, list, top) {
const rowHeight = this.filterRowHeight;
top -= (top % (rowHeight * 2)) + rowHeight;
if (top < 0) {
top = 0;
} else {
let bottomTop = this._get(col.key, 'filterHeight') - (12 * rowHeight);
if (bottomTop < 0) {
bottomTop = 0;
}
if (top > bottomTop) {
top = bottomTop;
}
}
if (this._get(col.key, 'filterTop') !== top) {
this._set(col.key, 'filterTop', top);
const startIndex = top / rowHeight;
let array = this._get(col.key, 'filterSource');
if (startIndex + 12 < array.length) {
array = array.slice(startIndex, startIndex + 12);
} else {
array = array.slice(-12);
}
const content = list.querySelector('.filter-content');
content.replaceChildren();
this._doFillFilterList(content, array, list.querySelector('.filter-all'));
content.style.top = `${top + rowHeight}px`;
}
}
_onDragStart(e, col) {
if (this._notHeader(e.target.tagName)) {
return;
}
if (e.currentTarget.classList.contains('sticky')) {
return;
}
const index = indexOfParent(e.currentTarget) - (this.expandable ? 1 : 0);
const cx = getClientX(e);
const clearEvents = attr => {
for (let event of ['mousemove', 'mouseup']) {
if (Object.prototype.hasOwnProperty.call(attr, event)) {
window.removeEventListener(event, attr[event]);
delete attr[event];
}
}
};
let attr = this._var.colAttrs[col.key];
if (attr == null) {
attr = this._var.colAttrs[col.key] = {};
} else {
clearEvents(attr);
}
attr.dragging = true;
const draggerCellLeft = this._var.refs.header.querySelector('th:last-child').offsetLeft;
// const gridScrollLeft = this._var.el.scrollLeft;
let p = this._var.el;
let gridLeftFromWindow = p.offsetLeft;
while ((p = p.offsetParent) != null) {
gridLeftFromWindow += p.offsetLeft + p.clientLeft;
}
const mouse = cx - e.currentTarget.offsetLeft + this._var.el.scrollLeft - gridLeftFromWindow;
const dragmove = e => {
const cx2 = getClientX(e);
const offset = cx2 - cx;
let pos = attr.offset;
let dragging;
if (pos == null) {
if (offset > MiniDragOffset || offset < -MiniDragOffset) {
dragging = true;
}
} else if (pos !== offset) {
dragging = true;
}
if (dragging) {
this._changingColumnOrder(index, offset, mouse, draggerCellLeft);
attr.offset = offset;
}
};
attr.mousemove = e => throttle(dragmove, RefreshInterval, this, e);
attr.mouseup = () => {
clearEvents(attr);
if (attr.offset == null) {
delete attr.dragging;
} else {
setTimeout(() => {
delete attr.dragging;
delete attr.offset;
});
this._changeColumnOrder(index);
}
};
['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event]));
}
_onResizeStart(e, col) {
const cx = getClientX(e);
const width = col.width;
const index = indexOfParent(e.currentTarget.parentElement) - (this.expandable ? 1 : 0);
const window = this.window ?? global;
const clearEvents = attr => {
for (let event of ['mousemove', 'mouseup']) {
if (Object.prototype.hasOwnProperty.call(attr, event)) {
window.removeEventListener(event, attr[event]);
delete attr[event];
}
}
};
let attr = this._var.colAttrs[col.key];
if (attr == null) {
attr = this._var.colAttrs[col.key] = {};
} else {
clearEvents(attr);
}
attr.resizing = width;
const resizemove = e => {
const cx2 = getClientX(e);
const val = width + (cx2 - cx);
if (val < MiniColumnWidth) {
return;
}
attr.resizing = val;
attr.sizing = true;
this._changeColumnWidth(index, val);
};
attr.mousemove = e => throttle(resizemove, RefreshInterval, this, e);
attr.mouseup = e => {
clearEvents(attr);
const width = attr.resizing;
if (width != null) {
setTimeout(() => delete attr.resizing);
if (attr.sizing) {
delete attr.sizing;
delete attr.autoResize;
this._changeColumnWidth(index, width);
if (typeof this.onColumnChanged === 'function') {
this.onColumnChanged(ColumnChangedType.Resize, index, width);
}
}
}
e.stopPropagation();
e.preventDefault();
};
['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event]));
}
_onAutoResize(e, col) {
const th = e.currentTarget.parentElement;
const index = indexOfParent(th);
const offset = this.expandable ? 1 : 0;
let width = th.querySelector('div:first-child').scrollWidth;
for (let row of this._tableRows) {
const element = row.children[index + offset].children[0];
const w = element.scrollWidth;
if (w > width) {
width = w;
}
}
if (width < MiniColumnWidth) {
width = MiniColumnWidth;
}
if (width > 0 && width !== col.width) {
width += 12;
this._changeColumnWidth(index - offset, width);
if (typeof this.onColumnChanged === 'function') {
this.onColumnChanged(ColumnChangedType.Resize, index - offset, width);
}
}
}
_onColumnAllChecked(col, flag) {
if (this._var.currentSource == null) {
return;
}
const key = col.key;
const isFunction = typeof col.enabled === 'function';
const isString = typeof col.enabled === 'string';
if (typeof col.onAllChecked === 'function') {
col.onAllChecked.call(this, col, flag);
} else {
for (let row of this._var.currentSource) {
const item = row.values;
if (item == null) {
continue;
}
const enabled = isFunction ? col.enabled(item) : isString ? item[col.enabled] : col.enabled;
if (enabled !== false) {
item[key] = flag;
row.__changed = true;
if (typeof col.onChanged === 'function') {
col.onChanged.call(this, item, flag);
}
}
}
this.refresh();
}
}
_onScroll(e) {
if (this._var.colAttrs.__filtering != null) {
this._onCloseFilter();
}
if (this.onBodyScrolled === 'function') {
this.onBodyScrolled(e);
}
if (!this.virtual) {
return;
}
const top = e.target.scrollTop;
this._scrollToTop(top);
}
_onGridMouseMove(e, holder) {
e.stopPropagation();
if (e.target.classList.contains('ui-grid-hover-holder')) {
return;
}
let [parent, target] = this._getRowTarget(e.target);
if (parent == null) {
delete holder.dataset.row;
delete holder.dataset.col;
if (holder.classList.contains('active')) {
holder.classList.remove('active');
}
return;
}
if (this._getParentElement(parent) !== this._var.el) {
// sub ui-grid
return;
}
const element = target.children[0];
if (element?.tagName !== 'SPAN') {
if (holder.classList.contains('active')) {
delete holder.dataset.row;
delete holder.dataset.col;
holder.classList.remove('active');
}
return;
}
const row = target.dataset.row;
const col = target.dataset.col;
if (holder.dataset.row === row &&
holder.dataset.col === col) {
return;
}
if (element.scrollWidth > element.offsetWidth) {
holder.dataset.row = row;
holder.dataset.col = col;
holder.innerText = element.innerText;
const top = target.offsetTop + this._var.refs.table.offsetTop;
let left = target.offsetLeft;
let width = holder.offsetWidth;
if (width > this._var.bodyClientWidth) {
width = this._var.bodyClientWidth;
}
const maxleft = this._var.bodyClientWidth + this._var.scrollLeft - width;
if (left > maxleft) {
left = maxleft;
}
const height = target.offsetHeight;
holder.style.cssText = `top: ${top}px; left: ${left}px; max-width: ${this._var.bodyClientWidth}px; height: ${height - 2}px`;
holder.classList.add('active');
} else if (holder.classList.contains('active')) {
delete holder.dataset.row;
delete holder.dataset.col;
holder.classList.remove('active');
}
}
_onRowClicked(e, index, colIndex) {
const startIndex = this._var.startIndex;
const selectedIndex = startIndex + index;
if (typeof this.willSelect === 'function' && !this.willSelect(selectedIndex, colIndex)) {
return;
}
// multi-select
let flag = false;
const selectedIndexes = this._var.selectedIndexes;
if (this.multiSelect) {
if (e.ctrlKey) {
const i = selectedIndexes.indexOf(selectedIndex);
if (i < 0) {
selectedIndexes.push(selectedIndex);
} else {
selectedIndexes.splice(i, 1);
}
flag = true;
} else if (e.shiftKey && selectedIndexes.length > 0) {
if (selectedIndexes.length > 1 || selectedIndexes[0] !== selectedIndex) {
let start = selectedIndexes[selectedIndexes.length - 1];
let end;
if (start > selectedIndex) {
end = start;
start = selectedIndex;
} else {
end = selectedIndex;
}
selectedIndexes.splice(0);
for (let i = start; i <= end; ++i) {
selectedIndexes.push(i);
}
flag = true;
}
}
}
if (!flag && (selectedIndexes.length !== 1 || selectedIndexes[0] !== selectedIndex)) {
selectedIndexes.splice(0, selectedIndexes.length, selectedIndex);
flag = true;
}
// apply style
if (flag) {
if (this.readonly !== true) {
this.refresh();
} else {
this._tableRows.forEach((row, i) => {
if (selectedIndexes.includes(startIndex + i)) {
row.classList.add('selected');
} else if (row.classList.contains('selected')) {
row.classList.remove('selected');
}
});
}
if (typeof this.onSelectedRowChanged === 'function') {
this.onSelectedRowChanged(selectedIndex);
}
}
this._var.selectedColumnIndex = colIndex;
if ((this.fullrowClick || colIndex >= 0) && e.buttons === 1 && typeof this.cellClicked === 'function') {
if (this.cellClicked(selectedIndex, colIndex) === false) {
e.stopPropagation();
e.preventDefault();
}
}
}
_onRowDblClicked(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'LAYER' && e.target.className === 'ui-check-inner' || e.target.tagName === 'LABEL' && (e.target.className === 'ui-drop-text' || e.target.className === 'ui-drop-caret')) {
return;
}
const index = this.selectedIndex;
if (typeof this.onRowDblClicked === 'function') {
this.onRowDblClicked(index);
}
if (typeof this.onCellDblClicked === 'function') {
const colIndex = this._var.selectedColumnIndex;
if (this.fullrowClick || colIndex >= 0) {
this.onCellDblClicked(index, colIndex);
}
}
}
_onRowChanged(e, index, col, value, cell, blur) {
if (this._var.currentSource == null) {
return;
}
const row = this._var.currentSource[this._var.startIndex + index];
delete row.source;
const item = row.values;
if (item == null) {
return;
}
let enabled = col.enabled;
if (typeof enabled === 'function') {
enabled = enabled.call(col, item);
} else if (typeof enabled === 'string') {
enabled = item[enabled];
}
if (enabled !== false) {
const val = item[col.key];
let oldValue;
if (val != null && Object.prototype.hasOwnProperty.call(val, 'Value')) {
oldValue = val.Value;
val.Value = value;
} else {
oldValue = val;
item[col.key] = value;
}
let tip = col.tooltip;
if (typeof tip === 'function') {
tip = tip.call(col, item);
}
if (nullOrEmpty(tip)) {
cell.querySelector('.ui-tooltip-wrapper')?.remove();
} else {
setTooltip(cell.children[0], tip, false, this.element);
}
row.__changed = true;
if (blur) {
if (typeof col.onInputEnded === 'function') {
col.onInputEnded.call(this, item, value);
}
} else {
if (typeof col.onChanged === 'function') {
col.onChanged.call(this, item, value, oldValue, e);
}
}
}
}
_onExpandable(_e, index, _row) {
if (this._var.currentSource == null) {
return;
}
const vals = this._var.currentSource[this._var.startIndex + index];
vals.__expanded = !vals.__expanded
this.refresh();
}
}