diff --git a/lib/ui/css/grid.scss b/lib/ui/css/grid.scss index 8ce9ff9..254b9c6 100644 --- a/lib/ui/css/grid.scss +++ b/lib/ui/css/grid.scss @@ -19,6 +19,7 @@ --row-bg-color: #fff; --row-active-bg-color: #fafafa; --row-selected-bg-color: #e6f2fb; + --total-row-bg-color: #b3b3b3; --text-disabled-color: gray; --filter-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05); @@ -79,8 +80,9 @@ table-layout: fixed; >thead { + color: var(--header-fore-color); + tr { - color: var(--header-fore-color); position: sticky; top: 0; z-index: 2; @@ -251,15 +253,54 @@ } } - >tbody { - color: var(--cell-fore-color); + >tbody, + >tfoot { >.ui-grid-row { line-height: var(--line-height); white-space: nowrap; + box-sizing: border-box; + + >td { + padding: 0; + + &.sticky { + position: sticky; + z-index: 1; + } + + >span { + padding: var(--spacing-cell); + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: pre; + } + } + } + } + + >tfoot { + color: var(--header-fore-color); + position: absolute; + width: 100%; + background-color: var(--total-row-bg-color); + + >.ui-grid-row>td { + font-weight: bold; + + &.sticky { + background-color: var(--total-row-bg-color); + } + } + } + + >tbody { + color: var(--cell-fore-color); + + >.ui-grid-row { background-color: var(--row-bg-color); border-bottom: 1px solid var(--cell-border-color); - box-sizing: border-box; &:hover { background-color: var(--row-active-bg-color); @@ -278,11 +319,8 @@ } >td { - padding: 0; &.sticky { - position: sticky; - z-index: 1; background-color: var(--row-bg-color); } @@ -302,14 +340,6 @@ } } - >span { - padding: var(--spacing-cell); - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: pre; - } - >input[type="text"], >textarea { border: none; @@ -387,6 +417,7 @@ justify-content: center; align-items: center; position: relative; + padding: var(--spacing-s); >svg { width: 16px; diff --git a/lib/ui/grid/column.d.ts b/lib/ui/grid/column.d.ts deleted file mode 100644 index dc38b7a..0000000 --- a/lib/ui/grid/column.d.ts +++ /dev/null @@ -1,370 +0,0 @@ -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"; - /** - * 列是否可用(可编辑),允许以下类型

- * `boolean` 则直接使用该值

- * `string` 则以该值为关键字从行数据中取值作为判断条件

- * `(item: GridItem) => boolean` 则调用该函数(上下文为列定义对象),以返回值作为判断条件

