This commit is contained in:
2025-12-24 10:55:40 +08:00
parent eec9d6045c
commit 752bb23571
25 changed files with 2348 additions and 816 deletions

391
lib/ui/grid/column.d.ts vendored Normal file
View File

@@ -0,0 +1,391 @@
import { Grid, GridItem, GridItemWrapper, GridSourceItem } from "./grid";
import { Dropdown, DropdownOptions } from "../dropdown";
/** 列类型枚举 */
declare enum GridColumnType {
/** 通用列 */
Common = 0,
/** 单行文本框列 */
Input = 1,
/** 下拉选择列 */
Dropdown = 2,
/** 复选框列 */
Checkbox = 3,
/** 图标列 */
Icon = 4,
/** 多行文本列 */
Text = 5,
/** 日期选择列 */
Date = 6
}
/** 列定义接口 */
export interface GridColumnDefinition {
/** 列关键字,默认以该关键字从行数据中提取单元格值,行数据的关键字属性值里包含 DisplayValue 则优先显示此值 */
key?: string;
/** 列的类型,可以为 {@linkcode GridColumn} 的子类,或者内置类型 {@linkcode GridColumnType} */
type?: GridColumnType | typeof GridColumn;
/** 列标题文本 */
caption?: string;
/** 列标题的元素样式 */
captionStyle?: { [key: string]: string };
/** 大于 0 则设置为该宽度,否则根据列内容自动调整列宽 */
width?: number;
/** 列对齐方式 */
align?: "left" | "center" | "right";
/**
* 列是否可用(可编辑),允许以下类型<br/><br/>
* `boolean` 则直接使用该值<br/><br/>
* `string` 则以该值为关键字从行数据中取值作为判断条件<br/><br/>
* `(item: GridItem) => boolean` 则调用该函数(上下文为列定义对象),以返回值作为判断条件<br/><br/>
*/
enabled?: boolean | string | ((item: GridItem) => boolean);
/**
* 单元格取值采用该方法返回的值
* @param item 行数据对象
* @param editing 是否处于编辑状态
* @param body Grid 控件的 `&lt;tbody&gt;` 部分
*/
filter?: (item: GridItem, editing: boolean, body?: HTMLElement) => any;
/** 单元格以该值填充内容忽略filter与关键字属性 */
text?: string;
/** 列是否可见 */
visible?: boolean;
/** 列是否允许调整宽度 */
resizable?: boolean;
/** 列是否允许排序 */
sortable?: boolean;
/** 列是否允许重排顺序 */
orderable?: boolean;
/** 列为复选框类型时是否在列头增加全选复选框 */
allcheck?: boolean;
/** 单元格css样式对象仅在重建行元素时读取 */
css?: { [key: string]: string };
/** 根据返回值填充单元格样式(填充行列数据时读取) */
styleFilter?: (item: GridItem) => { [key: string]: string };
/** 根据返回值设置单元格背景色 */
bgFilter?: (item: GridItem) => string;
/** 给单元格元素附加事件(事件函数上下文为数据行对象) */
events?: { [event: string]: Function };
/** 根据返回值设置单元格元素的附加属性,允许直接设置对象也支持函数返回对象 */
attrs?: { [key: string]: string } | ((item: GridItem) => { [key: string]: string });
/** 是否允许进行列头过滤 */
allowFilter?: boolean;
/** 自定义列过滤器的数据源函数上下文为Grid */
filterSource?: Array<GridItem> | ((col: GridColumnDefinition) => Array<GridItem>);
/** 自定义列排序函数 */
sortFilter?: (a: GridItem, b: GridItem) => -1 | 0 | 1;
/** 列为下拉列表类型时以该值设置下拉框的参数 */
dropOptions?: DropdownOptions;
/** 列为下拉列表类型时以该值设置下拉列表数据源,支持函数返回,也支持返回异步对象 */
source?: Array<GridSourceItem> | ((item: GridItem) => Array<GridSourceItem> | Promise<Array<GridSourceItem>>);
/** 下拉列表数据源是否缓存结果即行数据未发生变化时仅从source属性获取一次值 */
sourceCache?: boolean;
/** 列为图标类型时以该值设置图标样式(函数上下文为列定义对象),默认值 `fa-light` */
iconType?: "fa-light" | "fa-regular" | "fa-solid";
/** 列为图标类型时以该值作为单元格元素的额外样式类型(函数上下文为列定义对象) */
iconClassName?: string | ((item: GridItem) => string);
/** 列为日期类型时以该值作为最小可选日期值 */
dateMin?: string;
/** 列为日期类型时以该值作为最大可选日期值 */
dateMax?: string;
/** 列为日期类型时自定义日期转字符串函数 */
dateValueFormatter?: string | ((date: Date) => string);
/** 以返回值额外设置单元格的tooltip函数上下文为列定义对象 */
tooltip?: string | ((item: GridItem) => string);
/**
* 列头复选框改变时触发
* @param this 上下文为 Grid 对象
* @param col 列定义对象
* @param flag 是否选中
* @eventProperty
*/
onAllChecked?: (this: Grid, col: GridColumnDefinition, flag: boolean) => void;
/**
* 单元格发生变化时触发
* @param this 上下文为 Grid 对象
* @param item 数据行对象
* @param value 修改后的值
* @param oldValue 修改前的值
* @param e 列修改事件传递过来的任意对象
* @eventProperty
*/
onChanged?: (this: Grid, item: GridItem, value: boolean | string | number, oldValue: boolean | string | number, e?: any) => void;
/**
* 文本单元格在输入完成时触发的事件
* @param this 上下文为 Grid 对象
* @param item 数据行对象
* @param value 修改后的文本框值
* @eventProperty
*/
onInputEnded?: (this: Grid, item: GridItem, value: string) => void;
/**
* 列过滤点击OK时触发的事件
* @param this 上下文为 Grid 对象
* @param col 列定义对象
* @param selected 选中的过滤项
* @eventProperty
*/
onFilterOk?: (this: Grid, col: GridColumnDefinition, selected: Array<GridItem>) => void;
/**
* 列过滤后触发的事件
* @param this 上下文为 Grid 对象
* @param col 列定义对象
* @eventProperty
*/
onFiltered?: (this: Grid, col: GridColumnDefinition) => void;
/**
* 列为下拉框类型时在下拉列表展开时触发的事件
* @param this 上下文为列定义对象
* @param item 数据行对象
* @param drop 下拉框对象
* @eventProperty
*/
onDropExpanded?: (this: GridColumnDefinition, item: GridItem, drop: Dropdown) => void;
/**
* 列为下拉框类型时在下拉列表关闭时触发的事件
* @param this 上下文为列定义对象
* @param item 数据行对象
* @param drop 下拉框对象
* @eventProperty
*/
onDropCollapsed?: (this: GridColumnDefinition, item: GridItem, drop: Dropdown) => void;
}
/** 列定义基类 */
export class GridColumn {
/** @ignore */
constructor();
/**
* 标记该类型是否支持列头批量操作
*/
static get headerEditing(): boolean;
/**
* 创建显示单元格时调用的方法
* @param col 列定义对象
* @returns 返回创建的单元格元素
* @virtual
*/
static create(col: GridColumnDefinition): HTMLElement;
/**
* 创建编辑单元格时调用的方法<br/><br/>
* 元素修改后设置行包装对象的 `__editing` 后,支持在离开编辑状态时及时触发 {@linkcode leaveEdit} 方法<br/>
* 更多例子参考代码中 {@linkcode GridDropdownColumn} 的实现。
* @param trigger 编辑事件回调函数e 参数会传递给 {@linkcode getValue} 方法
* @param col 列定义对象
* @param container 父容器元素
* @param vals 行包装对象,其 `values` 属性为行数据对象
* @returns 返回创建的编辑状态的单元格元素
* @virtual
*/
static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement;
/**
* 创建列头时调用的方法
* @param col 列定义对象
* @returns 返回创建的列头元素
* @virtual
*/
static createCaption?(col: GridColumnDefinition): HTMLElement;
/**
* 设置单元格值时调用的方法
* @param element 单元格元素
* @param val 待设置的单元格值
* @param vals 行包装对象
* @param col 列定义对象
* @param grid {@linkcode Grid} 对象
* @virtual
*/
static setValue(element: HTMLElement, val: string | boolean | number, vals: GridItemWrapper, col: GridColumnDefinition, grid: Grid): void;
/**
* 获取编辑状态单元格值时调用的方法
* @param e 由 {@linkcode createEdit} 方法中 `trigger` 函数传递来的对象
* @param col 列定义对象
* @returns 返回单元格的值
* @virtual
*/
static getValue(e: any, col: GridColumnDefinition): string | boolean | number;
/**
* 设置单元格样式时调用的方法
* @param element 单元格元素
* @param style 样式对象
* @virtual
*/
static setStyle(element: HTMLElement, style: { [key: string]: string }): void;
/**
* 设置单元格可用性时调用的方法
* @param element 单元格元素
* @param enabled 启用值为false时代表禁用
* @virtual
*/
static setEnabled(element: HTMLElement, enabled?: boolean): void;
/**
* 单元格离开编辑元素时触发,需要由行包装对象的 `__editing` 来确定是否触发。
* @param element 单元格元素
* @param container 父容器元素
* @virtual
*/
static leaveEdit?(element: HTMLElement, container: HTMLElement): void;
}
/** 单行文本列 */
export class GridInputColumn extends GridColumn {
/**
* 设置该类型是否支持触发 {@linkcode GridColumnDefinition.onInputEnded} 方法<br/>
* 该属性返回 `true` 后,在任意事件中修改行包装对象的 `__editing` 值,则会在行列元素变动时及时触发 {@linkcode GridColumnDefinition.onInputEnded} 方法,避免例如文本框还未触发 `onchange` 事件就被移除元素而导致的问题<br/>
* 更多例子参考代码中 {@linkcode GridInputColumn} 的实现
*/
static get editing(): boolean;
/**
* @inheritdoc GridColumn.createEdit
* @override
*/
static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement;
/**
* @inheritdoc GridColumn.setValue
* @override
*/
static setValue(element: HTMLElement, val: string, vals: GridItemWrapper, col: GridColumnDefinition, grid: Grid): void;
/**
* @inheritdoc GridColumn.getValue
* @override
*/
static getValue(e: any): string;
/**
* @inheritdoc GridColumn.setEnabled
* @override
*/
static setEnabled(element: HTMLElement, enabled?: boolean): void;
}
/** 多行文本列 */
export class GridTextColumn extends GridInputColumn {
/**
* @inheritdoc GridInputColumn.createEdit
* @override
*/
static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement;
/**
* @inheritdoc GridInputColumn.setValue
* @override
*/
static setValue(element: HTMLElement, val: string, vals: GridItemWrapper, col: GridColumnDefinition, grid: Grid): void;
}
/** 下拉选择列 */
export class GridDropdownColumn extends GridColumn {
/**
* @inheritdoc GridColumn.createEdit
* @override
*/
static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement;
/**
* @inheritdoc GridColumn.setValue
* @override
*/
static setValue(element: HTMLElement, val: string, vals: GridItemWrapper, col: GridColumnDefinition): void;
/**
* @inheritdoc GridColumn.getValue
* @override
*/
static getValue(e: any, col: GridColumnDefinition): string;
/**
* @inheritdoc GridColumn.setEnabled
* @override
*/
static setEnabled(element: HTMLElement, enabled?: boolean): void;
/**
* @inheritdoc GridColumn.leaveEdit
* @override
*/
static leaveEdit?(element: HTMLElement, container: HTMLElement): void;
}
/** 复选框列 */
export class GridCheckboxColumn extends GridColumn {
/**
* @inheritdoc GridColumn.createEdit
* @override
*/
static createEdit(trigger: (e: any) => void): HTMLElement;
/**
* @inheritdoc GridColumn.setValue
* @override
*/
static setValue(element: HTMLElement, val: boolean): void;
/**
* @inheritdoc GridColumn.getValue
* @override
*/
static getValue(e: any): boolean;
/**
* @inheritdoc GridColumn.setEnabled
* @override
*/
static setEnabled(element: HTMLElement, enabled?: boolean): void;
}
/** 单选框列 */
export class GridRadioboxColumn extends GridCheckboxColumn {
/**
* @inheritdoc GridCheckboxColumn.createEdit
* @override
*/
static createEdit(trigger: (e: any) => void): HTMLElement;
}
/** 图标列 */
export class GridIconColumn extends GridColumn {
/**
* @inheritdoc GridColumn.create
* @override
*/
static create(): HTMLElement;
/**
* @inheritdoc GridColumn.setValue
* @override
*/
static setValue(element: HTMLElement, val: string, vals: GridItemWrapper, col: GridColumnDefinition): void;
/**
* @inheritdoc GridColumn.setEnabled
* @override
*/
static setEnabled(element: HTMLElement, enabled?: boolean): void;
}
/** 日期选择列 */
export class GridDateColumn extends GridColumn {
/**
* @inheritdoc GridColumn.createEdit
* @override
*/
static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement;
/**
* 设置单元格值时调用的方法<br/><br/>
* 支持以下几种数据类型<br/><br/>
* `"2024-01-26"`<br/>
* `"1/26/2024"`<br/>
* `"638418240000000000"`<br/>
* `new Date('2024-01-26')`<br/>
* @param element 单元格元素
* @param val 待设置的单元格值
* @override
*/
static setValue(element: HTMLElement, val: string | number): void;
/**
* @inheritdoc GridColumn.getValue
* @override
*/
static getValue(e: any): string | number;
/**
* @inheritdoc GridColumn.setEnabled
* @override
*/
static setEnabled(element: HTMLElement, enabled?: boolean): void;
/**
* 格式化日期对象为 M/d/yyyy 格式的字符串
* @param date 日期对象
* @returns 返回格式化后的字符串
*/
static formatDate(date: Date): string;
}

