feature: expandable rows

add: comments
This commit is contained in:
lchen committed 2024-02-01 17:29:30 +08:00
1 parent 24b5a22595
commit 5d199a2bfb
5 files changed
+574 -111

No files matched your search

+34 -20
View File
@@ -12,6 +12,17 @@ import { createDateInput, formatDate, setDateValue, getDateValue } from "../date
* @class
*/
export class GridColumn {
/**
* 设置该类型是否支持触发 {@linkcode GridColumnDefinition}`.onInputEnded` 方法<br/>
* 该属性返回 `true` 后,在任意事件中修改行包装对象的 `__editing` 值,则会在行列元素变动时及时触发 [onInputEnded]{@linkcode GridColumnDefinition#onInputEnded} 方法,避免例如文本框还未触发 `onchange` 事件就被移除元素而导致的问题
*
* 更多例子参考代码中 {@linkcode GridInputColumn} 的实现
* @member
* @name GridColumn.editing
* @readonly
* @type {boolean}
*/
/**
* 创建显示单元格时调用的方法
* @param {GridColumnDefinition} col - 列定义对象
@@ -27,6 +38,8 @@ export class GridColumn {
*
* 元素修改后设置行包装对象的 `__editing` 后,支持在离开编辑状态时及时触发 [leaveEdit]{@linkcode GridColumn.leaveEdit} 方法<br/>
* 更多例子参考代码中 {@linkcode GridDropdownColumn} 的实现。
* @method
* @name GridColumn.createEdit
* @param {Function} trigger - 编辑事件回调函数,`e` 参数会传递给 [getValue]{@linkcode GridColumn.getValue} 方法
* @param {GridColumnDefinition} col - 列定义对象
* @param {HTMLElement} container - 父容器元素
@@ -34,15 +47,25 @@ export class GridColumn {
* @returns {HTMLElement} 返回创建的编辑状态的单元格元素
* @virtual
*/
static createEdit() { }
/**
* 创建列头时调用的方法
* @method
* @name GridColumn.createCaption
* @param {GridColumnDefinition} col - 列定义对象
* @returns {HTMLElement} 返回创建的列头元素
* @virtual
*/
static createCaption() { }
/**
* 获取编辑状态单元格值时调用的方法
* @method
* @name GridColumn.getValue
* @param {any} `e` 由 [createEdit]{@linkcode GridColumn.createEdit} 方法中 `trigger` 函数传递来的对象
* @param {GridColumnDefinition} col - 列定义对象
* @returns {(string | boolean | number)} 返回单元格的值
* @virtual
*/
/**
* 设置单元格值时调用的方法
@@ -57,15 +80,6 @@ export class GridColumn {
element.innerText = val;
}
/**
* 获取编辑状态单元格值时调用的方法
* @param {any} `e` 由 [createEdit]{@linkcode GridColumn.createEdit} 方法中 `trigger` 函数传递来的对象
* @param {GridColumnDefinition} col - 列定义对象
* @returns {(string | boolean | number)} 返回单元格的值
* @virtual
*/
static getValue() { }
/**
* 设置单元格样式时调用的方法
* @param {HTMLElement} element - 单元格元素
@@ -94,27 +108,22 @@ export class GridColumn {
/**
* 单元格离开编辑元素时触发,需要由行包装对象的 `__editing` 来确定是否触发。
* @method
* @name GridColumn.leaveEdit
* @param {HTMLElement} element - 单元格元素
* @param {HTMLElement} container - 父容器元素
* @virtual
*/
static leaveEdit() { }
static toString() { return '[object Column]' }
}
/**
* 单行文本列
* 单行文本输入列
* @class
* @extends GridColumn
*/
export class GridInputColumn extends GridColumn {
/**
* 设置该类型是否支持触发 [onInputEnded]{@linkcode GridColumnDefinition.onInputEnded} 方法<br/>
* 该属性返回 `true` 后,在任意事件中修改行包装对象的 `__editing` 值,则会在行列元素变动时及时触发 [onInputEnded]{@linkcode GridColumnDefinition.onInputEnded} 方法,避免例如文本框还未触发 `onchange` 事件就被移除元素而导致的问题<br/>
* 更多例子参考代码中 {@linkcode GridInputColumn} 的实现
* @readonly
* @type {boolean}
*/
static get editing() { return true };
static createEdit(trigger, col, _wrapper, vals) {
@@ -185,6 +194,11 @@ export class GridTextColumn extends GridInputColumn {
const SymbolDropdown = Symbol.for('ui-dropdown');
/**
* 下拉选择列
* @class
* @extends GridColumn
*/
export class GridDropdownColumn extends GridColumn {
static createEdit(trigger, col, container, it) {
const drop = new Dropdown({
+520 -89
View File
@@ -15,12 +15,13 @@ const ColumnChangedType = {
Resize: 'resize',
Sort: 'sort'
};
const RefreshInterval = isMobile() ? 32 : 0;
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) {
@@ -58,9 +59,9 @@ let r = lang;
* 行数据包装接口
* @typedef GridItemWrapper
* @property {object} values - 真实数据对象
* @property {(GridItem | any)} values.[key]] 数据属性
* @property {(GridItem | any)} values.{key} 数据属性
* @property {object} source - 下拉数据源缓存对象
* @property {GridSourceItem[]} source.[key]] 数据源
* @property {GridSourceItem[]} source.{key} 数据源
*/
/**
@@ -84,6 +85,7 @@ let r = lang;
* @param {GridItem} item - 行数据对象
* @param {boolean} editing - 是否处于编辑状态
* @param {HTMLElement} [body] - Grid 控件的 `<tbody>` 部分
* @returns {any} - 返回过滤后的显示或编辑值
* @this GridColumnDefinition
*/
@@ -175,11 +177,46 @@ let r = lang;
* @this GridColumnDefinition
*/
/**
* 下拉列表参数对象
* @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} 返回格式化后的结果
*/
/**
* 列定义接口
* @typedef {object} GridColumnDefinition
* @property {string} key - 列关键字,默认以该关键字从行数据中提取单元格值,行数据的关键字属性值里包含 DisplayValue 则优先显示此值
* @property {(GridColumnTypeEnum | GridColumn)} [type=Grid.ColumnTypes.Common] - 列的类型,可以为 {@linkcode GridColumn} 的子类,或者内置类型 {@linkcode GridColumnTypeEnum}
* @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 则设置为该宽度,否则根据列内容自动调整列宽
@@ -188,8 +225,16 @@ let r = lang;
*
* * `boolean` 则直接使用该值
* * `string` 则以该值为关键字从行数据中取值作为判断条件
* * `(item: GridItem) => boolean` 则调用该函数(上下文为列定义对象),以返回值作为判断条件
* * `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 控件的 `<tbody>` 部分
* @property {any} filter.{returns} - 返回过滤后的显示或编辑值
* @property {string} [text] - 单元格以该值填充内容,忽略filter与关键字属性
* @property {boolean} [visible=true] - 列是否可见
* @property {boolean} [resizable=true] - 列是否允许调整宽度
@@ -198,30 +243,72 @@ let r = lang;
* @property {boolean} [allcheck=false] - 列为复选框类型时是否在列头增加全选复选框
* @property {object} [css] - 单元格css样式对象(仅在重建行元素时读取)
* @property {GridItemObjectCallback} [styleFilter] - 根据返回值填充单元格样式(填充行列数据时读取)
* @property {GridColumnDefinition} styleFilter.{this} - 上下文为列定义对象
* @property {GridItem} styleFilter.item - 行数据对象
* @property {object} styleFilter.{returns} - 返回样式对象
* @property {GridItemStringCallback} [bgFilter] - 根据返回值设置单元格背景色
* @property {GridColumnDefinition} bgFilter.{this} - 上下文为列定义对象
* @property {GridItem} bgFilter.item - 行数据对象
* @property {string} bgFilter.{returns} - 返回单元格背景色字符串
* @property {object} [events] - 给单元格元素附加事件(事件函数上下文为数据行对象)
* @property {Function} events.[event]] - 事件回调函数
* @property {(object | GridItemObjectCallback)} [attrs] - 根据返回值设置单元格元素的附加属性,允许直接设置对象也支持函数返回对象
* @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] - 自定义列过滤器的数据源(函数上下文为Grid)
* @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[] | GridDropdownSourceCallback | Promise<GridSourceItem[]>)} [source] - 列为下拉列表类型时以该值设置下拉列表数据源,支持函数返回,也支持返回异步对象
* @property {(GridSourceItem[] | Promise<GridSourceItem[]> | 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 | GridItemStringCallback)} [iconClassName] - 列为图标类型时以该值作为单元格元素的额外样式类型(函数上下文为列定义对象)
* @property {("fa-light" | "fa-regular" | "fa-solid")} [iconType=fa-light] - 列为图标类型时以该值设置图标样式
* @property {(string | GridItemStringCallback)} [iconClassName] - 列为图标类型时以该值作为单元格元素的额外样式类型,支持直接使用字符串或者调用如下函数
* @property {GridColumnDefinition} iconClassName.{this} - 上下文为列定义对象
* @property {GridItem} iconClassName.item - 行数据对象
* @property {string} iconClassName.{returns} - 返回额外样式类型字符串
* @property {string} [dateMin] - 列为日期类型时以该值作为最小可选日期值
* @property {string} [dateMax] - 列为日期类型时以该值作为最大可选日期值
* @property {DateFormatterCallback} [dateValueFormatter] - 列为日期类型时自定义日期格式化函数
* @property {(string | GridItemStringCallback)} [tooltip] - 以返回值额外设置单元格的tooltip(函数上下文为列定义对象)
* @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 字符串
* @property {GridColumnCheckedCallback} [onAllChecked] - 列头复选框改变时触发
* @property {Grid} onAllChecked.{this} - 上下文为 Grid
* @property {GridColumnDefinition} onAllChecked.col - 列定义对象
* @property {boolean} onAllChecked.flag - 是否选中
* @property {GridCellChangedCallback} [onChanged] - 单元格发生变化时触发
* @property {Grid} onChanged.{this} - 上下文为 Grid
* @property {GridItem} onChanged.item - 行数据对象
* @property {(boolean | string | number)} onChanged.value - 修改后的值
* @property {(boolean | string | number)} onChanged.oldValue - 修改前的值
* @property {any} [onChanged.e] - 列修改事件传递过来的任意对象
* @property {GridCellInputEndedCallback} [onInputEnded] - 文本单元格在输入完成时触发的事件
* @property {Grid} onInputEnded.{this} - 上下文为 Grid
* @property {GridColumnDefinition} onInputEnded.col - 列定义对象
* @property {string} onInputEnded.value - 修改后的文本框值
* @property {GridColumnFilterOkCallback} [onFilterOk] - 列过滤点击 `OK` 时触发的事件
* @property {Grid} onFilterOk.{this} - 上下文为 Grid
* @property {GridColumnDefinition} onFilterOk.col - 列定义对象
* @property {GridItem[]} onFilterOk.selected - 选中的过滤项
* @property {GridColumnFilteredCallback} [onFiltered] - 列过滤后触发的事件
* @property {Grid} onFiltered.{this} - 上下文为 Grid
* @property {GridColumnDefinition} onFiltered.col - 列定义对象
* @property {GridColumnDropExpandedCallback} [onDropExpanded] - 列为下拉框类型时在下拉列表展开时触发的事件
* @property {GridColumnDefinition} onDropExpanded.{this} - 上下文为列定义对象
* @property {GridItem} onDropExpanded.item - 行数据对象
* @property {Dropdown} onDropExpanded.drop - 拉框对象
*/
/**
@@ -275,6 +362,20 @@ const GridColumnDirection = {
* @property {("asc" | "desc")} order - 升序或降序
*/
/**
* 扩展行对象接口
* @typedef GridExpandableObject
* @property {HTMLElement} element - 扩展行元素
*/
/**
* 扩展行生成回调函数
* @callback GridExpandableObjectCallback
* @param {GridItem} item - 行数据对象
* @returns {GridExpandableObject} 返回扩展行对象
* @this Grid
*/
/**
* Grid 控件基础类
*
@@ -324,30 +425,168 @@ const GridColumnDirection = {
*/
export class Grid {
_var = {
/**
* 父容器元素
* @type {HTMLElement}
*/
parent: null,
/**
* Grid 元素 - `div.ui-grid`
* @type {HTMLDivElement}
*/
el: null,
/**
* 全部数据数组
* @type {GridItemWrapper[]}
*/
source: null,
/**
* 当前已过滤显示的数据数组
* @type {GridItemWrapper[]}
*/
currentSource: null,
/**
* 当前选中的列索引
* @type {number}
*/
selectedColumnIndex: -1,
/**
* 当前选中的行索引数组
* @type {number[]}
*/
selectedIndexes: null,
/**
* 虚模式头部索引
* @type {number}
*/
startIndex: 0,
/**
* 旧选择索引数组
* @type {number[]}
*/
oldSelectedIndexes: null,
/**
* 旧虚模式头部索引
*/
oldIndex: null,
/**
* 当前滚动上边距
* @type {number}
*/
scrollTop: 0,
/**
* 当前滚动左边距
* @type {number}
*/
scrollLeft: 0,
/**
* 一页高度可显示的行数
* @type {number}
*/
rowCount: -1,
/**
* 列类型缓存字典
* @property {GridColumn} {key} - 关键字对应列的类型缓存对象
*/
colTypes: {},
colAttrs: {}
/**
* 列属性字典
* @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}
*/
__filtered: false,
/**
* 过滤面板已打开
* @type {boolean}
*/
__filtering: false,
/**
* 上一个目标排序列索引
* @type {number}
*/
__orderIndex: -1,
},
/**
* 是否处于渲染中
* @type {boolean}
*/
rendering: false,
/**
* 正文高度
* @type {number}
*/
containerHeight: null,
/**
* 正文宽度
* @type {number}
*/
bodyClientWidth: null,
/**
* 是否需要 resize
* @type {boolean}
*/
needResize: null,
/**
* 页面元素引用
*/
refs: {
/**
* 表格引用 - table.ui-grid-table
* @type {HTMLTableElement}
*/
table: null,
/**
* 表格正文引用 - tbody
* @type {HTMLTableSectionElement}
*/
body: null,
/**
* 表格头部引用 - thead>tr
* @type {HTMLTableSectionElement}
*/
header: null,
/**
* 加载状态元素引用 - div.ui-grid-loading
* @type {HTMLDivElement}
*/
loading: null,
/**
* 大小计算元素引用 - span.ui-grid-sizer
* @type {HTMLSpanElement}
*/
sizer: null,
/**
* 包装元素引用 - div.ui-grid-wrapper
* @type {HTMLDivElement}
*/
wrapper: null,
/**
* 拖拽块引用 - div.dragger
* @type {HTMLDivElement}
*/
dragger: null,
/**
* 拖拽光标引用 - layer.dragger-cursor
* @type {HTMLElement}
*/
draggerCursor: null,
}
};
// _var.source;
// _var.currentSource;
// _var.parent;
// _var.el;
// _var.refs;
// _var.rendering;
// _var.selectedColumnIndex = -1;
// _var.selectedIndexes;
// _var.startIndex = 0;
// _var.needResize;
// _var.containerHeight;
// _var.bodyClientWidth;
// _var.rowCount = -1;
// _var.scrollTop;
// _var.scrollLeft;
// _var.colTypes = {};
// _var.colAttrs = {};
// _var.vtable = [];
/**
* 列定义的数组
@@ -460,8 +699,26 @@ export class Grid {
* 排序列数组
* @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;
/**
* 即将选中行时触发
@@ -469,6 +726,7 @@ export class Grid {
* @param {number} index - 即将选中的行索引
* @param {number} colIndex - 即将选中的列索引
* @returns {boolean} 返回 `false`、`null`、`undefined`、`0` 等则取消选中动作
* @this Grid
*/
willSelect;
/**
@@ -477,6 +735,7 @@ export class Grid {
* @param {number} index - 点击的行索引
* @param {number} colIndex - 点击的列索引
* @returns {boolean} 返回 false 则取消事件冒泡
* @this Grid
*/
cellClicked;
@@ -484,6 +743,7 @@ export class Grid {
* 选中行发生变化时触发的事件
* @event
* @param {number} index - 选中的行索引
* @this Grid
*/
onSelectedRowChanged;
/**
@@ -491,12 +751,14 @@ export class Grid {
* @event
* @param {number} index - 双击的行索引
* @param {number} colIndex - 双击的列索引
* @this Grid
*/
onCellDblClicked;
/**
* 行双击时触发的事件
* @event
* @param {number} index - 双击的行索引
* @this Grid
*/
onRowDblClicked;
/**
@@ -509,31 +771,56 @@ export class Grid {
* * "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 }
/**
*
* @param {(string | HTMLElement)?} container Grid 控件所在的父容器,可以是 string 表示选择器,也可以是 HTMLElement 对象
* Grid 控件构造函数<br/>
* _**构造时可以不进行赋值,但是调用 init 函数时则必须进行赋值**_
* 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]] 返回的多语言
* @param {string} getText.{returns} 返回的多语言
*/
constructor(container, getText) {
this._var.parent = typeof container === 'string' ? document.querySelector(container) : container;
@@ -589,6 +876,8 @@ export class Grid {
/**
* 获取已过滤的数据数组,或者设置数据并刷新列表
* @type {GridItem[]}
* @property {any} Value - 值
* @property {string} DisplayValue - 显示值
*/
get source() { return this._var.currentSource?.map(s => s.values) }
set source(list) {
@@ -608,6 +897,13 @@ export class Grid {
this._refreshSource(list);
}
/**
* 获取已过滤的数据中的扩展对象数组
* @type {GridExpandableObject[]}
* @property {HTMLElement} element - 扩展行元素
*/
get sourceExpandable() { return this._var.currentSource?.map(s => s.__expandable_object) }
/**
* 设置单行数据
* @param {number} index - 行索引
@@ -840,9 +1136,13 @@ export class Grid {
return this.columns[this.sortIndex]?.key;
}
get tableRows() {
get _tableRows() {
// return [...this._var.refs.body.children];
return Array.prototype.slice.call(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'));
}
/**
@@ -856,7 +1156,7 @@ export class Grid {
if (this.readonly !== true) {
this.refresh();
} else {
this.tableRows.forEach((row, i) => {
this._tableRows.forEach((row, i) => {
if (indexes.includes(startIndex + i)) {
row.classList.add('selected');
} else if (row.classList.contains('selected')) {
@@ -955,6 +1255,38 @@ export class Grid {
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);
@@ -969,7 +1301,7 @@ export class Grid {
const table = createElement('table', 'ui-grid-table');
this._var.refs.table = table;
this._createHeader(table);
const body = this._createBody(table);
this._createBody(table);
wrapper.appendChild(table);
// tooltip
if (!this.tooltipDisabled) {
@@ -985,7 +1317,7 @@ export class Grid {
});
holder.addEventListener('dblclick', e => this._onRowDblClicked(e));
wrapper.appendChild(holder);
body.addEventListener('mousemove', e => throttle(this._onBodyMouseMove, HoverInternal, this, e, holder), { passive: true });
wrapper.addEventListener('mousemove', e => throttle(this._onGridMouseMove, HoverInternal, this, e, holder), { passive: true });
}
// loading
@@ -1057,7 +1389,7 @@ export class Grid {
const filtered = this.columns.some(c => c.filterValues != null);
if ((filtered ^ this._var.colAttrs.__filtered) === 1) {
this._var.colAttrs.__filtered = filtered;
const headers = this._var.refs.header.children;
const headers = this._headerCells;
for (let i = 0; i < this.columns.length; ++i) {
const ele = headers[i].querySelector('.filter');
if (ele == null) {
@@ -1076,7 +1408,7 @@ export class Grid {
if (this.extraRows > 0) {
length += this.extraRows;
}
this._var.containerHeight = length * (this.rowHeight + 1);
this._var.containerHeight = (length + 1) * (this.rowHeight + 1);
if (!keep) {
this._var.el.scrollTop = 0;
// this._var.el.scrollLeft = 0;
@@ -1095,7 +1427,7 @@ export class Grid {
throw new Error('body has not been created.');
}
const widths = {};
this._fillRows(this.tableRows, this.columns, widths);
this._fillRows(this._tableRows, this.columns, widths);
if (this._var.needResize && widths.flag) {
this._var.needResize = false;
this.columns.forEach((col, i) => {
@@ -1161,7 +1493,7 @@ export class Grid {
}
this.sortArray = null;
const direction = this.sortDirection;
[...this._var.refs.header.children].forEach((th, i) => {
[...this._headerCells].forEach((th, i) => {
const arrow = th.querySelector('.arrow');
if (arrow == null) {
return;
@@ -1223,7 +1555,7 @@ export class Grid {
this.refresh();
}
// arrow icon
[...this._var.refs.header.children].forEach((th, i) => {
[...this._headerCells].forEach((th, i) => {
const arrow = th.querySelector('.arrow');
if (arrow == null) {
return;
@@ -1263,7 +1595,7 @@ export class Grid {
grid.onSelectedRowChanged = rowChanged;
const reload = index => {
grid.selectedIndexes = [index];
grid.scrollTop = index * grid.rowHeight;
grid.scrollTop = index * (grid.rowHeight + 1);
rowChanged(index);
}
buttonWrapper.append(
@@ -1457,7 +1789,19 @@ export class Grid {
const header = createElement('tr');
thead.appendChild(header);
const sizer = this._var.refs.sizer;
let left = 0;
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');
@@ -1600,42 +1944,47 @@ export class Grid {
width += col.width + 1;
}
}
if (width > 0) {
table.style.width = `${width}px`;
if (this.expandable) {
width += ExpandableWidth;
}
table.style.width = `${width}px`;
// body content
body.addEventListener('mousedown', e => {
let [parent, target] = this._getRowTarget(e.target);
const rowIndex = indexOfParent(parent);
let colIndex = indexOfParent(target);
if (colIndex >= this.columns.length) {
colIndex = -1;
}
this._onRowClicked(e, rowIndex, colIndex);
});
body.addEventListener('dblclick', e => this._onRowDblClicked(e));
// this._adjustRows();
// this._adjustRows(body);
this._var.refs.body = body;
// this.refresh();
return body;
}
_adjustRows() {
_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 content = this._var.refs.body;
const exists = content.children.length;
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 = 0;
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');
const cell = createElement('td', 'ui-grid-cell');
if (col.visible !== false) {
let style = this._get(col.key, 'style') ?? {};
if (col.isfixed) {
@@ -1677,9 +2026,11 @@ export class Grid {
content.appendChild(row);
}
} else if (count < 0) {
for (let i = -1; i >= count; i -= 1) {
// content.removeChild(content.children[exists + i]);
content.children[exists + i].remove();
let last = content.querySelectorAll('&>.ui-grid-row')[exists + count];
while (last != null) {
const next = last.nextElementSibling;
last.remove();
last = next;
}
}
}
@@ -1698,6 +2049,7 @@ export class Grid {
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) {
@@ -1714,11 +2066,59 @@ export class Grid {
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];
const cell = row.children[j + offset];
if (cell == null) {
return;
}
@@ -1833,12 +2233,13 @@ export class Grid {
style.width = w;
style['max-width'] = w;
style['min-width'] = w;
const headerCells = this._var.refs.header.children;
const headerCells = this._headerCells;
let element = headerCells[index];
element.style.width = w;
element.style.maxWidth = w;
element.style.minWidth = w;
let left = 0;
// 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;
@@ -1851,15 +2252,17 @@ export class Grid {
}
}
}
for (let row of this.tableRows) {
element = row.children[index];
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 + 1; i < this.columns.length; ++i) {
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;
@@ -1873,7 +2276,7 @@ export class Grid {
}
_changingColumnOrder(index, offset, mouse, draggerCellLeft) {
const children = this._var.refs.header.children;
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);
@@ -1945,9 +2348,10 @@ export class Grid {
return;
}
const header = this._var.refs.header;
const children = header.children;
const rows = this.tableRows;
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];
@@ -1959,7 +2363,7 @@ export class Grid {
columns.splice(targetIndex, 0, current);
header.insertBefore(children[index], children[targetIndex].nextElementSibling);
for (let row of rows) {
row.insertBefore(row.children[index], row.children[targetIndex].nextElementSibling);
row.insertBefore(row.children[index + offset], row.children[targetIndex + offset].nextElementSibling);
}
} else {
targetIndex = orderIndex;
@@ -1972,7 +2376,7 @@ export class Grid {
columns.splice(targetIndex, 0, current);
header.insertBefore(children[index], children[targetIndex]);
for (let row of rows) {
row.insertBefore(row.children[index], row.children[targetIndex]);
row.insertBefore(row.children[index + offset], row.children[targetIndex + offset]);
}
}
if (this.sortArray == null || this.sortArray.length === 0) {
@@ -2066,6 +2470,13 @@ export class Grid {
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);
}
@@ -2243,7 +2654,11 @@ export class Grid {
if (typeof col.onFilterOk === 'function') {
col.onFilterOk.call(this, col, array);
} else {
col.filterValues = array.map(a => a.DisplayValue);
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();
@@ -2287,8 +2702,9 @@ export class Grid {
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, 'DisplayValue') ? item.DisplayValue : item;
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) {
@@ -2350,7 +2766,7 @@ export class Grid {
if (e.currentTarget.classList.contains('sticky')) {
return;
}
const index = indexOfParent(e.currentTarget);
const index = indexOfParent(e.currentTarget) - (this.expandable ? 1 : 0);
const cx = getClientX(e);
const clearEvents = attr => {
for (let event of ['mousemove', 'mouseup']) {
@@ -2411,7 +2827,7 @@ export class Grid {
_onResizeStart(e, col) {
const cx = getClientX(e);
const width = col.width;
const index = indexOfParent(e.currentTarget.parentElement);
const index = indexOfParent(e.currentTarget.parentElement) - (this.expandable ? 1 : 0);
const window = this.window ?? global;
const clearEvents = attr => {
for (let event of ['mousemove', 'mouseup']) {
@@ -2462,9 +2878,10 @@ export class Grid {
_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].children[0];
for (let row of this._tableRows) {
const element = row.children[index + offset].children[0];
const w = element.scrollWidth;
if (w > width) {
width = w;
@@ -2475,9 +2892,9 @@ export class Grid {
}
if (width > 0 && width !== col.width) {
width += 12;
this._changeColumnWidth(index, width);
this._changeColumnWidth(index - offset, width);
if (typeof this.onColumnChanged === 'function') {
this.onColumnChanged(ColumnChangedType.Resize, index, width);
this.onColumnChanged(ColumnChangedType.Resize, index - offset, width);
}
}
}
@@ -2524,7 +2941,8 @@ export class Grid {
this._scrollToTop(top);
}
_onBodyMouseMove(e, holder) {
_onGridMouseMove(e, holder) {
e.stopPropagation();
if (e.target.classList.contains('ui-grid-hover-holder')) {
return;
}
@@ -2537,6 +2955,10 @@ export class Grid {
}
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')) {
@@ -2621,7 +3043,7 @@ export class Grid {
if (this.readonly !== true) {
this.refresh();
} else {
this.tableRows.forEach((row, i) => {
this._tableRows.forEach((row, i) => {
if (selectedIndexes.includes(startIndex + i)) {
row.classList.add('selected');
} else if (row.classList.contains('selected')) {
@@ -2705,4 +3127,13 @@ export class Grid {
}
}
}
_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();
}
}