diff --git a/lib/app/communications/lib.js b/lib/app/communications/lib.js index bfdef7e..64bb325 100644 --- a/lib/app/communications/lib.js +++ b/lib/app/communications/lib.js @@ -297,7 +297,7 @@ export function getMessageStatus(comm, r, _var) { let statusTips; if (statusUpdatable !== false || ls.length > 1) { statusTips = createElement('div', tip => { - for (let i = 0; i < msgs.length; i++) { + for (let i = 0; i < msgs.length; ++i) { tip.appendChild(createElement('div', t => { const p = msgs[i]; if (statusUpdatable !== false && p.StatusChanged) { diff --git a/lib/ui/css/grid.scss b/lib/ui/css/grid.scss index 9cc4962..01c381b 100644 --- a/lib/ui/css/grid.scss +++ b/lib/ui/css/grid.scss @@ -546,6 +546,61 @@ } } } + + .ui-sort-panel-content { + height: 100%; + display: flex; + flex-direction: column; + + >.ui-sort-panel-buttons { + flex: 0 0 auto; + white-space: nowrap; + overflow: hidden; + + >button { + margin-right: 6px; + border: none; + line-height: 28px; + color: var(--title-color); + border-radius: var(--corner-radius); + padding: 0 10px; + box-sizing: border-box; + height: 28px; + cursor: pointer; + user-select: none; + background-color: var(--title-bg-color); + transition: opacity .12s ease; + display: inline-flex; + align-items: center; + + &:hover { + opacity: .8; + } + + &:disabled { + opacity: .6; + cursor: default; + } + + >svg { + flex: 0 0 auto; + width: 16px; + height: 16px; + fill: var(--title-color); + } + + >span { + flex: 1 1 auto; + margin-left: 4px; + } + } + } + + >.ui-sort-panel-grid { + flex: 1 1 auto; + position: relative; + } + } } /*@media (prefers-color-scheme: dark) { diff --git a/lib/ui/dropdown.d.ts b/lib/ui/dropdown.d.ts index 5e93352..14da60f 100644 --- a/lib/ui/dropdown.d.ts +++ b/lib/ui/dropdown.d.ts @@ -5,19 +5,19 @@ export interface DropdownItem { /** 显示文本 */ text: string; /** 源码显示内容 */ - html?: HTMLElement + html?: HTMLElement | string; } /** 下拉框选项接口 */ export interface DropdownOptions { - /** 文本关键字,默认值 text */ + /** 文本关键字,默认值 `text` */ textKey?: string; - /** 值关键字,默认值 value */ + /** 值关键字,默认值 `value` */ valueKey?: string; - /** 源码显示的关键字,默认值 html */ + /** 源码显示的关键字,默认值 `html` */ htmlKey?: string; - /** 最大输入长度,默认值 500 */ - maxLength?: Number; + /** 最大输入长度,默认值 `500` */ + maxLength?: number; /** 是否允许多选 */ multiSelect?: boolean; /** 选中值 */ @@ -35,7 +35,7 @@ export interface DropdownOptions { /** 搜索提示文本,默认值取语言资源 `searchHolder` "Search..." */ searchPlaceholder?: string; /** 焦点索引 */ - tabIndex?: Number; + tabIndex?: number; /** 输入框的提示文本 */ placeholder?: string; /** 是否固定为向下展开 */ diff --git a/lib/ui/grid/column.d.ts b/lib/ui/grid/column.d.ts index 6da6c40..dc38b7a 100644 --- a/lib/ui/grid/column.d.ts +++ b/lib/ui/grid/column.d.ts @@ -30,7 +30,7 @@ export interface GridColumnDefinition { /** 列标题的元素样式 */ captionStyle?: { [key: string]: string }; /** 大于 0 则设置为该宽度,否则根据列内容自动调整列宽 */ - width?: Number; + width?: number; /** 列对齐方式 */ align?: "left" | "center" | "right"; /** @@ -89,6 +89,8 @@ export interface GridColumnDefinition { dateMin?: string; /** 列为日期类型时以该值作为最大可选日期值 */ dateMax?: string; + /** 列为日期类型时自定义日期转字符串函数 */ + dateValueFormatter?: (date: Date) => string; /** 以返回值额外设置单元格的tooltip(函数上下文为列定义对象) */ tooltip?: string | ((item: GridItem) => string); @@ -109,7 +111,7 @@ export interface GridColumnDefinition { * @param e 列修改事件传递过来的任意对象 * @eventProperty */ - onChanged?: (this: Grid, item: GridItem, value: boolean | string | Number, oldValue: boolean | string | Number, e?: any) => void; + onChanged?: (this: Grid, item: GridItem, value: boolean | string | number, oldValue: boolean | string | number, e?: any) => void; /** * 文本单元格在输入完成时触发的事件 * @param this 上下文为 Grid 对象 @@ -145,6 +147,8 @@ export interface GridColumnDefinition { /** 列定义基类 */ export class GridColumn { + /** @ignore */ + constructor(); /** * 创建显示单元格时调用的方法 * @param col 列定义对象 @@ -180,7 +184,7 @@ export class GridColumn { * @param grid {@linkcode Grid} 对象 * @virtual */ - static setValue(element: HTMLElement, val: string | boolean | Number, vals: GridItemWrapper, col: GridColumnDefinition, grid: Grid): void; + static setValue(element: HTMLElement, val: string | boolean | number, vals: GridItemWrapper, col: GridColumnDefinition, grid: Grid): void; /** * 获取编辑状态单元格值时调用的方法 * @param e 由 {@linkcode createEdit} 方法中 `trigger` 函数传递来的对象 @@ -188,7 +192,7 @@ export class GridColumn { * @returns 返回单元格的值 * @virtual */ - static getValue(e: any, col: GridColumnDefinition): string | boolean | Number; + static getValue(e: any, col: GridColumnDefinition): string | boolean | number; /** * 设置单元格样式时调用的方法 * @param element 单元格元素 @@ -336,15 +340,22 @@ export class GridDateColumn extends GridColumn { */ static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement; /** - * @inheritdoc GridColumn.setValue + * 设置单元格值时调用的方法

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

+ * `"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; + static setValue(element: HTMLElement, val: string | number): void; /** * @inheritdoc GridColumn.getValue * @override */ - static getValue(e: any): string | Number; + static getValue(e: any): string | number; /** * @inheritdoc GridColumn.setEnabled * @override diff --git a/lib/ui/grid/column.js b/lib/ui/grid/column.js index df07cf5..132929c 100644 --- a/lib/ui/grid/column.js +++ b/lib/ui/grid/column.js @@ -111,7 +111,7 @@ export class GridDropdownColumn extends GridColumn { wrapper: container.parentElement }); drop.onSelected = trigger; - drop.onExpanded = function () { + drop.onExpanded = () => { if (it.__editing == null) { it.__editing = { [col.key]: true @@ -161,10 +161,10 @@ export class GridDropdownColumn extends GridColumn { return source; } - static _setValue(source, element, val) { - const data = source?.find(v => v.value === val); + static _setValue(source, element, val, opts) { + const data = source?.find(v => v[opts?.valueKey ?? 'value'] === val); if (data != null) { - val = data.text; + val = data[opts?.textKey ?? 'text']; } super.setValue(element, val); } @@ -173,9 +173,9 @@ export class GridDropdownColumn extends GridColumn { if (element.tagName !== 'DIV') { let source = this._getSource(item, col); if (source instanceof Promise) { - source.then(s => this._setValue(s, element, val)); + source.then(s => this._setValue(s, element, val, col.dropOptions)); } else { - this._setValue(source, element, val); + this._setValue(source, element, val, col.dropOptions); } return; } @@ -312,26 +312,35 @@ export class GridDateColumn extends GridColumn { static setValue(element, val) { if (element.tagName === 'INPUT') { - if (isNaN(val) || /^\d{4}-\d{2}-\d{2}$/.test(val)) { - element.value = val; + if (isNaN(val)) { + if (/^\d{4}-\d{2}-\d{2}$/.test(val)) { + element.value = val; + } else if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(val)) { + element.value = this._toDateValue(new Date(val)); + } else { + element.value = ''; + } } else { - val = new Date((val - 621355968e9) / 1e4); - const month = String(val.getMonth() + 1).padStart(2, '0'); - const date = String(val.getDate()).padStart(2, '0'); - element.value = `${val.getFullYear()}-${month}-${date}`; + if (!(val instanceof Date)) { + val = new Date((val - 621355968e9) / 1e4); + } + element.value = this._toDateValue(val); } } else { element.innerText = this.formatDate(val); } } - static getValue(e) { + static getValue(e, col) { const date = e.target?.valueAsDate; if (date instanceof Date && !isNaN(date)) { const year = date.getFullYear(); if (year < 1900 || year > 9999) { return ''; } + if (typeof col.dateValueFormatter === 'function') { + return col.dateValueFormatter(date); + } return String(date.getTime() * 1e4 + 621355968e9); } return ''; @@ -341,6 +350,15 @@ export class GridDateColumn extends GridColumn { element.disabled = enabled === false; } + static _toDateValue(dt) { + if (isNaN(dt)) { + return ''; + } + const month = String(dt.getMonth() + 1).padStart(2, '0'); + const date = String(dt.getDate()).padStart(2, '0'); + return `${dt.getFullYear()}-${month}-${date}`; + } + static _resolveDate(s) { if (s instanceof Date) { return s; diff --git a/lib/ui/grid/grid.d.ts b/lib/ui/grid/grid.d.ts index b44ee7e..a7c5bef 100644 --- a/lib/ui/grid/grid.d.ts +++ b/lib/ui/grid/grid.d.ts @@ -1,4 +1,12 @@ import { GridColumnDefinition } from "./column" +/** + * 单元格点击回调函数 + * + * @callback cellClickedCallback + * @param {number} index - 点击的行索引 + * @param {number} colIndex - 点击的列索引 + * @returns {boolean} 返回 `false` 则取消事件冒泡 + */ /** 列数据接口 */ interface GridItem { @@ -27,17 +35,33 @@ interface GridSourceItem { /** Grid 语言资源接口 */ interface GridLanguages { /** - * “所有”文本 - * - * @default `( All )` - */ + * “所有”文本,默认值 `( All )` */ all: string; - /** “确定”文本,默认值 OK */ + /** “确定”文本,默认值 `OK` */ ok: string; - /** “重置”文本,默认值 Reset */ + /** “重置”文本,默认值 `Reset` */ reset: string; - /** “空”文本,默认值 ( Null ) */ - null: 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"; } /** 列排序枚举 */ @@ -49,7 +73,7 @@ declare enum GridColumnDirection { } /** 列事件枚举 */ -declare enum GridColumnColumnEvent { +declare enum GridColumnEvent { /** 重排事件 */ Reorder = "reorder", /** 宽调整事件 */ @@ -80,41 +104,43 @@ export class Grid { * 判断列是否为复选框列 * @param type 列类型 */ - isCheckbox(type: Number): boolean; + 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 */ + /** 行数大于等于该值则启用虚模式,默认值 `100` */ + virtualCount?: number; + /** 表格行高,默认值 `36` */ + rowHeight?: number; + /** 文本行高,默认值 `24` */ + lineHeight?: number; + /** 列表底部留出额外行的空白,默认值 `0` */ + extraRows?: number; + /** 过滤条件列表的行高,默认值 `30` */ + filterRowHeight?: number; + /** 列表高度值,为 0 时列表始终显示全部内容(自增高),为非数字或者小于 0 则根据容器高度来确定虚模式的渲染行数,默认值 `null` */ + height?: number; + /** 列表是否为只读,默认值 `false` */ readonly?: boolean; - /** 是否允许多选,默认值 false */ + /** 是否允许多选,默认值 `false` */ multiSelect?: boolean; - /** 为 false 时只有点击在单元格内才会选中行,默认值 true */ + /** 为 false 时只有点击在单元格内才会选中行,默认值 `true` */ fullrowClick?: boolean; - /** 单元格 tooltip 是否禁用,默认值 false */ + /** 单元格 tooltip 是否禁用,默认值 `false` */ tooltipDisabled?: boolean; - /** 列头是否显示,默认值 true */ + /** 列头是否显示,默认值 `true` */ headerVisible?: boolean; - /** 监听事件的窗口载体,默认值 window */ + /** 监听事件的窗口载体,默认值 `window` */ window?: Window - /** 排序列的索引,默认值 -1 */ - sortIndex?: Number; - /** 排序方式,正数升序,负数倒序,默认值 1 */ + /** 排序列的索引,默认值 `-1` */ + sortIndex?: number; + /** 排序方式,正数升序,负数倒序,默认值 `1` */ sortDirection?: GridColumnDirection; + /** 排序列 */ + sortArray?: Array; /** * Grid 控件构造函数 @@ -130,34 +156,33 @@ export class Grid { * @param colIndex 即将选中的列索引 * @eventProperty */ - willSelect?: (index: Number, colIndex: Number) => boolean; + willSelect?: (index: number, colIndex: number) => boolean; /** * 单元格单击时触发,colIndex 为 -1 则表示点击的是行的空白处,返回 false 则取消事件冒泡 - * @param index 点击的行索引 - * @param colIndex 点击的列索引 + * @property {cellClickedCallback} * @eventProperty */ - cellClicked?: (index: Number, colIndex: Number) => boolean; + cellClicked?: (index: number, colIndex: number) => boolean; /** * 选中行发生变化时触发的事件 * @param index 选中的行索引 * @eventProperty */ - onSelectedRowChanged?: (index?: Number) => void; + onSelectedRowChanged?: (index?: number) => void; /** * 单元格双击时触发的事件,colIndex 为 -1 则表示点击的是行的空白处 * @param index 双击的行索引 * @param colIndex 双击的列索引 * @eventProperty */ - onCellDblClicked?: (index: Number, colIndex: Number) => void; + onCellDblClicked?: (index: number, colIndex: number) => void; /** * 行双击时触发的事件 * @param index 双击的行索引 * @eventProperty */ - onRowDblClicked?: (index: Number) => void; + onRowDblClicked?: (index: number) => void; /** * 列发生变化时触发的事件 * @param type 事件类型

@@ -168,31 +193,39 @@ export class Grid { * @param value 变化的值 * @eventProperty */ - onColumnChanged?: (type: GridColumnColumnEvent, colIndex: Number, value: Number | GridColumnDirection) => void; + onColumnChanged?: (type: GridColumnEvent, colIndex: number, value: number | GridColumnDirection) => void; /** * 列滚动时触发的事件 * @param e 滚动事件对象 * @eventProperty */ onBodyScrolled?: (e: Event) => void; + /** + * 多列排序后触发的事件 + * @param array 排序列定义数组 + * @eventProperty + */ + onSorted?: (array?: Array) => void; /** 获取数据数组 */ - get source(): GridItem; + get source(): Array; /** 设置数据,并刷新列表 */ set source(list: Array); /** 获取当前选中的行索引的数组 */ - get selectedIndexes(): Array; + get selectedIndexes(): Array; /** 设置当前选中的行索引的数组,并刷新列表 */ - set selectedIndexes(indexes: Array); + set selectedIndexes(indexes: Array); /** 获取 Grid 当前是否处于加载状态 */ get loading(): boolean; /** 使 Grid 进入加载状态 */ set loading(flag: boolean); /** 获取 Grid 当前滚动的偏移量 */ - get scrollTop(): Number; + get scrollTop(): number; /** 设置 Grid 滚动偏移量 */ - set scrollTop(top: Number); + set scrollTop(top: number); + /** 获取已过滤后的当前列表中的数据数组 */ + get sourceFiltered(): Array; /** 获取 Grid 的页面元素 */ get element(): HTMLElement; /** 获取当前 Grid 是否已发生改变 */ @@ -202,7 +235,7 @@ export class Grid { /** 获取当前排序的列关键字,为 null 则当前无排序列 */ get sortKey(): string | undefined; /** 获取当前选中行的索引,为 -1 则当前没有选中行 */ - get selectedIndex(): Number | -1; + get selectedIndex(): number | -1; /** * 初始化Grid控件 @@ -219,26 +252,27 @@ export class Grid { * @param index 行索引 * @param item 待设置的行数据值 */ - setItem(index: Number, item: GridItem): void; + setItem(index: number, item: GridItem): void; /** * 添加行数据 * @param item 待添加的行数据值 * @param index 待添加的行索引 */ - addItem(item: GridItem, index?: Number): void; + addItem(item: GridItem, index?: number): void; /** * 删除行数据 * @param index 待删除的行索引 + * @returns 返回已删除的行数据 */ - removeItem(index: Number): void; + removeItem(index: number): GridItem; /** * 滚动到指定行的位置 * @param index 待滚动至的行索引 */ - scrollToIndex(index: Number): void; + scrollToIndex(index: number): void; /** * 调整 Grid 元素的大小,一般需要在宽度变化时(如页面大小发生变化时)调用 - * @param force 是否强制reload,默认只有待渲染的行数发生变化时才会调用reload + * @param force 是否强制 {@linkcode reload},默认只有待渲染的行数发生变化时才会调用 * @param keep 是否保持当前滚动位置 */ resize(force?: boolean, keep?: boolean): void; @@ -257,11 +291,20 @@ export class Grid { resetChange(): void; /** * 根据当前排序字段进行列排序 - * @param reload 为 true 则在列排序后调用 reload 方法 + * @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 02472cb..df27d69 100644 --- a/lib/ui/grid/grid.js +++ b/lib/ui/grid/grid.js @@ -6,6 +6,7 @@ import { createElement } from "../../functions"; import { createIcon } from "../icon"; import { createCheckbox } from "../checkbox"; import { setTooltip } from "../tooltip"; +import { Popup, showAlert } from "../popup"; import { convertCssStyle } from "../extension"; import { GridColumn, GridInputColumn, GridTextColumn, GridDropdownColumn, GridCheckboxColumn, GridIconColumn, GridDateColumn } from "./column"; @@ -89,6 +90,7 @@ export class Grid { window = global; sortIndex = -1; sortDirection = 1; + sortArray = null; willSelect; cellClicked; @@ -119,7 +121,18 @@ export class Grid { all: r('allItem', '( All )'), ok: r('ok', 'OK'), reset: r('reset', 'Reset'), - null: r('null', '( Null )') + cancel: r('cancel', 'Cancel'), + null: r('null', '( Null )'), + addLevel: r('', 'Add level'), + deleteLevel: r('', 'Delete level'), + copyLevel: r('', 'Copy level'), + asc: r('', 'Ascending'), + desc: r('', 'Descending'), + column: r('', 'Column'), + order: r('', 'Order'), + sort: r('', 'Sort'), + requirePrompt: r('', 'Column required.'), + duplicatePrompt: r('', 'Column duplicated: "{column}"') }; } @@ -146,6 +159,8 @@ export class Grid { this._refreshSource(list); } + get sourceFiltered() { return this._var.currentSource?.map(s => s.values) ?? this.source } + setItem(index, item) { if (this._var.source == null) { throw new Error('no source'); @@ -172,8 +187,9 @@ export class Grid { if (this._var.source == null) { throw new Error('no source'); } - this._var.source.splice(index, 1); + const item = this._var.source.splice(index, 1)[0]; this.reload(); + return item; } _refreshSource(list) { @@ -202,6 +218,8 @@ export class Grid { if (this.sortIndex >= 0) { this.sortColumn(); + } else if (this.sortArray?.length > 0) { + this.sort(); } this.resize(); } @@ -351,8 +369,12 @@ export class Grid { this._var.el = grid; this._var.rendering = false; - if (this._var.source != null && this.sortIndex >= 0) { - this.sortColumn(); + if (this._var.source != null) { + if (this.sortIndex >= 0) { + this.sortColumn(); + } else if (this.sortArray?.length > 0) { + this.sort(); + } } } @@ -452,31 +474,12 @@ export class Grid { } } - sortColumn(reload) { - const index = this.sortIndex; - const col = this.columns[index]; - if (col == null) { - return; - } - const direction = this.sortDirection; - [...this._var.refs.header.children].forEach((th, i) => { - const arrow = th.querySelector('.arrow'); - if (arrow == null) { - return; - } - if (i === index) { - arrow.className = `arrow ${(direction !== 1 ? 'desc' : 'asc')}`; - } else if (arrow.className !== 'arrow') { - arrow.className = 'arrow'; - } - }); - let comparer; + _getComparer(col, direction) { if (typeof col.sortFilter !== 'function') { - let direction = this.sortDirection; if (isNaN(direction)) { direction = 1; } - comparer = (a, b) => { + return (a, b) => { a = this._getItemProp(a.values, true, col); b = this._getItemProp(b.values, true, col); if (a == null && typeof b === 'number') { @@ -491,9 +494,30 @@ export class Grid { } return a === b ? 0 : (a > b ? 1 : -1) * direction; }; - } else { - comparer = (a, b) => col.sortFilter(a.values, b.values) * direction; } + return (a, b) => col.sortFilter(a.values, b.values) * direction; + } + + sortColumn(reload) { + const index = this.sortIndex; + const col = this.columns[index]; + if (col == null) { + return; + } + this.sortArray = null; + const direction = this.sortDirection; + [...this._var.refs.header.children].forEach((th, i) => { + const arrow = th.querySelector('.arrow'); + if (arrow == null) { + return; + } + if (i === index) { + arrow.className = `arrow ${(direction !== 1 ? 'desc' : 'asc')}`; + } else if (arrow.className !== 'arrow') { + arrow.className = 'arrow'; + } + }); + const comparer = this._getComparer(col, direction); this._var.source.sort(comparer); if (this._var.colAttrs.__filtered === true) { this._var.currentSource.sort(comparer); @@ -508,11 +532,257 @@ export class Grid { } } + sort(reload) { + const sortArray = this.sortArray; + if (sortArray == null || sortArray.length === 0) { + return; + } + this.sortIndex = -1; + const comparer = (a, b) => { + for (let i = 0; i < sortArray.length; ++i) { + const s = sortArray[i]; + const col = this.columns.find(c => c.key === s.column && c.visible !== false); + if (col != null) { + const result = this._getComparer(col, s.order === 'desc' ? -1 : 1)(a, b); + if (result !== 0) { + return result; + } + } + } + return 0; + }; + this._var.source.sort(comparer); + if (this._var.colAttrs.__filtered === true) { + this._var.currentSource.sort(comparer); + } + if (this._var.rowCount < 0) { + return; + } + if (reload) { + this.reload(); + } else { + this.refresh(); + } + // arrow icon + [...this._var.refs.header.children].forEach((th, i) => { + const arrow = th.querySelector('.arrow'); + if (arrow == null) { + return; + } + const col = this.columns[i]; + const s = sortArray.find(s => s.column === col.key && col.visible !== false); + if (s != null) { + arrow.className = `arrow ${s.order}`; + } else if (arrow.className !== 'arrow') { + arrow.className = 'arrow'; + } + }); + } + clearHeaderCheckbox() { const boxes = this._var.refs.header.querySelectorAll('.ui-check-wrapper>input'); boxes.forEach(box => box.checked = false); } + showSortPanel() { + const content = createElement('div', 'ui-sort-panel-content'); + const buttonWrapper = createElement('div', 'ui-sort-panel-buttons'); + const grid = new Grid(null, r); + grid.langs = this.langs; + const rowChanged = index => { + buttonWrapper.querySelector('.ui-button-delete').disabled = index < 0; + buttonWrapper.querySelector('.ui-button-copy').disabled = index < 0; + buttonWrapper.querySelector('.ui-button-move-up').disabled = index < 1; + buttonWrapper.querySelector('.ui-button-move-down').disabled = index >= grid.source.length - 1; + }; + grid.onSelectedRowChanged = rowChanged; + const reload = index => { + grid.selectedIndexes = [index]; + grid.scrollTop = index * grid.rowHeight; + rowChanged(index); + } + buttonWrapper.append( + createElement('button', null, + createIcon('fa-light', 'plus'), + createElement('span', span => { + span.innerText = this.langs.addLevel; + span.addEventListener('click', () => { + let index = grid.selectedIndex; + const n = { column: '', order: 'asc' }; + if (index >= 0) { + index += 1; + grid.addItem(n, index); + } else { + grid.addItem(n); + index = grid.source.length - 1; + } + reload(index); + }); + }) + ), + createElement('button', 'ui-button-delete', + createIcon('fa-light', 'times'), + createElement('span', span => { + span.innerText = this.langs.deleteLevel; + span.addEventListener('click', () => { + let index = grid.selectedIndex; + if (index < 0) { + return; + } + grid.removeItem(index); + const length = grid.source.length; + if (index >= length) { + index = length - 1; + } + reload(index); + }); + }) + ), + createElement('button', 'ui-button-copy', + createIcon('fa-light', 'copy'), + createElement('span', span => { + span.innerText = this.langs.copyLevel; + span.addEventListener('click', () => { + const index = grid.selectedIndex; + if (index < 0) { + return; + } + const item = grid.source[index]; + if (item == null) { + return; + } + grid.addItem(Object.assign({}, item), index + 1); + reload(index + 1); + }); + }) + ), + createElement('button', button => { + button.className = 'ui-button-move-up'; + const icon = createIcon('fa-light', 'chevron-up'); + icon.addEventListener('click', () => { + const index = grid.selectedIndex; + if (index < 1) { + return; + } + const item = grid.source[index]; + if (item == null) { + return; + } + const it = grid.removeItem(index); + grid.addItem(it, index - 1); + reload(index - 1); + }); + button.appendChild(icon); + }), + createElement('button', button => { + button.className = 'ui-button-move-down'; + const icon = createIcon('fa-light', 'chevron-down'); + icon.addEventListener('click', () => { + const index = grid.selectedIndex; + if (index >= grid.source.length - 1) { + return; + } + const item = grid.source[index]; + if (item == null) { + return; + } + const it = grid.removeItem(index); + grid.addItem(it, index + 1); + reload(index + 1); + }); + button.appendChild(icon); + }) + ); + const gridWrapper = createElement('div', 'ui-sort-panel-grid'); + content.append(buttonWrapper, gridWrapper); + const columnSource = this.columns.filter(c => c.sortable !== false && c.visible !== false); + grid.columns = [ + { + key: 'column', + caption: this.langs.column, + width: 270, + type: Grid.ColumnTypes.Dropdown, + dropOptions: { + textKey: 'caption', + valueKey: 'key' + }, + source: columnSource, + sortable: false, + orderable: false + }, + { + key: 'order', + caption: this.langs.order, + width: 150, + type: Grid.ColumnTypes.Dropdown, + source: [ + { value: 'asc', text: this.langs.asc }, + { value: 'desc', text: this.langs.desc } + ], + sortable: false, + orderable: false + } + ]; + const pop = new Popup({ + title: this.langs.sort, + content, + resizable: true, + buttons: [ + { + text: this.langs.ok, + trigger: () => { + const source = grid.source; + if (source == null || source.length === 0) { + this.sortArray = null; + } else { + const dict = {}; + for (let i = 0; i < source.length; ++i) { + const it = source[i]; + if (it.column == null || it.column === '') { + grid.selectedIndexes = [i]; + grid.refresh(); + showAlert(this.langs.sort, this.langs.requirePrompt, 'warn'); + return false; + } + if (Object.prototype.hasOwnProperty.call(dict, it.column)) { + grid.selectedIndexes = [i]; + grid.refresh(); + let name = columnSource.find(c => c.key === it.column); + if (name == null) { + name = it.column; + } else { + name = name.caption; + } + showAlert(this.langs.sort, this.langs.duplicatePrompt.replace('{column}', name), 'warn'); + return false; + } + dict[it.column] = true; + } + this.sortArray = source; + this.sortDirection = 1; + this.sort(); + } + if (typeof this.onSorted === 'function') { + this.onSorted(this.sortArray); + } + return true; + } + }, + { text: this.langs.cancel } + ], + onResizeEnded: () => grid.resize() + }); + const source = this.sortArray || [{ column: '', order: 'asc' }]; + pop.show(this._var.el).then(() => { + pop.container.style.cssText += 'width: 520px; height: 400px'; + grid.init(gridWrapper); + grid.source = source.filter(s => s.column === '' || columnSource.find(c => c.key === s.column) != null); + grid.selectedIndexes = [0]; + grid.refresh(); + rowChanged(0); + }); + } + _createHeader(table) { const thead = createElement('thead'); if (this.headerVisible === false) { @@ -691,7 +961,7 @@ export class Grid { const exists = content.children.length; count -= exists; if (count > 0) { - for (let i = 0; i < count; i += 1) { + for (let i = 0; i < count; ++i) { const row = createElement('tr', 'ui-grid-row'); let left = 0; cols.forEach((col, j) => { @@ -940,7 +1210,7 @@ export class Grid { idx ??= 0; } else { const count = children.length; - for (let i = index; i < count - 1 && offset >= 0; i += 1) { + for (let i = index; i < count - 1 && offset >= 0; ++i) { element = children[i]; if (element == null || !element.className || element.classList.contains('sticky')) { idx = i; @@ -994,7 +1264,7 @@ export class Grid { if (targetIndex > 1) { targetIndex = orderIndex - 1; // const current = columns[index]; - // for (let i = index; i < targetIndex; i += 1) { + // for (let i = index; i < targetIndex; ++i) { // columns[i] = columns[i + 1]; // } // columns[targetIndex] = current; @@ -1018,16 +1288,18 @@ export class Grid { row.insertBefore(row.children[index], row.children[targetIndex]); } } - // refresh sortIndex - [...children].forEach((th, i) => { - const arrow = th.querySelector('.arrow'); - if (arrow == null) { - return; - } - if (arrow.className !== 'arrow') { - this.sortIndex = i; - } - }); + if (this.sortArray == null || this.sortArray.length === 0) { + // refresh sortIndex + [...children].forEach((th, i) => { + const arrow = th.querySelector('.arrow'); + if (arrow == null) { + return; + } + if (arrow.className !== 'arrow') { + this.sortIndex = i; + } + }); + } if (typeof this.onColumnChanged === 'function') { this.onColumnChanged(ColumnChangedType.Reorder, index, targetIndex); @@ -1641,7 +1913,7 @@ export class Grid { end = selectedIndex; } selectedIndexes.splice(0); - for (let i = start; i <= end; i += 1) { + for (let i = start; i <= end; ++i) { selectedIndexes.push(i); } flag = true; @@ -1713,7 +1985,7 @@ export class Grid { if (enabled !== false) { const val = item[col.key]; let oldValue; - if (val != null && Object.prototype.hasOwnProperty.call(val, 'Value') != null) { + if (val != null && Object.prototype.hasOwnProperty.call(val, 'Value')) { oldValue = val.Value; val.Value = value; } else {