- */ - enabled?: boolean | string | ((item: GridItem) => boolean); - /** - * 单元格取值采用该方法返回的值 - * @param item 行数据对象 - * @param editing 是否处于编辑状态 - * @param body Grid 控件的 `<tbody>` 部分 - */ - 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 | ((col: GridColumnDefinition) => Array); - /** 自定义列排序函数 */ - sortFilter?: (a: GridItem, b: GridItem) => -1 | 0 | 1; - /** 列为下拉列表类型时以该值设置下拉框的参数 */ - dropOptions?: DropdownOptions; - /** 列为下拉列表类型时以该值设置下拉列表数据源,支持函数返回,也支持返回异步对象 */ - source?: Array | ((item: GridItem) => Array | Promise>); - /** 下拉列表数据源是否缓存结果(即行数据未发生变化时仅从source属性获取一次值) */ - sourceCache?: boolean; - /** 列为图标类型时以该值设置图标样式(函数上下文为列定义对象),默认值 `fa-light` */ - iconType?: "fa-light" | "fa-regular" | "fa-solid"; - /** 列为图标类型时以该值作为单元格元素的额外样式类型(函数上下文为列定义对象) */ - iconClassName?: string | ((item: GridItem) => string); - /** 列为日期类型时以该值作为最小可选日期值 */ - dateMin?: string; - /** 列为日期类型时以该值作为最大可选日期值 */ - dateMax?: string; - /** 列为日期类型时自定义日期转字符串函数 */ - dateValueFormatter?: (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) => 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; -} - -/** 列定义基类 */ -export class GridColumn { - /** @ignore */ - constructor(); - /** - * 创建显示单元格时调用的方法 - * @param col 列定义对象 - * @returns 返回创建的单元格元素 - * @virtual - */ - static create(col: GridColumnDefinition): HTMLElement; - /** - * 创建编辑单元格时调用的方法

- * 元素修改后设置行包装对象的 `__editing` 后,支持在离开编辑状态时及时触发 {@linkcode leaveEdit} 方法
- * 更多例子参考代码中 {@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} 方法
- * 该属性返回 `true` 后,在任意事件中修改行包装对象的 `__editing` 值,则会在行列元素变动时及时触发 {@linkcode GridColumnDefinition.onInputEnded} 方法,避免例如文本框还未触发 `onchange` 事件就被移除元素而导致的问题
- * 更多例子参考代码中 {@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 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; - /** - * 设置单元格值时调用的方法

- * 支持以下几种数据类型

- * `"2024-01-26"`
- * `"1/26/2024"`
- * `"638418240000000000"`
- * `new Date('2024-01-26')`
- * @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; -} \ No newline at end of file diff --git a/lib/ui/grid/column.js b/lib/ui/grid/column.js index eea67ff..5340f0a 100644 --- a/lib/ui/grid/column.js +++ b/lib/ui/grid/column.js @@ -254,6 +254,12 @@ export class GridDropdownColumn extends GridColumn { return drop; } + /** + * @private + * @param {Map} item + * @param {GridColumnDefinition} col + * @returns {GridSourceItem[]} + */ static _getSource(item, col) { let source; if (col.sourceCache !== false) { diff --git a/lib/ui/grid/grid.d.ts b/lib/ui/grid/grid.d.ts deleted file mode 100644 index 50743ec..0000000 --- a/lib/ui/grid/grid.d.ts +++ /dev/null @@ -1,323 +0,0 @@ -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 }; -} - -/** 下拉框列数据源接口 */ -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; - /** 多语言资源对象 */ - langs?: GridLanguages; - /** 行数大于等于该值则启用虚模式,默认值 `100` */ - virtualCount?: number; - /** 表格行高,默认值 `36` */ - rowHeight?: number; - /** 文本行高,默认值 `24` */ - lineHeight?: number; - /** 列表底部留出额外行的空白,默认值 `0` */ - extraRows?: number; - /** 过滤条件列表的行高,默认值 `30` */ - filterRowHeight?: number; - /** 列表高度值,为 0 时列表始终显示全部内容(自增高),为非数字或者小于 0 则根据容器高度来确定虚模式的渲染行数,默认值 `null` */ - height?: number; - /** 列表是否为只读,默认值 `false` */ - readonly?: boolean; - /** 是否允许多选,默认值 `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; - - /** - * Grid 控件构造函数 - * @param container Grid 控件所在的父容器,可以是 string 表示选择器,也可以是 HTMLElement 对象

- * 构造时可以不进行赋值,但是调用 init 函数时则必须进行赋值 - * @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 事件类型

- * "reorder" 为发生列重排事件,此时 value 为目标列索引
- * "resize" 为发生列宽调整事件,此时 value 为列宽度值
- * "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) => void; - - /** 返回所有数据的数据(未过滤) */ - get allSource(): Array; - /** 获取数据数组(已过滤) */ - get source(): Array; - /** 设置数据,并刷新列表 */ - set source(list: Array); - /** 获取当前选中的行索引的数组 */ - get selectedIndexes(): Array; - /** 设置当前选中的行索引的数组,并刷新列表 */ - set selectedIndexes(indexes: Array); - /** 获取 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): 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, index?: number): Array - /** - * 删除行数据 - * @param index 待删除的行索引 - * @returns 返回已删除的行数据 - */ - removeItem(index: number): GridItem; - /** - * 批量删除行数据 - * @param indexes 待删除的行索引数组,未传值时删除所有行 - * @returns 返回已删除的行数据数组 - */ - removeItems(indexes?: Array): Array; - /** - * 滚动到指定行的位置 - * @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; -} \ No newline at end of file diff --git a/lib/ui/grid/grid.js b/lib/ui/grid/grid.js index 19176c9..c54ae6b 100644 --- a/lib/ui/grid/grid.js +++ b/lib/ui/grid/grid.js @@ -1,5 +1,5 @@ import '../css/grid.scss'; -import { global, isPositive, isMobile, throttle, truncate } from "../../utility"; +import { global, isPositive, isMobile, throttle, truncate, debounce } from "../../utility"; import { r as lang } from "../../utility/lgres"; import { nullOrEmpty } from "../../utility/strings"; import { createElement } from "../../functions"; @@ -23,6 +23,11 @@ const MiniColumnWidth = 50; const FilterPanelWidth = 200; const ExpandableWidth = 24; +/** + * @private + * @param {Event} e + * @returns {number} + */ function getClientX(e) { if (e == null) { return null; @@ -31,6 +36,11 @@ function getClientX(e) { return cx ?? e.clientX; } +/** + * @private + * @param {HTMLElement} target + * @returns {number} + */ function indexOfParent(target) { // return [...target.parentElement.children].indexOf(target); return Array.prototype.indexOf.call(target.parentElement.children, target); @@ -49,8 +59,8 @@ const ColumnTypeDefs = { let r = lang; /** - * 行数据接口 - * @typedef {object} GridItem + * 键值对 + * @typedef ValueItem * @property {any} Value - 值 * @property {string} DisplayValue - 显示值 */ @@ -58,15 +68,13 @@ let r = lang; /** * 行数据包装接口 * @typedef GridItemWrapper - * @property {object} values - 真实数据对象 - * @property {(GridItem | any)} values.{key} 数据属性 - * @property {object} source - 下拉数据源缓存对象 - * @property {GridSourceItem[]} source.{key} 数据源 + * @property {Map} values - 真实数据对象 + * @property {Map} source - 下拉数据源缓存对象 */ /** * 下拉框数据源接口 - * @typedef {object} GridSourceItem + * @typedef GridSourceItem * @property {string} value - 值 * @property {string} text - 显示文本 */ @@ -74,7 +82,7 @@ let r = lang; /** * 行数据可用性回调函数 * @callback GridItemBooleanCallback - * @param {GridItem} item - 行数据对象 + * @param {Map} item - 行数据对象 * @returns {boolean} 返回是否可用 * @this GridColumnDefinition */ @@ -82,7 +90,7 @@ let r = lang; /** * 行数据过滤回调函数 * @callback GridItemFilterCallback - * @param {GridItem} item - 行数据对象 + * @param {Map} item - 行数据对象 * @param {boolean} editing - 是否处于编辑状态 * @param {HTMLElement} [body] - Grid 控件的 `` 部分 * @returns {any} - 返回过滤后的显示或编辑值 @@ -92,15 +100,15 @@ let r = lang; /** * 行数据处理回调函数 * @callback GridItemObjectCallback - * @param {GridItem} item - 行数据对象 - * @returns {object} 返回任意对象 + * @param {Map} item - 行数据对象 + * @returns {any} 返回任意对象 * @this GridColumnDefinition */ /** * 行数据字符串回调函数 * @callback GridItemStringCallback - * @param {GridItem} item - 行数据对象 + * @param {Map} item - 行数据对象 * @returns {string} 返回字符串 * @this GridColumnDefinition */ @@ -109,22 +117,22 @@ let r = lang; * 列过滤器数据源回调函数 * @callback GridColumnFilterSourceCallback * @param {GridColumnDefinition} col - 列定义对象 - * @returns {GridItem[]} 返回过滤器的数据数组 + * @returns {ValueItem[]} 返回过滤器的数据数组 * @this Grid */ /** * 行数据排序回调函数 * @callback GridItemSortCallback - * @param {GridItem} a - 对比行数据1 - * @param {GridItem} b - 对比行数据2 + * @param {Map} a - 对比行数据1 + * @param {Map} b - 对比行数据2 * @returns {number} 返回大小对比结果 */ /** * 下拉列表数据源回调函数 * @callback GridDropdownSourceCallback - * @param {GridItem} item - 行数据对象 + * @param {Map} item - 行数据对象 * @returns {GridSourceItem[]} 行下拉列表数据源 */ @@ -172,7 +180,7 @@ let r = lang; * @property {number} type.Text=5 - 多行文本列 * @property {number} type.Date=6 - 日期选择列 * @property {string} [caption] - 列标题文本 - * @property {object} [captionStyle] - 列标题的元素样式 + * @property {any} [captionStyle] - 列标题的元素样式 * @property {number} [width] - 大于 0 则设置为该宽度,否则根据列内容自动调整列宽 * @property {("left" |"center" | "right")} [align=left] 列对齐方式 * @property {(boolean | string | GridItemBooleanCallback)} [enabled] - 列是否可用(可编辑),允许以下类型 @@ -181,11 +189,11 @@ let r = lang; * * `string` 则以该值为关键字从行数据中取值作为判断条件 * * `GridItemBooleanCallback` 则调用如下回调,以返回值作为判断条件 * @property {GridColumnDefinition} enabled.{this} - 上下文为列定义对象 - * @property {GridItem} enabled.item - 行数据对象 + * @property {Map} enabled.item - 行数据对象 * @property {boolean} enabled.{returns} - 返回是否启用 * @property {GridItemFilterCallback} [filter] - 单元格取值采用该函数返回的值 * @property {GridColumnDefinition} filter.{this} - 上下文为列定义对象 - * @property {GridItem} filter.item - 行数据对象 + * @property {Map} filter.item - 行数据对象 * @property {boolean} filter.editing - 是否处于编辑状态 * @property {HTMLElement} [filter.body] - Grid 控件的 `` 部分 * @property {any} filter.{returns} - 返回过滤后的显示或编辑值 @@ -197,38 +205,39 @@ let r = lang; * @property {boolean} [allcheck=false] - 列为复选框类型时是否在列头增加全选复选框 * @property {boolean} [shrink=false] - 列为收缩列,禁用自动调整大小 * @property {string} [class] - 单元格元素的额外样式类型字符串(仅在重建行元素时读取) - * @property {object} [css] - 单元格css样式对象(仅在重建行元素时读取) - * @property {(object | GridItemObjectCallback)} [style] - 单元格样式(填充行列数据时读取),支持直接返回样式对象或调用函数返回(若赋值则忽略 [styleFilter)]{@linkcode GridColumnDefinition#styleFilter}) + * @property {any} [css] - 单元格css样式对象(仅在重建行元素时读取) + * @property {any} [totalCss] - 合计行样式(仅在重建合计行元素时读取) + * @property {(any | GridItemObjectCallback)} [style] - 单元格样式(填充行列数据时读取),支持直接返回样式对象或调用函数返回(若赋值则忽略 [styleFilter]{@linkcode GridColumnDefinition#styleFilter}) * @property {GridColumnDefinition} style.{this} - 上下文为列定义对象 - * @property {GridItem} style.item - 行数据对象 - * @property {object} style.{returns} - 返回样式对象 + * @property {Map} style.item - 行数据对象 + * @property {any} style.{returns} - 返回样式对象 * @property {(@deprecated)} [styleFilter] - **已过时**
_根据返回值填充单元格样式(填充行列数据时读取)_ * @property {(string | GridItemStringCallback)} [background] - 设置单元格背景色(填充行列数据时读取),支持直接设置颜色字符串或调用函数返回(若赋值则忽略 [bgFilter]{@linkcode GridColumnDefinition#bgFilter}) * @property {GridColumnDefinition} background.{this} - 上下文为列定义对象 - * @property {GridItem} background.item - 行数据对象 + * @property {Map} background.item - 行数据对象 * @property {string} background.{returns} - 返回单元格背景色字符串 * @property {(@deprecated)} [bgFilter] - **已过时**
_根据返回值设置单元格背景色_ - * @property {object} [events] - 给单元格元素附加事件(事件函数上下文为数据行对象) + * @property {Map} [events] - 给单元格元素附加事件(事件函数上下文为数据行对象) * @property {Function} events.{event} - 事件回调函数 - * @property {Function} events.{event}.{this} - 上下文为行数据对象 - * @property {Function} events.{event}.e - 事件参数 - * @property {(object | GridItemObjectCallback)} [attrs] - 根据返回值设置单元格元素的附加属性,允许直接设置对象也支持调用如下函数返回对象 + * @property {Map} events.{event}.{this} - 上下文为行数据对象 + * @property {Event} events.{event}.e - 事件参数 + * @property {(any | GridItemObjectCallback)} [attrs] - 根据返回值设置单元格元素的附加属性,允许直接设置对象也支持调用如下函数返回对象 * @property {GridColumnDefinition} attrs.{this} - 上下文为列定义对象 - * @property {GridItem} attrs.item - 行数据对象 - * @property {object} attrs.{returns} - 返回附加属性对象 + * @property {Map} attrs.item - 行数据对象 + * @property {any} attrs.{returns} - 返回附加属性对象 * @property {boolean} [allowFilter=false] - 是否允许进行列头过滤 * @property {boolean} [filterAllowNull=false] - 是否区分 `null` 与空字符串 - * @property {(GridItem[] | GridColumnFilterSourceCallback)} [filterSource] - 自定义列过滤器的数据源,允许调用如下函数 + * @property {(ValueItem[] | GridColumnFilterSourceCallback)} [filterSource] - 自定义列过滤器的数据源,允许调用如下函数 * @property {Grid} filterSource.{this} - 上下文为 Grid * @property {GridColumnDefinition} filterSource.col - 列定义对象 - * @property {GridItem[]} filterSource.{returns} - 返回过滤器的数据数组 + * @property {ValueItem[]} filterSource.{returns} - 返回过滤器的数据数组 * @property {GridItemSortCallback} [sortFilter] - 自定义列排序函数 - * @property {GridItem} sortFilter.a - 对比行数据1 - * @property {GridItem} sortFilter.b - 对比行数据2 + * @property {Map} sortFilter.a - 对比行数据1 + * @property {Map} sortFilter.b - 对比行数据2 * @property {number} sortFilter.{returns} - 返回大小对比结果 * @property {DropdownOptions} [dropOptions] - 列为下拉列表类型时以该值设置下拉框的参数 * @property {(GridSourceItem[] | Promise | GridDropdownSourceCallback)} [source] - 列为下拉列表类型时以该值设置下拉列表数据源,支持返回异步对象,也支持如下函数返回 - * @property {GridItem} source.item - 行数据对象 + * @property {Map} source.item - 行数据对象 * @property {GridSourceItem[]} source.{returns} - 返回行下拉列表数据源 * @property {boolean} [sourceCache=false] - 下拉列表数据源是否缓存结果(即行数据未发生变化时仅从source属性获取一次值) * @property {("fa-light" | "fa-regular" | "fa-solid")} [iconType=fa-light] - 列为图标类型时以该值设置图标样式 @@ -239,7 +248,7 @@ let r = lang; * @property {any} dateValueFormatter.{returns} - 返回格式化后的结果 * @property {(string | GridItemStringCallback)} [tooltip] - 额外设置单元格的 tooltip,支持直接使用字符串或者调用如下函数 * @property {GridColumnDefinition} tooltip.{this} - 上下文为列定义对象 - * @property {GridItem} tooltip.item - 行数据对象 + * @property {Map} tooltip.item - 行数据对象 * @property {string} tooltip.{returns} - 返回额外 tooltip 字符串 * @example * [ @@ -319,6 +328,240 @@ let r = lang; * ] */ class GridColumnDefinition { + /** + * 列关键字,默认以该关键字从行数据中提取单元格值,行数据的关键字属性值里包含 DisplayValue 则优先显示此值 + * @type {string} + * @private + */ + key; + /** + * 列的类型,可以为 {@linkcode GridColumn} 的子类,或者内置类型 {@linkcode Grid.ColumnTypes} + * @type {(GridColumnTypeEnum | GridColumn)} + * @default Grid.ColumnTypes.Common + * @private + */ + type; + /** + * 列标题文本 + * @type {string} + * @private + */ + caption; + /** + * 列标题的元素样式 + * @type {any} + * @private + */ + captionStyle; + /** + * 大于 0 则设置为该宽度,否则根据列内容自动调整列宽 + * @type {number} + * @private + */ + width; + /** + * 列对齐方式 + * @type {("left" |"center" | "right")} + * @default left + * @private + */ + align; + /** + * 列是否可用(可编辑),允许以下类型 + * + * * `boolean` 则直接使用该值 + * * `string` 则以该值为关键字从行数据中取值作为判断条件 + * * `GridItemBooleanCallback` 则调用如下回调,以返回值作为判断条件 + * @type {(boolean | string | GridItemBooleanCallback)} + * @private + */ + enabled; + /** + * 单元格取值采用该函数返回的值 + * @type {GridItemFilterCallback} + * @private + */ + filter; + /** + * 单元格以该值填充内容,忽略filter与关键字属性 + * @type {string} + * @private + */ + text; + /** + * 列是否可见 + * @type {boolean} + * @default true + * @private + */ + visible; + /** + * 列是否允许调整宽度 + * @type {boolean} + * @default true + * @private + */ + resizable; + /** + * 列是否允许排序 + * @type {boolean} + * @default true + * @private + */ + sortable; + /** + * 列是否允许重排顺序 + * @type {boolean} + * @default true + * @private + */ + orderable; + /** + * 列为复选框类型时是否在列头增加全选复选框 + * @type {boolean} + * @default false + * @private + */ + allcheck; + /** + * 列为收缩列,禁用自动调整大小 + * @type {boolean} + * @default false + * @private + */ + shrink; + /** + * 单元格元素的额外样式类型字符串(仅在重建行元素时读取) + * @type {string} + * @private + */ + class; + /** + * 单元格css样式对象(仅在重建行元素时读取) + * @type {any} + * @private + */ + css; + /** + * 合计行样式(仅在重建合计行元素时读取) + * @type {any} + * @private + */ + totalCss; + /** + * 单元格样式(填充行列数据时读取),支持直接返回样式对象或调用函数返回(若赋值则忽略 [styleFilter]{@linkcode GridColumnDefinition#styleFilter}) + * @type {(any | GridItemObjectCallback)} + * @private + */ + style; + /** + * **已过时**
_根据返回值填充单元格样式(填充行列数据时读取)_ + * @type {GridItemObjectCallback} + * @private + * @deprecated + */ + styleFilter; + /** + * 设置单元格背景色(填充行列数据时读取),支持直接设置颜色字符串或调用函数返回(若赋值则忽略 [bgFilter]{@linkcode GridColumnDefinition#bgFilter}) + * @type {(string | GridItemStringCallback)} + * @private + */ + background; + /** + * **已过时**
_根据返回值设置单元格背景色_ + * @type {GridItemStringCallback} + * @private + * @deprecated + */ + bgFilter; + /** + * 给单元格元素附加事件(事件函数上下文为数据行对象) + * @type {Map} + * @private + */ + events; + /** + * 根据返回值设置单元格元素的附加属性,允许直接设置对象也支持调用如下函数返回对象 + * @type {(any | GridItemObjectCallback)} + * @private + */ + attrs; + /** + * 是否允许进行列头过滤 + * @type {boolean} + * @default false + * @private + */ + allowFilter; + /** + * 是否区分 `null` 与空字符串 + * @type {boolean} + * @default false + * @private + */ + filterAllowNull; + /** + * 自定义列过滤器的数据源,允许调用如下函数 + * @type {(ValueItem[] | GridColumnFilterSourceCallback)} + * @private + */ + filterSource; + /** + * 自定义列排序函数 + * @type {GridItemSortCallback} + * @private + */ + sortFilter; + /** + * 列为下拉列表类型时以该值设置下拉框的参数 + * @type {DropdownOptions} + * @private + */ + dropOptions; + /** + * 列为下拉列表类型时以该值设置下拉列表数据源,支持返回异步对象,也支持如下函数返回 + * @type {(GridSourceItem[] | Promise | GridDropdownSourceCallback)} + * @private + */ + source; + /** + * 下拉列表数据源是否缓存结果(即行数据未发生变化时仅从source属性获取一次值) + * @type {boolean} + * @default false + * @private + */ + sourceCache; + /** + * 列为图标类型时以该值设置图标样式 + * @type {("fa-light" | "fa-regular" | "fa-solid")} + * @default fa-light + * @private + */ + iconType; + /** + * 列为日期类型时以该值作为最小可选日期值 + * @type {string} + * @private + */ + dateMin; + /** + * 列为日期类型时以该值作为最大可选日期值 + * @type {string} + * @private + */ + dateMax; + /** + * 列为日期类型时自定义日期格式化函数 + * @type {DateFormatterCallback} + * @private + */ + dateValueFormatter; + /** + * 额外设置单元格的 tooltip,支持直接使用字符串或者调用如下函数 + * @type {(string | GridItemStringCallback)} + * @private + */ + tooltip; /** * 列头复选框改变时触发 * @type {Function} @@ -331,7 +574,7 @@ class GridColumnDefinition { /** * 单元格发生变化时触发 * @event - * @param {GridItem} item - 行数据对象 + * @param {Map} item - 行数据对象 * @param {(boolean | string | number)} value - 修改后的值 * @param {(boolean | string | number)} oldValue - 修改前的值 * @param {any} [e] - 列修改事件传递过来的任意对象 @@ -350,7 +593,7 @@ class GridColumnDefinition { * 列过滤点击 `OK` 时触发的事件 * @event * @param {GridColumnDefinition} col - 列定义对象 - * @param {GridItem[]} selected - 选中的过滤项 + * @param {ValueItem[]} selected - 选中的过滤项 * @this Grid */ onFilterOk; @@ -364,7 +607,7 @@ class GridColumnDefinition { /** * 列为下拉框类型时在下拉列表展开时触发的事件 * @event - * @param {GridItem} item - 行数据对象 + * @param {Map} item - 行数据对象 * @param {Dropdown} drop - 拉框对象 * @this GridColumnDefinition */ @@ -431,7 +674,7 @@ const GridColumnDirection = { /** * 扩展行生成回调函数 * @callback GridExpandableObjectCallback - * @param {GridItem} item - 行数据对象 + * @param {Map} item - 行数据对象 * @returns {GridExpandableObject} 返回扩展行对象 * @this Grid */ @@ -553,20 +796,26 @@ export class Grid { oldIndex: null, /** * 当前滚动上边距 - * @private * @type {number} + * @private */ scrollTop: 0, /** * 当前滚动左边距 - * @private * @type {number} + * @private */ scrollLeft: 0, /** - * 一页高度可显示的行数 + * 浏览器是否为 Firefox + * @type {boolean} * @private + */ + isFirefox: false, + /** + * 一页高度可显示的行数 * @type {number} + * @private */ rowCount: -1, /** @@ -578,7 +827,7 @@ export class Grid { /** * 列属性字典 * @private - * @property {object} {key} - 关键字对应列的拖拽、调整大小暂存对象 + * @property {any} {key} - 关键字对应列的拖拽、调整大小暂存对象 * @property {boolean} {key}.dragging - 列正在拖拽 * @property {number} {key}.offset - 列拖拽偏移 * @property {Function} {key}.mousemove - 拖拽或调整大小移动回调函数 @@ -586,8 +835,8 @@ export class Grid { * @property {number} {key}.resizing - 列临时大小 * @property {boolean} {key}.sizing - 列已进入修改大小的状态 * @property {boolean} {key}.autoResize - 列需要自动调整大小 - * @property {object} {key}.style - 列样式对象 - * @property {GridItem[]} {key}.filterSource - 列过滤面板数据源 + * @property {any} {key}.style - 列样式对象 + * @property {ValueItem[]} {key}.filterSource - 列过滤面板数据源 * @property {number} {key}.filterHeight - 列过滤面板高度 * @property {number} {key}.filterTop - 列过滤面板滚动头部间距 */ @@ -629,6 +878,18 @@ export class Grid { * @private */ containerHeight: null, + /** + * 合计行高度 + * @type {number} + * @private + */ + footerHeight: null, + /** + * 合计行底边距偏移量 + * @type {number} + * @private + */ + footerOffset: null, /** * 正文宽度 * @type {number} @@ -664,6 +925,12 @@ export class Grid { * @private */ header: null, + /** + * 表格合计行引用 - tfooter>tr + * @type {HTMLTableSectionElement} + * @private + */ + footer: null, /** * 加载状态元素引用 - div.ui-grid-loading * @type {HTMLDivElement} @@ -702,9 +969,14 @@ export class Grid { * @type {GridColumnDefinition[]} */ columns = []; + /** + * 合计行数据 + * @type {Map} + */ + total = null; /** * 多语言资源对象 - * @type {object} + * @type {any} * @property {string} [all=( All )] * @property {string} [ok=OK] * @property {string} [reset=Reset] @@ -763,11 +1035,6 @@ export class Grid { * @type {number | null} */ height; - /** - * 列表是否为只读 - * @type {boolean} - */ - readonly; /** * 是否允许多选 * @type {boolean} @@ -828,7 +1095,7 @@ export class Grid { * 扩展行生成器 * @type {GridExpandableObjectCallback} * @property {Grid} {this} - 上下文为 Grid - * @property {GridItem} item - 行数据对象 + * @property {Map} item - 行数据对象 * @property {GridExpandableObject} {returns} - 返回扩展行对象 * @property {number} {returns}.index - 行索引 * @property {HTMLElement} {returns}.element - 扩展行元素 @@ -899,7 +1166,7 @@ export class Grid { /** * 扩展行展开时触发的事件 * @event - * @param {GridItem} item - 行数据对象 + * @param {Map} item - 行数据对象 * @param {GridExpandableObject} expandableObject - 由 [expandableGenerator]{@linkcode Grid#expandableGenerator} 产生的行扩展对象 * @param {HTMLElement} expandableObject.element - 扩展行元素 * @this Grid @@ -908,7 +1175,7 @@ export class Grid { /** * 扩展行收缩时触发的事件 * @event - * @param {GridItem} item - 行数据对象 + * @param {Map} item - 行数据对象 * @param {GridExpandableObject} expandableObject - 由 [expandableGenerator]{@linkcode Grid#expandableGenerator} 产生的行扩展对象 * @param {HTMLElement} expandableObject.element - 扩展行元素 * @this Grid @@ -984,7 +1251,7 @@ export class Grid { /** * 返回所有数据的数据(未过滤) * @readonly - * @type {GridItem[]} + * @type {Array>} */ get allSource() { return this._var.source?.map(s => s.values) } @@ -1000,7 +1267,7 @@ export class Grid { /** * 获取已过滤的数据数组,或者设置数据并刷新列表 - * @type {GridItem[]} + * @type {Array>} * @property {any} Value - 值 * @property {string} DisplayValue - 显示值 */ @@ -1032,7 +1299,7 @@ export class Grid { /** * 设置单行数据 * @param {number} index - 行索引 - * @param {GridItem} item - 待设置的行数据对象 + * @param {Map} item - 待设置的行数据对象 */ setItem(index, item) { if (this._var.currentSource == null) { @@ -1053,9 +1320,9 @@ export class Grid { /** * 添加行数据 - * @param {GridItem} item - 待添加的行数据值 + * @param {Map} item - 待添加的行数据值 * @param {number} [index] - 待添加的行索引 - * @returns {GridItem} 返回已添加的行数据 + * @returns {Map} 返回已添加的行数据 */ addItem(item, index) { if (this._var.currentSource == null) { @@ -1091,9 +1358,9 @@ export class Grid { /** * 批量添加行数据 - * @param {GridItem[]} array - 待添加的行数据数组 + * @param {Array>} array - 待添加的行数据数组 * @param {number} [index] - 待添加的行索引 - * @returns {GridItem[]} 返回已添加的行数据数组 + * @returns {Array>} 返回已添加的行数据数组 */ addItems(array, index) { if (this._var.currentSource == null) { @@ -1135,7 +1402,7 @@ export class Grid { /** * 删除行数据 * @param {number} index - 待删除的行索引 - * @returns {GridItem} 返回已删除的行数据 + * @returns {Map} 返回已删除的行数据 */ removeItem(index) { if (this._var.currentSource == null) { @@ -1163,7 +1430,7 @@ export class Grid { /** * 批量删除行数据 * @param {number[]} [indexes] - 待删除的行索引数组,未传值时删除所有行 - * @returns {GridItem[]} 返回已删除的行数据数组 + * @returns {Array>} 返回已删除的行数据数组 */ removeItems(indexes) { if (this._var.currentSource == null) { @@ -1210,6 +1477,10 @@ export class Grid { return array; } + /** + * @private + * @param {GridItemWrapper[]} list + */ _refreshSource(list) { list ??= this._var.source; if (this._var.colAttrs.__filtered === true) { @@ -1265,15 +1536,31 @@ export class Grid { return this.columns[this.sortIndex]?.key; } + /** + * @private + * @type {HTMLTableRowElement[]} + */ get _tableRows() { // return [...this._var.refs.body.children]; return Array.prototype.slice.call(this._var.refs.body.querySelectorAll('&>.ui-grid-row')); } + /** + * @private + * @type {HTMLTableCellElement[]} + */ get _headerCells() { return Array.prototype.slice.call(this._var.refs.header.querySelectorAll('&>th.column')); } + /** + * @private + * @type {HTMLTableCellElement[]} + */ + get _footerCells() { + return Array.prototype.slice.call(this._var.refs.footer.querySelectorAll('&>.ui-grid-cell')); + } + /** * 获取虚模式起始索引 * @readonly @@ -1362,6 +1649,7 @@ export class Grid { container = ele; } this._var.parent = container; + this._var.isFirefox = /Firefox\//i.test(navigator.userAgent); const grid = createElement('div', 'ui-grid'); grid.setAttribute('tabindex', 0); grid.addEventListener('keydown', e => { @@ -1416,7 +1704,7 @@ export class Grid { if (parent == null) { return; } - const rowIndex = indexOfParent(parent); + const rowIndex = parent.classList.contains('ui-grid-total-row') ? -1 : indexOfParent(parent); let colIndex = indexOfParent(target) - (this.expandable ? 1 : 0); if (colIndex >= this.columns.length) { colIndex = -1; @@ -1438,6 +1726,7 @@ export class Grid { this._var.refs.table = table; this._createHeader(table); this._createBody(table); + this._createFooter(table); wrapper.appendChild(table); // tooltip if (!this.tooltipDisabled) { @@ -1470,13 +1759,15 @@ export class Grid { this.sortColumn(true); } else if (this.sortArray?.length > 0) { this.sort(true); + } else { + this.reload(); } } } /** * 设置数据列表,该方法为 [source]{@linkcode Grid#source} 属性的语法糖 - * @param {GridItem[]} source - 待设置的数据列表 + * @param {Array>} source - 待设置的数据列表 */ setData(source) { this.source = source; @@ -1545,6 +1836,19 @@ export class Grid { } this._refreshSource(); return; + } else if (filtered) { + const headers = this._headerCells; + for (let i = 0; i < this.columns.length; ++i) { + const ele = headers[i].querySelector('.filter'); + if (ele == null) { + continue; + } + if (this.columns[i].filterValues != null) { + ele.classList.add('active'); + } else { + ele.classList.remove('active'); + } + } } let length = this._var.currentSource?.length ?? 0; if (this.extraRows > 0) { @@ -1552,14 +1856,24 @@ export class Grid { } this._var.containerHeight = length * (this.rowHeight + 1); if (!keep) { + this._var.scrollTop = 0; 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(); + // size adjustment + const headerHeight = this._var.headerHeight || this.rowHeight; + if (this.total != null) { + const footerHeight = this._var.footerHeight || this.rowHeight; + this._var.refs.wrapper.style.height = `${headerHeight + this._var.containerHeight + footerHeight}px`; + // footer position + this._var.footerOffset = this._var.refs.table.offsetHeight - this._var.el.clientHeight; + this._var.refs.footer.parentElement.style.bottom = `${this._var.refs.table.offsetTop + this._var.footerOffset - this._var.el.scrollTop}px`; + } else { + this._var.refs.wrapper.style.height = `${headerHeight + this._var.containerHeight}px`; + } } /** @@ -1586,7 +1900,11 @@ export class Grid { } }); } else { - this._var.headerHeight = this._var.refs.table.children[0].offsetHeight; + const children = this._var.refs.table.children; + this._var.headerHeight = children[0].offsetHeight; + if (children.length > 2) { + this._var.footerHeight = children[2].offsetHeight; + } } } @@ -1602,6 +1920,20 @@ export class Grid { } } + /** + * @private + * @callback PrivateGridComparerCallback + * @param {GridItemWrapper} a + * @param {GridItemWrapper} b + * @returns {number} + */ + + /** + * @private + * @param {GridColumnDefinition} col + * @param {GridColumnDirection} direction + * @returns {PrivateGridComparerCallback} + */ _getComparer(col, direction) { if (typeof col.sortFilter !== 'function') { if (isNaN(direction)) { @@ -1925,6 +2257,11 @@ export class Grid { }); } + /** + * @private + * @param {HTMLTableElement} table + * @returns {HTMLTableSectionElement} + */ _createHeader(table) { const thead = createElement('thead'); if (this.headerVisible === false) { @@ -2085,6 +2422,11 @@ export class Grid { return thead; } + /** + * @private + * @param {HTMLTableElement} table + * @returns {HTMLTableSectionElement} + */ _createBody(table) { const body = createElement('tbody'); table.appendChild(body); @@ -2108,6 +2450,68 @@ export class Grid { return body; } + /** + * @private + * @param {HTMLTableElement} table + * @returns {HTMLTableSectionElement} + */ + _createFooter(table) { + const tfoot = createElement('tfoot'); + tfoot.style.display = 'none'; + table.appendChild(tfoot); + tfoot.addEventListener('dblclick', e => this._onRowDblClicked(e)); + const footer = createElement('tr', 'ui-grid-row ui-grid-total-row'); + tfoot.appendChild(footer); + let left = this.expandable ? ExpandableWidth : 0; + if (this.expandable) { + footer.appendChild(createElement('td', td => { + td.className = 'ui-expandable sticky'; + const w = `${ExpandableWidth}px`; + td.style.cssText = convertCssStyle({ + 'width': w, + 'max-width': w, + 'min-width': w, + 'left': '0px' + }); + }, createElement('div'))); + } + this.columns.forEach((col, j) => { + const cell = createElement('td', 'ui-grid-cell'); + if (col.visible !== false) { + let style = this._get(col.key, 'style') ?? {}; + if (col.isfixed) { + cell.classList.add('sticky'); + style.left = `${left}px`; + } + left += col.width; + cell.dataset.col = String(j); + if (col.totalCss != null) { + style = { ...style, ...col.totalCss }; + } + style = convertCssStyle(style); + if (style !== '') { + cell.style.cssText = style; + } + const element = GridColumn.create(col) + if (typeof col.class === 'string') { + GridColumn.setClass(element, col.class); + } + cell.appendChild(element); + } else { + cell.style.display = 'none'; + } + footer.appendChild(cell); + }) + footer.appendChild(createElement('td', td => td.innerText = '\u00a0')); + + this._var.refs.footer = footer; + return tfoot; + } + + /** + * @private + * @param {HTMLTableSectionElement} content + */ _adjustRows(content) { let count = this._var.rowCount; if (isNaN(count) || count < 0 || !this.virtual) { @@ -2191,6 +2595,12 @@ export class Grid { } } + /** + * @private + * @param {HTMLTableRowElement[]} rows + * @param {GridColumnDefinition[]} cols + * @param {any} widths + */ _fillRows(rows, cols, widths) { const startIndex = this._var.startIndex; const selectedIndexes = this._var.selectedIndexes; @@ -2207,7 +2617,7 @@ export class Grid { } const offset = this.expandable ? 1 : 0; const readonly = this.readonly; - [...rows].forEach((row, i) => { + rows.forEach((row, i) => { const vals = this._var.currentSource[startIndex + i]; if (vals == null) { return; @@ -2398,8 +2808,47 @@ export class Grid { delete vals.__editing; } }); + // total + const tfoot = this._var.refs.footer.parentElement; + if (this.total != null) { + if (tfoot.style.display === 'none') { + tfoot.style.display = ''; + } + const cells = this._var.refs.footer.children; + this.columns.forEach((col, j) => { + if (col.visible === false) { + return; + } + const cell = cells[j + offset]; + if (cell == null) { + return; + } + let val = this.total[col.key]; + if (val?.DisplayValue != null) { + val = val.DisplayValue; + } + val ??= ''; + const element = cell.children[0]; + GridColumn.setValue(element, val); + // auto resize + if (this._var.needResize && this._get(col.key, 'autoResize')) { + const width = element.scrollWidth + 12; + if (width > 0 && widths != null && (isNaN(widths[j]) || widths[j] < width)) { + widths[j] = width; + widths.flag = true; + } + } + }); + } else if (tfoot.style.display === '') { + tfoot.style.display = 'none'; + } } + /** + * @private + * @param {number} index + * @param {number} width + */ _changeColumnWidth(index, width) { const col = this.columns[index]; // const oldwidth = col.width; @@ -2449,9 +2898,45 @@ export class Grid { } } } - this._var.headerHeight = this._var.refs.table.children[0].offsetHeight; + // footer + const children = this._var.refs.table.children; + const hasTotal = this.total != null; + if (hasTotal) { + const footerCells = this._footerCells; + element = footerCells[index]; + element.style.width = w; + element.style.maxWidth = w; + element.style.minWidth = w; + if (col.isfixed) { + let l = left; + for (let i = index + 1; i < this.columns.length; ++i) { + if (this.columns[i].isfixed) { + footerCells[i].style.left = `${l}px`; + l += this.columns[i].width; + } else { + break; + } + } + } + } + this._var.headerHeight = children[0].offsetHeight; + if (hasTotal) { + this._var.footerHeight = children[2].offsetHeight; + const footerOffset = this._var.refs.table.offsetHeight - this._var.el.clientHeight; + if (this._var.footerOffset !== footerOffset) { + this._var.footerOffset = footerOffset; + this._var.refs.footer.parentElement.style.bottom = `${this._var.refs.table.offsetTop + footerOffset - this._var.el.scrollTop}px`; + } + } } + /** + * @private + * @param {number} index + * @param {number} offset + * @param {number} mouse + * @param {number} draggerCellLeft + */ _changingColumnOrder(index, offset, mouse, draggerCellLeft) { const children = this._headerCells; let element = children[index]; @@ -2515,6 +3000,10 @@ export class Grid { } } + /** + * @private + * @param {number} index + */ _changeColumnOrder(index) { this._var.refs.dragger.style.display = ''; this._var.refs.draggerCursor.style.display = ''; @@ -2575,6 +3064,12 @@ export class Grid { } } + /** + * @private + * @param {number} top + * @param {boolean} [reload] + * @returns {number} + */ _scrollToTop(top, reload) { const rowHeight = (this.rowHeight + 1); top -= (top % (rowHeight * 2)) + (RedumCount * rowHeight); @@ -2605,6 +3100,12 @@ export class Grid { return top; } + /** + * @private + * @param {string} key + * @param {("autoResize" | "style" | "resizing" | "dragging" | "filterSource" | "filterHeight" | "filterTop")} name + * @returns {any} + */ _get(key, name) { const attr = this._var.colAttrs[key]; if (attr == null) { @@ -2613,6 +3114,12 @@ export class Grid { return attr[name]; } + /** + * @private + * @param {string} key + * @param {("autoResize" | "style" | "filterSource" | "filterHeight" | "filterTop")} name + * @param {any} value + */ _set(key, name, value) { const attr = this._var.colAttrs[key]; if (attr == null) { @@ -2622,6 +3129,13 @@ export class Grid { } } + /** + * @private + * @param {any} item + * @param {boolean} editing + * @param {GridColumnDefinition} col + * @returns {any} + */ _getItemProp(item, editing, col) { let value; if (typeof col?.filter === 'function') { @@ -2639,6 +3153,11 @@ export class Grid { return value; } + /** + * @private + * @param {HTMLElement} target + * @returns {HTMLElement[]} + */ _getRowTarget(target) { let parent; while ((parent = target.parentElement) != null && !parent.classList.contains('ui-grid-row')) { @@ -2647,6 +3166,11 @@ export class Grid { return [parent, target]; } + /** + * @private + * @param {HTMLElement} element + * @returns {HTMLElement} + */ _getParentElement(element) { while (element != null && element.className !== 'ui-grid') { element = element.parentElement; @@ -2654,10 +3178,21 @@ export class Grid { return element; } + /** + * @private + * @param {string} tagName + * @returns {boolean} + */ _notHeader(tagName) { return /^(input|label|layer|svg|use)$/i.test(tagName); } + /** + * @private + * @param {MouseEvent} e + * @param {GridColumnDefinition} col + * @param {boolean} [force] + */ _onHeaderClicked(e, col, force) { if (!force && (this._get(col.key, 'resizing') || this._get(col.key, 'dragging'))) { return; @@ -2679,6 +3214,11 @@ export class Grid { } } + /** + * @private + * @param {MouseEvent} [e] + * @returns {boolean} + */ _onCloseFilter(e) { if (e != null) { if ((e.target.tagName === 'LAYER' && e.target.classList.contains('filter')) || @@ -2701,6 +3241,11 @@ export class Grid { return false; } + /** + * @private + * @param {MouseEvent} e + * @param {GridColumnDefinition} col + */ _onFilter(e, col) { if (this._onCloseFilter()) { return; @@ -2870,6 +3415,13 @@ export class Grid { filter.classList.add('hover'); } + /** + * @private + * @param {GridColumnDefinition} col + * @param {HTMLDivElement} list + * @param {ValueItem[]} array + * @param {HTMLDivElement} all + */ _fillFilterList(col, list, array, all) { list.querySelector('.filter-holder')?.remove(); list.querySelector('.filter-content')?.remove(); @@ -2897,6 +3449,12 @@ export class Grid { list.append(holder, content); } + /** + * @private + * @param {HTMLDivElement} content + * @param {ValueItem[]} array + * @param {HTMLDivElement} all + */ _doFillFilterList(content, array, all) { for (let item of array) { const div = createElement('div', 'filter-item'); @@ -2912,6 +3470,12 @@ export class Grid { } } + /** + * @private + * @param {GridColumnDefinition} col + * @param {HTMLDivElement} list + * @param {number} top + */ _onFilterScroll(col, list, top) { const rowHeight = this.filterRowHeight; top -= (top % (rowHeight * 2)) + rowHeight; @@ -2942,6 +3506,11 @@ export class Grid { } } + /** + * @private + * @param {MouseEvent} e + * @param {GridColumnDefinition} col + */ _onDragStart(e, col) { if (this._notHeader(e.target.tagName)) { return; @@ -3007,6 +3576,11 @@ export class Grid { ['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event])); } + /** + * @private + * @param {MouseEvent} e + * @param {GridColumnDefinition} col + */ _onResizeStart(e, col) { const cx = getClientX(e); const width = col.width; @@ -3058,6 +3632,11 @@ export class Grid { ['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event])); } + /** + * @private + * @param {MouseEvent} e + * @param {GridColumnDefinition} col + */ _onAutoResize(e, col) { const th = e.currentTarget.parentElement; const index = indexOfParent(th); @@ -3082,6 +3661,11 @@ export class Grid { } } + /** + * @private + * @param {GridColumnDefinition} col + * @param {boolean} flag + */ _onColumnAllChecked(col, flag) { if (this._var.currentSource == null) { return; @@ -3110,6 +3694,10 @@ export class Grid { } } + /** + * @private + * @param {Event} e + */ _onScroll(e) { if (this._var.colAttrs.__filtering != null) { this._onCloseFilter(); @@ -3118,12 +3706,27 @@ export class Grid { this.onBodyScrolled(e); } if (!this.virtual) { + if (this.total != null) { + this._var.refs.footer.parentElement.style.bottom = `${this._var.footerOffset - e.target.scrollTop}px`; + } return; } const top = e.target.scrollTop; this._scrollToTop(top); + if (this.total != null) { + this._var.refs.footer.parentElement.style.bottom = `${this._var.refs.table.offsetTop + this._var.footerOffset - e.target.scrollTop}px`; + } + if (this._var.isFirefox) { + // 修复 firefox 下列头显示位置不正确的问题 + debounce(this.refresh, RefreshInterval, this); + } } + /** + * @private + * @param {MouseEvent} e + * @param {HTMLDivElement} holder + */ _onGridMouseMove(e, holder) { e.stopPropagation(); if (e.target.classList.contains('ui-grid-hover-holder')) { @@ -3161,7 +3764,7 @@ export class Grid { holder.dataset.row = row; holder.dataset.col = col; holder.innerText = element.innerText; - const top = target.offsetTop + this._var.refs.table.offsetTop; + const top = (parent.classList.contains('ui-grid-total-row') ? this._var.refs.footer.parentElement.offsetTop + 1 : target.offsetTop) + this._var.refs.table.offsetTop; let left = target.offsetLeft; let width = holder.offsetWidth; if (width > this._var.bodyClientWidth) { @@ -3181,6 +3784,12 @@ export class Grid { } } + /** + * @private + * @param {MouseEvent} e + * @param {number} index + * @param {number} colIndex + */ _onRowClicked(e, index, colIndex) { const startIndex = this._var.startIndex; const selectedIndex = startIndex + index; @@ -3247,6 +3856,10 @@ export class Grid { } } + /** + * @private + * @param {MouseEvent} e + */ _onRowDblClicked(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'LAYER' && e.target.className === 'ui-check-inner' || e.target.tagName === 'LABEL' && (e.target.className === 'ui-drop-text' || e.target.className === 'ui-drop-caret')) { return; @@ -3263,6 +3876,15 @@ export class Grid { } } + /** + * @private + * @param {any} e + * @param {number} index + * @param {GridColumnDefinition} col + * @param {any} value + * @param {HTMLTableCellElement} cell + * @param {boolean} [blur] + */ _onRowChanged(e, index, col, value, cell, blur) { if (this._var.currentSource == null) { return; @@ -3311,6 +3933,12 @@ export class Grid { } } + /** + * @private + * @param {MouseEvent} _e + * @param {number} index + * @param {HTMLTableRowElement} _row + */ _onExpandable(_e, index, _row) { if (this._var.currentSource == null) { return; diff --git a/lib/ui/tooltip.js b/lib/ui/tooltip.js index 4955087..f4a51a9 100644 --- a/lib/ui/tooltip.js +++ b/lib/ui/tooltip.js @@ -2,6 +2,8 @@ import './css/tooltip.scss'; import { createElement } from "../functions"; // import { global } from "../utility"; +const pointerHeight = 12; + export function setTooltip(container, content, flag = false, parent = null) { const isParent = parent instanceof HTMLElement; if (isParent) { @@ -77,9 +79,9 @@ export function setTooltip(container, content, flag = false, parent = null) { const offsetHeight = wrapper.offsetHeight; const offsetWidth = wrapper.offsetWidth; if (isParent) { - top -= offsetHeight + 14; - if (top < -offsetHeight) { - top += c.offsetHeight + offsetHeight + 14; + top -= offsetHeight + pointerHeight; + if (top < 0) { + top += c.offsetHeight + offsetHeight + pointerHeight * 2; wrapper.classList.add('ui-tooltip-down'); } left += (c.offsetWidth - offsetWidth) / 2; @@ -125,20 +127,20 @@ export function setTooltip(container, content, flag = false, parent = null) { p = p.parentElement; } } - if (t - offsetHeight - 14 < 0) { + if (t - offsetHeight - pointerHeight < 0) { const containerOffsetHeight = c.offsetHeight; - if (t + containerOffsetHeight + offsetHeight + 14 > lastHeight) { + if (t + containerOffsetHeight + offsetHeight + pointerHeight > lastHeight) { top = t + (containerOffsetHeight - offsetHeight) / 2; if (top + offsetHeight + 1 > lastHeight) { top = lastHeight - offsetHeight - 1; } wrapper.classList.add('ui-tooltip-no'); } else { - top += containerOffsetHeight + 14; + top += containerOffsetHeight + pointerHeight; wrapper.classList.add('ui-tooltip-down'); } } else { - top -= offsetHeight + 14; + top -= offsetHeight + pointerHeight; wrapper.classList.remove('ui-tooltip-down'); } left += (c.offsetWidth - offsetWidth) / 2;