UNPKG

@serenity-is/corelib

Version:
708 lines (597 loc) 21.1 kB
import { Column, FormatterContext, Grid, GridOptions } from "@serenity-is/sleekgrid"; import { Culture, Fluent, ListResponse, nsSerenity, tryGetText, type Lookup, type PropertyItem } from "../../base"; import { ScriptData, getLookup } from "../../compat"; import { IGetEditValue, IReadOnly, ISetEditValue } from "../../interfaces"; import { ReflectionUtils } from "../../types/reflectionutils"; import { DataGrid } from "../datagrid/datagrid"; import { GridSelectAllButtonHelper } from "../helpers/gridselectallbuttonhelper"; import { GridUtils } from "../helpers/gridutils"; import { SlickFormatting } from "../helpers/slickformatting"; import { SlickTreeHelper } from "../helpers/slicktreehelper"; import { ToolButton } from "../widgets/toolbar"; import { Widget } from "../widgets/widget"; import { CascadedWidgetLink } from "./cascadedwidgetlink"; import { stripDiacritics } from "./combobox"; import { EditorUtils } from "./editorutils"; import { EditorProps } from "./editorwidget"; export interface CheckTreeItem<TSource> { isSelected?: boolean; hideCheckBox?: boolean; isAllDescendantsSelected?: boolean; id?: string; text?: string; parentId?: string; children?: CheckTreeItem<TSource>[]; source?: TSource; } export class CheckTreeEditor<TItem extends CheckTreeItem<TItem>, P = {}> extends DataGrid<TItem, P> implements IGetEditValue, ISetEditValue, IReadOnly { static [Symbol.typeInfo] = this.registerEditor(nsSerenity, [IGetEditValue, ISetEditValue, IReadOnly]); static override createDefaultElement() { return document.createElement("div"); } declare private itemById: { [key: string]: TItem }; constructor(props: EditorProps<P>) { super(props); this.domNode.classList.add('s-CheckTreeEditor'); this.updateItems(); } protected getIdProperty() { return "id"; } protected getTreeItems(): TItem[] { return []; } protected updateItems(): void { var items = this.getTreeItems(); var itemById: Record<any, TItem> = {}; for (var i = 0; i < items.length; i++) { var item = items[i]; item.children = []; if (item.id) { itemById[item.id] = item; } if (item.parentId) { var parent = itemById[item.parentId]; if (parent != null) { parent.children.push(item); } } } this.view.addData({ Entities: items, Skip: 0, Take: 0, TotalCount: items.length }); this.updateSelectAll(); this.updateFlags(); } getEditValue(property: PropertyItem, target: any): void { if (this.getDelimited()) target[property.name] = this.get_value().join(","); else target[property.name] = this.get_value(); } setEditValue(source: any, property: PropertyItem): void { var value = source[property.name]; this.set_value(value); } protected getButtons(): ToolButton[] { var selectAllText = this.getSelectAllText(); if (!selectAllText) { return null; } var self = this; var buttons: ToolButton[] = []; buttons.push(GridSelectAllButtonHelper.define(function () { return self; }, function (x) { return x.id; }, function (x1) { return x1.isSelected; }, (x2, v) => { if (x2.isSelected !== v) { x2.isSelected = v; this.itemSelectedChanged(x2); } }, null, () => { this.updateFlags(); })); return buttons; } protected itemSelectedChanged(item: TItem): void { } protected getSelectAllText(): string { return tryGetText('Controls.CheckTreeEditor.SelectAll') ?? 'Select All'; } protected isThreeStateHierarchy(): boolean { return false; } protected createSlickGrid(): Grid { this.domNode.classList.add("slick-no-cell-border", "slick-no-odd-even", "slick-hide-header"); var result = super.createSlickGrid(); result.resizeCanvas(); return result; } protected onViewFilter(item: TItem): boolean { if (!super.onViewFilter(item)) { return false; } var items = this.view.getItems(); var self = this; return SlickTreeHelper.filterCustom(item, function (x) { if (x.parentId == null) { return null; } if (self.itemById == null) { self.itemById = {}; for (var i = 0; i < items.length; i++) { var o = items[i]; if (o.id != null) { self.itemById[o.id] = o; } } } return self.itemById[x.parentId]; }); } protected getInitialCollapse(): boolean { return false; } protected onViewProcessData(response: ListResponse<TItem>): ListResponse<TItem> { response = super.onViewProcessData(response); this.itemById = null; SlickTreeHelper.setIndents(response.Entities, function (x) { return x.id; }, function (x1) { return x1.parentId; }, this.getInitialCollapse()); return response; } protected onClick(e: Event, row: number, cell: number): void { super.onClick(e, row, cell); if (!Fluent.isDefaultPrevented(e)) { SlickTreeHelper.toggleClick(e as any, row, cell, this.view, function (x) { return x.id; }); } if (Fluent.isDefaultPrevented(e)) { return; } var target = e.target as HTMLElement; if (target.classList.contains('check-box')) { e.preventDefault(); if (this._readOnly) return; var checkedOrPartial = target.classList.contains('checked') || target.classList.contains('partial'); var item = this.itemAt(row); var anyChanged = item.isSelected !== !checkedOrPartial; this.view.beginUpdate(); try { if (item.isSelected !== !checkedOrPartial) { item.isSelected = !checkedOrPartial; this.view.updateItem(item.id, item); this.itemSelectedChanged(item); } anyChanged = this.setAllSubTreeSelected(item, item.isSelected) || anyChanged; this.updateSelectAll(); this.updateFlags(); } finally { this.view.endUpdate(); } if (anyChanged) { Fluent.trigger(this.domNode, "change"); } } } protected updateSelectAll(): void { GridSelectAllButtonHelper.update(this, function (x) { return x.isSelected; }); } protected updateFlags(): void { var view = this.view; var items = view.getItems(); var threeState = this.isThreeStateHierarchy(); if (!threeState) { return; } view.beginUpdate(); try { for (var i = 0; i < items.length; i++) { var item = items[i]; if (item.children == null || item.children.length === 0) { var allsel = this.getDescendantsSelected(item); if (allsel !== item.isAllDescendantsSelected) { item.isAllDescendantsSelected = allsel; view.updateItem(item.id, item); } continue; } var allSelected = this.allDescendantsSelected(item); var selected = allSelected || this.anyDescendantsSelected(item); if (allSelected !== item.isAllDescendantsSelected || selected !== item.isSelected) { var selectedChange = item.isSelected !== selected; item.isAllDescendantsSelected = allSelected; item.isSelected = selected; view.updateItem(item.id, item); if (selectedChange) { this.itemSelectedChanged(item); } } } } finally { view.endUpdate(); } } protected getDescendantsSelected(item: TItem): boolean { return true; } protected setAllSubTreeSelected(item: TItem, selected: boolean): boolean { var result = false; for (var i = 0; i < item.children.length; i++) { var sub = item.children[i]; if (sub.isSelected !== selected) { result = true; sub.isSelected = selected; this.view.updateItem(sub.id, sub as TItem); this.itemSelectedChanged(sub as TItem); } if (sub.children.length > 0) { result = this.setAllSubTreeSelected(sub as TItem, selected) || result; } } return result; } protected allItemsSelected() { for (var i = 0; i < this.rowCount(); i++) { var row = this.itemAt(i); if (!row.isSelected) { return false; } } return this.rowCount() > 0; } protected allDescendantsSelected(item: TItem): boolean { if (item.children.length > 0) { for (var i = 0; i < item.children.length; i++) { var sub = item.children[i]; if (!sub.isSelected) { return false; } if (!this.allDescendantsSelected(sub as TItem)) { return false; } } } return true; } protected getDelimited() { return !!!!(this.options as any)['delimited']; } protected anyDescendantsSelected(item: TItem): boolean { if (item.children.length > 0) { for (var i = 0; i < item.children.length; i++) { var sub = item.children[i]; if (sub.isSelected) { return true; } if (this.anyDescendantsSelected(sub as TItem)) { return true; } } } return false; } protected getColumns(): Column[] { var self = this; var columns: Column[] = []; columns.push({ field: 'text', name: 'Record', width: 80, format: SlickFormatting.treeToggle(function () { return self.view; }, function (x) { return x.id; }, ctx => { var cls = 'check-box'; var item = ctx.item; if (item.hideCheckBox) { return this.getItemText(ctx); } var threeState = this.isThreeStateHierarchy(); if (item.isSelected) { if (threeState && !item.isAllDescendantsSelected) { cls += ' partial'; } else { cls += ' checked'; } } if (this._readOnly) cls += ' readonly'; return '<span class="' + cls + '"></span>' + this.getItemText(ctx); }) }); return columns; } protected getItemText(ctx: FormatterContext): string { return ctx.escape(); } protected getSlickOptions(): GridOptions { var opt = super.getSlickOptions(); opt.forceFitColumns = true; return opt; } protected sortItems(): void { if (!this.moveSelectedUp()) { return; } var oldIndexes: Record<string, number> = {}; var list = this.view.getItems(); var i = 0; for (var $t1 = 0; $t1 < list.length; $t1++) { var x = list[$t1]; oldIndexes[x.id] = i++; } list.sort(function (x1, y) { if (x1.isSelected && !y.isSelected) { return -1; } if (y.isSelected && !x1.isSelected) { return 1; } var c = Culture.stringCompare(x1.text, y.text); if (c !== 0) { return c; } return oldIndexes[x1.id] < oldIndexes[y.id] ? -1 : (oldIndexes[x1.id] > oldIndexes[y.id] ? 1 : 0); }); this.view.setItems(list, true); } protected moveSelectedUp(): boolean { return false; } declare private _readOnly: boolean; public get_readOnly() { return this._readOnly; } public set_readOnly(value: boolean) { if (!!this._readOnly != !!value) { this._readOnly = !!value; this.view.refresh(); } } private get_value(): string[] { var list = []; var items = this.view.getItems(); for (var i = 0; i < items.length; i++) { if (items[i].isSelected) { list.push(items[i].id); } } return list; } public get value() { return this.get_value(); } private set_value(value: string | string[]) { var selected: Record<string, boolean> = {}; if (value != null) { if (typeof value == "string") { value = value.split(',') .map(x => x?.trim()) .filter(x => !!x); } for (var i = 0; i < value.length; i++) { selected[value[i]] = true; } } this.view.beginUpdate(); try { var items = this.view.getItems(); for (var i1 = 0; i1 < items.length; i1++) { var item = items[i1]; var select = selected[item.id]; if (select !== item.isSelected) { item.isSelected = select; this.view.updateItem(item.id, item); } } this.updateSelectAll(); this.updateFlags(); this.sortItems(); } finally { this.view.endUpdate(); } } public set value(v: string[]) { this.set_value(v); } } export interface CheckLookupEditorOptions { lookupKey?: string; checkedOnTop?: boolean; showSelectAll?: boolean; hideSearch?: boolean; delimited?: boolean; cascadeFrom?: string; cascadeField?: string; cascadeValue?: any; filterField?: string; filterValue?: any; } export class CheckLookupEditor<TItem extends CheckTreeItem<TItem> = any, P extends CheckLookupEditorOptions = CheckLookupEditorOptions> extends CheckTreeEditor<CheckTreeItem<TItem>, P> { static [Symbol.typeInfo] = this.registerEditor(nsSerenity); declare private searchText: string; declare private enableUpdateItems: boolean; declare private lookupChangeUnbind: any; constructor(props: EditorProps<P>) { super(props); this.enableUpdateItems = true; this.setCascadeFrom(this.options.cascadeFrom); this.updateItems(); this.lookupChangeUnbind = ScriptData.bindToChange('Lookup.' + this.getLookupKey(), this.updateItems.bind(this)); } public destroy(): void { if (this.lookupChangeUnbind) { this.lookupChangeUnbind(); this.lookupChangeUnbind = null; } super.destroy(); } protected updateItems() { if (this.enableUpdateItems) super.updateItems(); } protected getLookupKey() { return this.options.lookupKey; } protected getButtons(): ToolButton[] { return super.getButtons() ?? (this.options.hideSearch ? null : []); } protected createToolbarExtensions() { super.createToolbarExtensions(); GridUtils.addQuickSearchInputCustom(this.toolbar.domNode, (field, text) => { this.searchText = stripDiacritics(text || '').toUpperCase(); this.view.setItems(this.view.getItems(), true); }); } protected getSelectAllText(): string { if (!this.options.showSelectAll) return null; return super.getSelectAllText(); } protected cascadeItems(items: TItem[]) { var val = this.get_cascadeValue(); if (val == null || val === '') { if (this.get_cascadeField()) { return []; } return items; } var key = val.toString(); var fld = this.get_cascadeField(); return items.filter(x => { var itemKey = (x as any)[fld] ?? ReflectionUtils.getPropertyValue(x, fld); return !!(itemKey != null && itemKey.toString() === key); }); } protected filterItems(items: TItem[]) { var val = this.get_filterValue(); if (val == null || val === '') { return items; } var key = val.toString(); var fld = this.get_filterField(); return items.filter(x => { var itemKey = (x as any)[fld] ?? ReflectionUtils.getPropertyValue(x, fld); return !!(itemKey != null && itemKey.toString() === key); }); } protected getLookupItems(lookup: Lookup<TItem>): TItem[] { return this.filterItems(this.cascadeItems(lookup.items)); } protected getTreeItems() { var lookup = getLookup<TItem>(this.options.lookupKey); var items = this.getLookupItems(lookup); return items.map(item => <CheckTreeItem<TItem>>{ id: ((item as any)[lookup.idField] ?? "").toString(), text: ((item as any)[lookup.textField] ?? "").toString(), source: item }); } protected onViewFilter(item: CheckTreeItem<TItem>) { return super.onViewFilter(item) && (!this.searchText || stripDiacritics(item.text || '') .toUpperCase().indexOf(this.searchText) >= 0); } protected moveSelectedUp(): boolean { return this.options.checkedOnTop; } protected get_cascadeFrom(): string { return this.options.cascadeFrom; } get cascadeFrom(): string { return this.get_cascadeFrom(); } protected getCascadeFromValue(parent: Widget<any>) { return EditorUtils.getValue(parent); } declare protected cascadeLink: CascadedWidgetLink<Widget<any>>; protected setCascadeFrom(value: string) { if (!value) { if (this.cascadeLink != null) { this.cascadeLink.set_parentID(null); this.cascadeLink = null; } this.options.cascadeFrom = null; return; } this.cascadeLink = new CascadedWidgetLink<Widget<any>>(Widget, this, p => { this.set_cascadeValue(this.getCascadeFromValue(p)); }); this.cascadeLink.set_parentID(value); this.options.cascadeFrom = value; } protected set_cascadeFrom(value: string) { if (value !== this.options.cascadeFrom) { this.setCascadeFrom(value); this.updateItems(); } } set cascadeFrom(value: string) { this.set_cascadeFrom(value); } protected get_cascadeField() { return (this.options.cascadeField ?? this.options.cascadeFrom); } get cascadeField(): string { return this.get_cascadeField(); } protected set_cascadeField(value: string) { this.options.cascadeField = value; } set cascadeField(value: string) { this.set_cascadeField(value); } protected get_cascadeValue(): any { return this.options.cascadeValue; } get cascadeValue(): any { return this.get_cascadeValue(); } protected set_cascadeValue(value: any) { if (this.options.cascadeValue !== value) { this.options.cascadeValue = value; this.value = []; this.updateItems(); } } set cascadeValue(value: any) { this.set_cascadeValue(value); } protected get_filterField() { return this.options.filterField; } get filterField(): string { return this.get_filterField(); } protected set_filterField(value: string) { this.options.filterField = value; } set filterField(value: string) { this.set_filterField(value); } protected get_filterValue(): any { return this.options.filterValue; } get filterValue(): any { return this.get_filterValue(); } protected set_filterValue(value: any) { if (this.options.filterValue !== value) { this.options.filterValue = value; this.value = null; this.updateItems(); } } set filterValue(value: any) { this.set_filterValue(value); } }