View File

@@ -368,6 +368,11 @@ export class GridDropdownColumn extends GridColumn {
col.onDropExpanded.call(col, wrapper.values, drop);
}
};
drop.onCollapsed = () => {
if (typeof col.onDropCollapsed === 'function') {
col.onDropCollapsed.call(col, wrapper.values, drop);
}
};
return drop.create();
}

325
lib/ui/grid/grid.d.ts vendored Normal file
View File

@@ -0,0 +1,325 @@
import { GridColumnDefinition } from "./column"
/**
* 单元格点击回调函数
*
* @param {number} index - 点击的行索引
* @param {number} colIndex - 点击的列索引
* @returns {boolean} 返回 `false` 则取消事件冒泡
*/
declare function cellClickedCallback(index: number, colIndex: number): boolean;
/** 列数据接口 */
interface GridItem {
/** 值 */
Value: any;
/** 显示值 */
DisplayValue: string;
}
/** 列数据行包装接口 */
interface GridItemWrapper {
/** 真实数据对象 */
values: { [key: string]: GridItem | any };
/** 下拉数据源缓存对象 */
source: { [key: string]: Array<GridSourceItem> };
}
/** 下拉框列数据源接口 */
interface GridSourceItem {
/** 值 */
value: string;
/** 显示文本 */
text: string;
}
/** Grid 语言资源接口 */
interface GridLanguages {
/**
* “所有”文本,默认值 `( All )` */
all: string;
/** “确定”文本,默认值 `OK` */
ok: string;
/** “重置”文本,默认值 `Reset` */
reset: string;
cancel: string;
/** “空”文本,默认值 `( Null )` */
null: string;
addLevel: string;
deleteLevel: string;
copyLevel: string;
asc: string;
desc: string;
column: string;
order: string;
sort: string;
requirePrompt: string;
duplicatePrompt: string;
}
/** Grid 列排序定义接口 */
interface GridColumnSortDefinition {
/** 排序列的关键字 */
column: string;
/** 升序或降序 */
order: "asc" | "desc";
}
/** 列排序枚举 */
declare enum GridColumnDirection {
/** 倒序 */
Descending = -1,
/** 升序 */
Ascending = 1
}
/** 列事件枚举 */
declare enum GridColumnEvent {
/** 重排事件 */
Reorder = "reorder",
/** 宽调整事件 */
Resize = "resize",
/** 排序事件 */
Sort = "sort"
}
/** Grid 控件基础类 */
export class Grid {
/** 列类型枚举 */
static ColumnTypes: {
/** 通用列(只读) */
Common: 0,
/** 单行文本列 */
Input: 1,
/** 下拉选择列 */
Dropdown: 2,
/** 复选框列 */
Checkbox: 3,
/** 图标列 */
Icon: 4,
/** 多行文本列 */
Text: 5,
/** 日期选择列 */
Date: 6,
/**
* 判断列是否为复选框列
* @param type 列类型
*/
isCheckbox(type: number): boolean;
};
/** 列定义的数组 */
columns: Array<GridColumnDefinition>;
/** 多语言资源对象 */
langs?: GridLanguages;
/** 行数大于等于该值则启用虚模式,默认值 `100` */
virtualCount?: number;
/** 表格行高,默认值 `36` */
rowHeight?: number;
/** 文本行高,默认值 `24` */
lineHeight?: number;
/** 列表底部留出额外行的空白,默认值 `0` */
extraRows?: number;
/** 过滤条件列表的行高,默认值 `30` */
filterRowHeight?: number;
/** 列表高度值,为 0 时列表始终显示全部内容(自增高),为非数字或者小于 0 则根据容器高度来确定虚模式的渲染行数,默认值 `null` */
height?: number;
/** 是否允许多选,默认值 `false` */
multiSelect?: boolean;
/** 为 false 时只有点击在单元格内才会选中行,默认值 `true` */
fullrowClick?: boolean;
/** 单元格 tooltip 是否禁用,默认值 `false` */
tooltipDisabled?: boolean;
/** 列头是否显示,默认值 `true` */
headerVisible?: boolean;
/** 监听事件的窗口载体,默认值 `window` */
window?: Window
/** 排序列的索引,默认值 `-1` */
sortIndex?: number;
/** 排序方式,正数升序,负数倒序,默认值 `1` */
sortDirection?: GridColumnDirection;
/** 排序列 */
sortArray?: Array<GridColumnSortDefinition>;
/**
* Grid 控件构造函数
* @param container Grid 控件所在的父容器,可以是 string 表示选择器,也可以是 HTMLElement 对象<br/><br/>
* <i>构造时可以不进行赋值,但是调用 init 函数时则必须进行赋值</i>
* @param getText (可选参数)获取多语言文本的函数代理
*/
constructor(container: string | HTMLElement, getText?: (id: string, def?: string) => string);
/**
* 即将选中行时触发,返回 false、null、undefined、0 等则取消选中动作
* @param index 即将选中的行索引
* @param colIndex 即将选中的列索引
* @eventProperty
*/
willSelect?: (index: number, colIndex: number) => boolean;
/**
* 单元格单击时触发colIndex 为 -1 则表示点击的是行的空白处,返回 false 则取消事件冒泡
* @eventProperty
*/
cellClicked?: typeof cellClickedCallback;
/**
* 选中行发生变化时触发的事件
* @param index 选中的行索引
* @eventProperty
*/
onSelectedRowChanged?: (index?: number) => void;
/**
* 单元格双击时触发的事件colIndex 为 -1 则表示点击的是行的空白处
* @param index 双击的行索引
* @param colIndex 双击的列索引
* @eventProperty
*/
onCellDblClicked?: (index: number, colIndex: number) => void;
/**
* 行双击时触发的事件
* @param index 双击的行索引
* @eventProperty
*/
onRowDblClicked?: (index: number) => void;
/**
* 列发生变化时触发的事件
* @param type 事件类型<br/><br/>
* "reorder" 为发生列重排事件,此时 value 为目标列索引<br/>
* "resize" 为发生列宽调整事件,此时 value 为列宽度值<br/>
* "sort" 为发生列排序事件,此时 value 为 1升序或 -1倒序
* @param colIndex 发生变化事件的列索引
* @param value 变化的值
* @eventProperty
*/
onColumnChanged?: (type: GridColumnEvent, colIndex: number, value: number | GridColumnDirection) => void;
/**
* 列滚动时触发的事件
* @param e 滚动事件对象
* @eventProperty
*/
onBodyScrolled?: (e: Event) => void;
/**
* 多列排序后触发的事件
* @param array 排序列定义数组
* @eventProperty
*/
onSorted?: (array?: Array<GridColumnSortDefinition>) => void;
/** 返回所有数据的数据(未过滤) */
get allSource(): Array<GridItem>;
/** 获取数据数组(已过滤) */
get source(): Array<GridItem>;
/** 设置数据,并刷新列表 */
set source(list: Array<GridItem>);
/** 获取列表是否为只读,默认值 `false` */
get readonly(): boolean;
/** 设置列表是否为只读 */
set readonly(flag: boolean);
/** 获取当前选中的行索引的数组 */
get selectedIndexes(): Array<number>;
/** 设置当前选中的行索引的数组,并刷新列表 */
set selectedIndexes(indexes: Array<number>);
/** 获取 Grid 当前是否处于加载状态 */
get loading(): boolean;
/** 使 Grid 进入加载状态 */
set loading(flag: boolean);
/** 获取 Grid 当前滚动的偏移量 */
get scrollTop(): number;
/** 设置 Grid 滚动偏移量 */
set scrollTop(top: number);
/** 获取 Grid 的页面元素 */
get element(): HTMLElement;
/** 获取当前 Grid 是否已发生改变 */
get changed(): boolean;
/** 获取当前是否为虚模式状态 */
get virtual(): boolean;
/** 获取当前排序的列关键字,为 null 则当前无排序列 */
get sortKey(): string | undefined;
/** 获取当前选中行的索引,为 -1 则当前没有选中行 */
get selectedIndex(): number | -1;
/**
* 初始化Grid控件
* @param container 父容器元素,若未传值则采用构造方法中传入的父容器元素
*/
init(container?: HTMLElement): void;
/**
* 设置数据列表,该方法为 set source 属性的语法糖
* @param source 待设置的数据列表
*/
setData(source: Array<GridItem>): void;
/**
* 设置单行数据
* @param index 行索引
* @param item 待设置的行数据值
*/
setItem(index: number, item: GridItem): void;
/**
* 添加行数据
* @param item 待添加的行数据值
* @param index 待添加的行索引
* @returns 返回已添加的行数据
*/
addItem(item: GridItem, index?: number): GridItem;
/**
* 批量添加行数据
* @param array 待添加的行数据数组
* @param index 待添加的行索引
* @returns 返回已添加的行数据数组
*/
addItems(array: Array<GridItem>, index?: number): Array<GridItem>
/**
* 删除行数据
* @param index 待删除的行索引
* @returns 返回已删除的行数据
*/
removeItem(index: number): GridItem;
/**
* 批量删除行数据
* @param indexes 待删除的行索引数组,未传值时删除所有行
* @returns 返回已删除的行数据数组
*/
removeItems(indexes?: Array<number>): Array<GridItem>;
/**
* 滚动到指定行的位置
* @param index 待滚动至的行索引
*/
scrollToIndex(index: number): void;
/**
* 调整 Grid 元素的大小,一般需要在宽度变化时(如页面大小发生变化时)调用
* @param force 是否强制 {@linkcode reload},默认只有待渲染的行数发生变化时才会调用
* @param keep 是否保持当前滚动位置
*/
resize(force?: boolean, keep?: boolean): void;
/**
* 重新计算需要渲染的行,并载入元素,一般需要在高度变化时调用
* @param keep 是否保持当前滚动位置
*/
reload(keep?: boolean): void;
/**
* 重新填充Grid单元格数据
*/
refresh(): void;
/**
* 把所有行重置为未修改的状态
*/
resetChange(): void;
/**
* 根据当前排序字段进行列排序
* @param reload 为 true 则在列排序后调用 {@linkcode Grid.reload} 方法
*/
sortColumn(reload?: boolean): void;
/**
* 根据当前排序列数组进行多列排序
* @param reload 为 true 则在多列排序后调用 {@linkcode Grid.reload} 方法
*/
sort(reload?: boolean): void;
/**
* 清除列头复选框的选中状态
*/
clearHeaderCheckbox(): void;
/**
* 显示多列排序设置面板
*/
showSortPanel(): void;
}

View File

@@ -285,6 +285,7 @@ let r = lang;
* @property {Function} [onFilterOk] - 列过滤点击 `OK` 时触发的事件
* @property {Function} [onFiltered] - 列过滤后触发的事件
* @property {Function} [onDropExpanded] - 列为下拉框类型时在下拉列表展开时触发的事件
* @property {Function} [onDropCollapsed] - 列为下拉框类型时在下拉列表关闭时触发的事件
* @interface
* @example
* [
@@ -410,6 +411,15 @@ let r = lang;
* @this GridColumnDefinition
* @memberof GridColumnDefinition
*/
/**
* 列为下拉框类型时在下拉列表关闭时触发的事件
* @name onDropCollapsed
* @event
* @param {GridRowItem} item - 行数据对象
* @param {Dropdown} drop - 下拉框对象
* @this GridColumnDefinition
* @memberof GridColumnDefinition
*/
/**
* 判断列是否始终编辑的回调函数
@@ -613,6 +623,12 @@ export class Grid {
* @private
*/
parent: null,
/**
* Grid 包裹元素
* @type {HTMLDivElement}
* @private
*/
container: null,
/**
* Grid 元素 - `div.ui-grid`
* @type {HTMLDivElement}
@@ -818,7 +834,7 @@ export class Grid {
*/
footer: null,
/**
* 加载状态元素引用 - div.ui-grid-loading
* 加载状态元素引用 - div.ui-loading
* @type {HTMLDivElement}
* @private
*/
@@ -862,6 +878,13 @@ export class Grid {
* @ignore
*/
langs = {};
/**
* 区域字符串
* @type {string}
* @default "en"
* @ignore
*/
lgid = 'en';
/**
* 行数大于等于该值则启用虚模式
* @type {number}
@@ -1124,6 +1147,7 @@ export class Grid {
* @param {string} getText.{returns} 返回的多语言
* @property {GridColumnDefinition[]} columns - 列定义的数组
* @property {GridLanguages} [langs] - 多语言资源对象
* @property {string} [lgid=en] - 区域字符串
* @property {number} [virtualCount=100] - 行数大于等于该值则启用虚模式
* @property {boolean} [autoResize=true] - 未设置宽度的列自动调整列宽
* @property {number} [rowHeight=36] - 表格行高,修改后同时需要在 `.ui-grid` 所在父容器重写 `--line-height` 的值以配合显示
@@ -1274,10 +1298,23 @@ export class Grid {
if (!Array.isArray(list)) {
throw new Error('source is not an Array.')
}
list = list.reduce((array, item) => {
array.push({
__level: 0,
values: item
});
if (Array.isArray(item.__children)) {
array.push(...item.__children.map(c => ({
__level: 1,
values: c
})));
}
return array;
}, []);
list = list.map((it, index) => {
return {
__index: index,
values: it
...it
};
});
this._var.source = list;
@@ -1335,9 +1372,11 @@ export class Grid {
if (flag === false) {
this._var.refs.loading.style.visibility = 'hidden';
this._var.refs.loading.style.opacity = 0;
this._var.el.style.overflow = '';
} else {
this._var.refs.loading.style.visibility = 'visible';
this._var.refs.loading.style.opacity = 1;
this._var.el.style.overflow = 'hidden';
}
}
@@ -1402,6 +1441,8 @@ export class Grid {
this._var.parent = container;
this._var.isFirefox = /Firefox\//i.test(navigator.userAgent);
this._var.enabledDict = {};
const c = createElement('div', 'ui-grid-container');
this._var.container = c;
const grid = createElement('div', 'ui-grid');
grid.setAttribute('tabindex', 0);
grid.addEventListener('keydown', e => {
@@ -1506,7 +1547,8 @@ export class Grid {
}
});
}
container.replaceChildren(grid);
c.appendChild(grid);
container.replaceChildren(c);
const sizer = createElement('span', 'ui-grid-sizer');
grid.appendChild(sizer);
this._var.refs.sizer = sizer;
@@ -1525,7 +1567,11 @@ export class Grid {
wrapper.appendChild(table);
// tooltip
if (!this.tooltipDisabled) {
const holder = createElement('div', 'ui-grid-hover-holder');
const holder = createElement('div', 'ui-grid-hover-holder ui-grid-hover ui-tooltip-color',
// createElement('div', 'ui-grid-hover-pointer ui-grid-hover ui-tooltip-color'),
createElement('div', 'ui-grid-hover-curtain ui-grid-hover ui-tooltip-color'),
createElement('div', 'ui-grid-hover-content ui-grid-hover')
);
holder.addEventListener('mousedown', e => {
const holder = e.currentTarget;
const row = Number(holder.dataset.row);
@@ -1542,8 +1588,8 @@ export class Grid {
}
// loading
const loading = createElement('div', 'ui-grid-loading',
createElement('div', null, createIcon('fa-regular', 'spinner-third'))
const loading = createElement('div', 'ui-loading',
createElement('div')
);
this._var.refs.loading = loading;
grid.appendChild(loading);
@@ -1958,7 +2004,8 @@ export class Grid {
const gridWrapper = createElement('div', 'ui-sort-panel-grid');
content.append(buttonWrapper, gridWrapper);
const columnSource = this.columns.filter(c => c.sortable !== false); // ticket 56389, && c.visible !== false
columnSource.sort((a, b) => a.caption > b.caption ? 1 : -1);
const lgid = this.lgid;
columnSource.sort((a, b) => String(a.caption).localeCompare(b.caption, lgid));
grid.columns = [
{
width: 80,
@@ -2421,8 +2468,12 @@ export class Grid {
return new Promise(resolve => {
let working;
let url;
let path = ScriptPath;
if (nullOrEmpty(path) && typeof consts !== 'undefined') {
path = consts.modulePath;
}
if (typeof module === 'string') {
url = `${ScriptPath}${module}`;
url = `${path}${module}`;
} else {
url = URL.createObjectURL(new Blob([`let wasm,WASM_VECTOR_LEN=0,cachegetUint8Memory0=null;function getUint8Memory0(){return null!==cachegetUint8Memory0&&cachegetUint8Memory0.buffer===wasm.memory.buffer||(cachegetUint8Memory0=new Uint8Array(wasm.memory.buffer)),cachegetUint8Memory0}let cachegetInt32Memory0=null;function getInt32Memory0(){return null!==cachegetInt32Memory0&&cachegetInt32Memory0.buffer===wasm.memory.buffer||(cachegetInt32Memory0=new Int32Array(wasm.memory.buffer)),cachegetInt32Memory0}function passArray8ToWasm0(e,t){const a=t(1*e.length);return getUint8Memory0().set(e,a/1),WASM_VECTOR_LEN=e.length,a}function getArrayU8FromWasm0(e,t){return getUint8Memory0().subarray(e/1,e/1+t)}function encode_raw(e,t){var a=passArray8ToWasm0(t,wasm.__wbindgen_malloc),r=WASM_VECTOR_LEN;wasm[e+"_encode_raw"](8,a,r);var s=getInt32Memory0()[2],n=getInt32Memory0()[3],m=getArrayU8FromWasm0(s,n).slice();return wasm.__wbindgen_free(s,1*n),m}self.addEventListener("message",e=>{const t=e.data.type;if("init"===t)if("function"==typeof WebAssembly.instantiateStreaming){const t={},a=fetch(e.data.path+"wasm_flate_bg.wasm");WebAssembly.instantiateStreaming(a,t).then(({instance:e})=>{wasm=e.exports,self.postMessage({type:"init",result:0})}).catch(e=>a.then(t=>{"application/wasm"!==t.headers.get("Content-Type")?self.postMessage({type:"init",error:"\`WebAssembly.instantiateStreaming\` failed because your server does not serve wasm with \`application/wasm\` MIME type. Original error: "+e.message}):self.postMessage({type:"init",error:e.message})}))}else self.postMessage({type:"init",error:"no \`WebAssembly.instantiateStreaming\`"});else if("compress"===t)if(null==wasm)self.postMessage({error:"no \`wasm\` instance"});else{let t=encode_raw("${compressed ?? 'deflate'}",e.data.data);self.postMessage(t,[t.buffer])}});`]));
}
@@ -2469,7 +2520,7 @@ export class Grid {
}
})
working = true;
worker.postMessage({ type: 'init', path: ScriptPath });
worker.postMessage({ type: 'init', path });
});
}
@@ -2515,6 +2566,7 @@ export class Grid {
direction = 1;
}
const editing = col.sortAsText !== true;
const lgid = this.lgid;
const comparer = (a, b) => {
a = this._getItemSortProp(a, editing, col);
b = this._getItemSortProp(b, editing, col);
@@ -2541,8 +2593,9 @@ export class Grid {
b = b.join(', ');
}
if (typeof a === 'string' && typeof b === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
// a = a.toLowerCase();
// b = b.toLowerCase();
return a.localeCompare(b, lgid);
}
} else {
if (a == null && b != null) {
@@ -2558,8 +2611,9 @@ export class Grid {
b = b.join(', ');
}
if (typeof a === 'string' && typeof b === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
// a = a.toLowerCase();
// b = b.toLowerCase();
return a.localeCompare(b, lgid);
}
}
return a === b ? 0 : (a > b ? 1 : -1);
@@ -2727,10 +2781,15 @@ export class Grid {
}
th.appendChild(wrapper);
if (!readonly && col.enabled !== false && col.allcheck && alwaysEditing) {
const check = createCheckbox({
switch: col.switch,
onchange: e => this._onColumnAllChecked(col, e.target.checked)
});
let check;
if (typeof type.createEdit === 'function') {
check = type.createEdit(e => this._onColumnAllChecked(col, e.target.checked), col);
} else {
check = createCheckbox({
switch: col.switch,
onchange: e => this._onColumnAllChecked(col, e.target.checked)
});
}
wrapper.appendChild(check);
}
let caption;
@@ -2752,7 +2811,7 @@ export class Grid {
if (col.captionTooltip != null) {
const help = createIcon('fa-solid', 'question-circle');
wrapper.appendChild(help);
setTooltip(help, col.captionTooltip, false, this._var.parent);
setTooltip(help, col.captionTooltip, false, this._var.container);
}
// order arrow
if (col.sortable) {
@@ -3041,6 +3100,12 @@ export class Grid {
} else if (row.classList.contains('selected')) {
row.classList.remove('selected');
}
if (vals.__level !== 0) {
row.classList.add('ui-grid-row-level');
row.classList.add(`level-${vals.__level}`);
} else {
row.classList.remove('ui-grid-row-level');
}
const stateChanged = virtualRow.editing !== selected;
virtualRow.editing = selected;
// data
@@ -3105,7 +3170,7 @@ export class Grid {
if (col.text != null) {
val = col.text;
} else if (typeof col.filter === 'function') {
val = col.filter(item, selected, this._var.refs.body, startIndex + i);
val = col.filter(item, !this.readonly && selected, this._var.refs.body, startIndex + i);
} else {
val = item[col.key];
if (val != null) {
@@ -3604,7 +3669,7 @@ export class Grid {
/**
* @private
* @param {string} key
* @param {("autoResize" | "style" | "resizing" | "dragging" | "filterSource" | "filterHeight" | "filterTop")} name
* @param {("autoResize" | "style" | "resizing" | "dragging" | "filterSource" | "filterHeight" | "filterTop" | "filterSearched")} name
* @returns {any}
*/
_get(key, name) {
@@ -3618,7 +3683,7 @@ export class Grid {
/**
* @private
* @param {string} key
* @param {("autoResize" | "style" | "filterSource" | "filterHeight" | "filterTop")} name
* @param {("autoResize" | "style" | "filterSource" | "filterHeight" | "filterTop" | "filterSearched")} name
* @param {any} value
*/
_set(key, name, value) {
@@ -3703,7 +3768,7 @@ export class Grid {
if (e.parentElement.classList.contains('ui-switch')) {
return true;
}
return /^(input|label|layer|svg|use)$/i.test(e.tagName);
return /^(i|input|label|layer|svg|use)$/i.test(e.tagName);
}
/**
@@ -3815,6 +3880,7 @@ export class Grid {
panel.style.height = '';
}
this._set(col.key, 'filterSearched', false);
// search
let searchbox;
if (col.allowSearch !== false) {
@@ -3884,6 +3950,7 @@ export class Grid {
const type = this._var.colTypes[col.key];
const isDateColumn = type === GridDateColumn || type instanceof GridDateColumn;
const filterAsValue = col.filterAsValue;
const lgid = this.lgid;
array.sort((itemA, itemB) => {
let a = itemA.Value;
let b = itemB.Value;
@@ -3901,8 +3968,9 @@ export class Grid {
b = itemB.DisplayValue;
}
if (typeof a === 'string' && typeof b === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
// a = a.toLowerCase();
// b = b.toLowerCase();
return a.localeCompare(b, lgid);
}
}
return a > b ? 1 : (a < b ? -1 : 0);
@@ -3920,7 +3988,7 @@ export class Grid {
};
});
this._fillFilterList(col, itemlist, array, itemall);
itemall.querySelector('input').checked = ![...itemlist.querySelectorAll('.filter-content input')].some(i => !i.checked);
itemall.querySelector('input').checked = array.find(i => !i.__checked) == null;
panel.appendChild(itemlist);
if (searchbox != null) {
searchbox.addEventListener('input', e => {
@@ -3937,6 +4005,7 @@ export class Grid {
}
return String(displayValue).toLowerCase().includes(key);
});
this._set(col.key, 'filterSearched', items.length !== array.length);
this._fillFilterList(col, itemlist, items, itemall);
this._set(col.key, 'filterTop', -1);
itemlist.dispatchEvent(new Event('scroll'));
@@ -3949,24 +4018,34 @@ export class Grid {
ok.className = 'button';
ok.innerText = this.langs.ok;
ok.addEventListener('click', () => {
const array = this._get(col.key, 'filterSource').filter(i => i.__checked !== false);
if (typeof col.onFilterOk === 'function') {
col.onFilterOk.call(this, col, array);
const filterSource = this._get(col.key, 'filterSource');
const filterSearched = this._get(col.key, 'filterSearched');
if (!filterSearched && filterSource.find(i => i.__checked === false) == null) {
// all checked, equals to 'Reset'
delete col.filterValues;
this._var.colAttrs.__filtered = this.columns.some(c => c.filterValues != null);
filter.replaceChildren(createIcon('fa-solid', this.filterIcon));
filter.classList.remove('active');
} else {
if (GridColumnTypeEnum.isAlwaysEditing(col.type)) {
col.filterValues = array.map(a => a.Value);
const array = filterSource.filter(i => i.__checked !== false);
if (typeof col.onFilterOk === 'function') {
col.onFilterOk.call(this, col, array);
} else {
const nullValue = col.filterAllowNull ? null : '';
col.filterValues = array.map(a => a.Value == null ? nullValue : a.DisplayValue);
if (GridColumnTypeEnum.isAlwaysEditing(col.type)) {
col.filterValues = array.map(a => a.Value);
} else {
const nullValue = col.filterAllowNull ? null : '';
col.filterValues = array.map(a => a.Value == null ? nullValue : a.DisplayValue);
}
}
this._var.colAttrs.__filtered = true;
filter.replaceChildren(createIcon('fa-solid', this.filteredIcon));
filter.classList.add('active');
}
this._var.colAttrs.__filtered = true;
this._refreshSource();
if (typeof col.onFiltered === 'function') {
col.onFiltered.call(this, col);
}
filter.replaceChildren(createIcon('fa-solid', this.filteredIcon));
filter.classList.add('active');
this._onCloseFilter();
});
}),
@@ -3975,7 +4054,7 @@ export class Grid {
reset.innerText = this.langs.reset;
reset.addEventListener('click', () => {
delete col.filterValues;
this._var.colAttrs.__filtered = this.columns.some(c => c.filterValues != null)
this._var.colAttrs.__filtered = this.columns.some(c => c.filterValues != null);
this._refreshSource();
if (typeof col.onFiltered === 'function') {
col.onFiltered.call(this, col);
@@ -3998,34 +4077,39 @@ export class Grid {
* @private
* @param {GridColumnDefinition} col
* @param {HTMLDivElement} list
* @param {ValueItem[]} array
* @param {ValueItem[]} source
* @param {HTMLDivElement} all
*/
_fillFilterList(col, list, array, all) {
_fillFilterList(col, list, source, all) {
list.querySelector('.filter-holder')?.remove();
list.querySelector('.filter-content')?.remove();
const rowHeight = this.filterRowHeight;
const height = array.length * rowHeight;
const height = source.length * rowHeight;
this._set(col.key, 'filterHeight', height);
const holder = createElement('div', 'filter-holder');
holder.style.height = `${height}px`;
const content = createElement('div', 'filter-content');
content.style.top = `${rowHeight}px`;
this._set(col.key, 'filterSource', array);
this._set(col.key, 'filterSource', source);
const propKey = GridColumnTypeEnum.isAlwaysEditing(col.type) ? 'Value' : 'DisplayValue';
const nullValue = col.filterAllowNull ? null : '';
const allSelected = !Array.isArray(col.filterValues);
for (let item of array) {
let v = item.Value ?? nullValue;
if (v != null) {
v = Object.prototype.hasOwnProperty.call(item, propKey) ? item[propKey] : item;
for (let item of source) {
if (item.__checked == null) {
let v = item.Value ?? nullValue;
if (v != null) {
v = Object.prototype.hasOwnProperty.call(item, propKey) ? item[propKey] : item;
}
item.__checked = allSelected || col.filterValues.some(it => Array.isArray(it) ? it.includes(v) : it === v);
}
item.__checked = allSelected || col.filterValues.some(it => Array.isArray(it) ? it.includes(v) : it === v);
}
if (array.length > 12) {
array = array.slice(0, 12);
let array;
if (source.length > 12) {
array = source.slice(0, 12);
} else {
array = source;
}
this._doFillFilterList(col, content, array, all);
this._doFillFilterList(col, content, array, source, all);
list.append(holder, content);
}
@@ -4034,9 +4118,10 @@ export class Grid {
* @param {GridColumnDefinition} col
* @param {HTMLDivElement} content
* @param {ValueItem[]} array
* @param {ValueItem[]} source
* @param {HTMLDivElement} all
*/
_doFillFilterList(col, content, array, all) {
_doFillFilterList(col, content, array, source, all) {
for (let item of array) {
const div = createElement('div', 'filter-item');
const title = Object.prototype.hasOwnProperty.call(item, 'DisplayValue') ? item.DisplayValue : item;
@@ -4053,7 +4138,7 @@ export class Grid {
title,
onchange: e => {
item.__checked = e.target.checked;
all.querySelector('input').checked = ![...content.querySelectorAll('input')].some(i => !i.checked);
all.querySelector('input').checked = source.find(i => !i.__checked) == null;
}
}));
content.appendChild(div);
@@ -4083,15 +4168,16 @@ export class Grid {
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);
let source = this._get(col.key, 'filterSource');
let array;
if (startIndex + 12 < source.length) {
array = source.slice(startIndex, startIndex + 12);
} else {
array = array.slice(-12);
array = source.slice(-12);
}
const content = list.querySelector('.filter-content');
content.replaceChildren();
this._doFillFilterList(col, content, array, list.querySelector('.filter-all'));
this._doFillFilterList(col, content, array, source, list.querySelector('.filter-all'));
content.style.top = `${top + rowHeight}px`;
}
}
@@ -4341,7 +4427,7 @@ export class Grid {
*/
_onGridMouseMove(e, holder) {
e.stopPropagation();
if (e.target.classList.contains('ui-grid-hover-holder')) {
if (e.target.classList.contains('ui-grid-hover')) {
return;
}
let [parent, target] = this._getRowTarget(e.target);
@@ -4364,8 +4450,10 @@ export class Grid {
holder.dataset.col === col) {
return;
}
const type = this._var.colTypes[this.columns[col]?.key];
if (type?.canEdit && this._var.virtualRows[row]?.editing) {
const key = this.columns[col]?.key ?? col;
const type = this._var.colTypes[key];
const virtualRow = this._var.virtualRows[row];
if (type?.canEdit && virtualRow?.editing) {
delete holder.dataset.row;
delete holder.dataset.col;
if (holder.classList.contains('active')) {
@@ -4391,7 +4479,7 @@ export class Grid {
element.scrollHeight > element.offsetHeight) {
holder.dataset.row = row;
holder.dataset.col = col;
holder.innerText = element.innerText;
holder.querySelector('.ui-grid-hover-content').innerText = element.innerText;
const top = (parent.classList.contains('ui-grid-total-row') ? this._var.refs.footer.parentElement.offsetTop + 1 : target.offsetTop) + this._var.refs.table.offsetTop;
let left = target.offsetLeft;
let width = holder.offsetWidth;
@@ -4402,8 +4490,13 @@ export class Grid {
if (left > maxleft) {
left = maxleft;
}
const height = target.offsetHeight;
holder.style.cssText = `top: ${top}px; left: ${left}px; max-width: ${this._var.wrapClientWidth}px; min-height: ${height - 2}px`;
// const height = target.offsetHeight;
holder.style.cssText = `top: ${top}px; left: ${left}px; max-width: ${this._var.wrapClientWidth}px`; // ; min-height: ${height - 2}px
if (top > this.rowHeight * 2) {
holder.classList.remove('ui-grid-hover-down');
} else {
holder.classList.add('ui-grid-hover-down');
}
holder.classList.add('active');
} else if (holder.classList.contains('active')) {
delete holder.dataset.row;