UNPKG

@dewesoft-web/grid

Version:

Dewesoft WebUI Grid

807 lines (633 loc) 22.3 kB
import { observable, computed, action, toJS, autorun, autorunAsync } from "mobx"; import * as CssSelectorGenerator from "css-selector-generator"; import * as _ from "lodash"; import "mark.js"; import { WebEvent, WebControl, DSMessage, EventArguments } from "@dewesoft-web/ui/events"; import { GridRowModel } from "./GridRowModel"; import { GridColumnModel } from "./GridColumnModel"; import { GridGroupModel, GroupEvent } from "./GridGroupModel"; import { Column, GridColumns, GroupedRows, Row, SelectOption, GridGroups, SortDirecton } from "../types"; import { objectToArray } from "../util"; import { ControlModel } from "@dewesoft-web/ui/controls/ControlModel"; export declare class Mark { constructor(context : HTMLElement | string); mark(sv, opt?) : Mark; unmark(opt?) : Mark; } // export declare class CssSelectorMaker { constructor(opts?); getSelector(el) : string; } interface Map { [key : string] : number; } interface Element { scrollIntoViewIfNeeded(optCenter? : boolean); } enum GridEvent { Initialized, Initializing, ColumnChanged, GroupsChanged, GroupChanged, RowChanged, SelectedColumnChanged, ShowGroupsChanged, RowOrderChanged, GroupByChanged, UnselectRows, SelectRows, SelectRow, RemoveRow, SetColumn, AddRow, Clear, } @DSMessage(["dsGrid", "onEvent"]) export class DSGridModel extends ControlModel<GridEvent> { @observable rows : GridRowModel[]; @observable columns : GridColumns; @observable showGroups : boolean = true; @observable groupByColumn : string; @observable private sortedColumn : string; @observable private sortDirection : SortDirecton; @observable searchQuery : string; @observable selectedColumn : GridColumnModel; private selectedRows : GridRowModel[]; private idToIndex : Map; private isSelecting : boolean; private rowGroups : GridGroups; private selectionStart : number; private selectionEnd : number; private editingRow : GridRowModel; private isInitialized : boolean; private marker : Mark; private markOptions : Object; private selectorGenerator : CssSelectorMaker; private selector : string; constructor(rows : Row[], cols : Column[], name : string, onInit? : (model : DSGridModel) => void) { super(WebControl.Grid, name); this.isInitialized = false; this.editingRow = undefined; this.selectionStart = -1; this.selectionEnd = -1; this.groupByColumn = "GroupName"; this.rowGroups = null; this.searchQuery = ""; this.marker = null; this.markOptions = { separateWordSearch: false, caseSensitive: false }; this.idToIndex = {}; this.selectedRows = []; this.selectorGenerator = new CssSelectorGenerator(); this.onRowEvent = this.onRowEvent.bind(this); this.onColumnEvent = this.onColumnEvent.bind(this); this.onGroupEvent = this.onGroupEvent.bind(this); this.sortRows = this.sortRows.bind(this); this.setElement = this.setElement.bind(this); this.startSelect = this.startSelect.bind(this); this.endSelect = this.endSelect.bind(this); this.onTrySelect = this.onTrySelect.bind(this); this.onRowSelect = this.onRowSelect.bind(this); this.editCell = this.editCell.bind(this); this.setSelectedColumnData = this.setSelectedColumnData.bind(this); this.selectWholeColumn = this.selectWholeColumn.bind(this); if (cols) { this.columns = {}; const colNum = cols.length; for (let i = 0; i < colNum; i++) { this.columns[cols[i].Property] = new GridColumnModel(cols[i], i, this.onColumnEvent); } } else { this.columns = {}; } if (rows) { this.rows = rows.map((row : Row) => { return new GridRowModel(row, this.columnProperties, this.onRowEvent); }); this.sortRows(); } else { this.rows = []; } this.triggerEvent(GridEvent.Initializing, { rows: toJS(this.rows), cols: objectToArray(toJS(this.columns)) }); autorun(() => { this.rowGroups = {} as any; for (const group of this.groups) { this.rowGroups[group.name] = group; } this.isInitialized = true; if (onInit) { onInit(this); } this.triggerEvent(GridEvent.GroupsChanged, { event: GroupEvent.Initialize, groups: this.rowGroups }); }); autorunAsync(() => { const query = this.searchQuery; if (!this.marker) { return; } if (query) { for (const row of this.rows) { row.hideNotMatching(this.searchQuery); } this.marker.unmark(); this.marker.mark(this.searchQuery, this.markOptions); } else { this.marker.unmark(); for (const row of this.rows) { row.forceVisible(); } } }); } @computed get searchIcon() { if (this.searchQuery.length === 0) { return "search"; } else { return "highlight_off"; } } @computed get sortedColumns() : GridColumnModel[] { const colNames = Object.keys(this.columns); const numColumns = colNames.length; const columns : GridColumnModel[] = new Array(numColumns); for (let i = 0; i < colNames.length; i++) { columns[i] = this.columns[colNames[i]]; } columns.sort(function(lhs, rhs) { return lhs.order - rhs.order; }); return columns; } @computed get columnProperties() : string[] { return Object.keys(this.columns); } @computed get rowsByGroup() : GroupedRows { const groupedRows = _.groupBy(this.rows, "data." + this.groupByColumn + ".value"); if (this.sortDirection === SortDirecton.None || !this.sortedColumn) { return groupedRows; } else if (this.sortDirection === SortDirecton.Ascending) { // noinspection TsLint for (const group in groupedRows) { groupedRows[group] = _.sortBy(groupedRows[group], (row : GridRowModel) => { return row.getValue(this.sortedColumn); }); } } else if (this.sortDirection === SortDirecton.Descending) { // noinspection TsLint for (const g in groupedRows) { groupedRows[g] = _.sortBy(groupedRows[g], (row : GridRowModel) => { return row.getValue(this.sortedColumn); }).reverse(); } } return groupedRows; } setElement(el) { this.selector = this.selectorGenerator.getSelector(el) + " tbody.data"; this.marker = new Mark(this.selector); } @action sortColumn(column : string) { if (this.sortedColumn !== column) { this.sortedColumn = column; this.sortDirection = SortDirecton.Ascending; return; } if (this.sortDirection === SortDirecton.Descending) { this.sortDirection = SortDirecton.Ascending; } else if (this.sortDirection === SortDirecton.Ascending) { this.sortDirection = SortDirecton.Descending; } else { // console.log("Should never come here"); } } unsortColumns() { this.sortedColumn = null; this.sortDirection = SortDirecton.None; } private get groupNames() { return Object.keys(this.rowsByGroup); } @computed get groups() : GridGroupModel[] { return this.groupNames.map((group) => { return new GridGroupModel(group, this.onGroupEvent); }); } @action private sortRows() { // console.log("Sorting rows ..."); const groupedRows = this.rowsByGroup; const groups = Object.keys(groupedRows); const sortedRows = new Array(this.rows.length); const keysToIndex = new Array(this.rows.length); let index = 0; for (const group of groups) { const groupRows = groupedRows[group]; const groupRowLen = groupRows.length; for (let i = 0; i < groupRowLen; i++) { keysToIndex[index] = groupRows[i].uniqueId; groupRows[i].setIndex(index); sortedRows[index++] = groupRows[i]; } } this.rows = sortedRows; this.triggerEvent(GridEvent.RowOrderChanged, { keys: keysToIndex }); } @action selectWholeColumn(col : string) { // this.selectedRows = []; // this.selectedRows = this.rows; // // if (this.selectedColumn) { // this.selectedColumn.setSelected(false); // } // // this.selectedColumn = this.columns[col]; // this.selectedColumn.setSelected(true); // // for (const row of this.selectedRows) { // row.select(); // } // if (!this.selectedColumn) { // return; // } // // const columnCells = document.querySelectorAll(`td[data-dsgrid-col='${this.selectedColumn.property}']`); // // for (let i = 0; i < columnCells.length; i++) { // columnCells.item(i).classList.add("selected_GridCell1QiS1"); // } } @action startSelect(row : GridRowModel, col : string) { for (const selectedRow of this.selectedRows) { selectedRow.deselect(); } this.selectionStart = row.getIndex(); this.selectedRows = [row]; this.isSelecting = true; row.first = true; row.last = true; if (row) { if (this.selectedColumn) { this.selectedColumn.setSelected(false); } this.selectedColumn = this.columns[col]; this.selectedColumn.setSelected(true); row.select(); } } @action onRowSelect(row : GridRowModel, col : string, selectToRow? : boolean) { if (col !== this.selectedColumn.property || this.columns[col].readOnly) { return; } if (row.selected) { if (this.selectedRows.length === 1) { return; } const index = this.selectedRows.indexOf(row); if (index !== -1) { this.selectedRows.splice(index, 1); row.deselect(); } } else if (selectToRow) { this.selectRows(this.selectionStart, row.getIndex()); if (this.selectedRows.length) { this.selectedRows[0].first = true; this.selectedRows[this.selectedRows.length - 1].last = true; } } else { this.selectedRows.push(row); row.select(); } } @action onTrySelect(row : GridRowModel) { if (!this.isSelecting || this.selectedColumn.readOnly) { return; } this.selectionEnd = row.getIndex(); if (this.selectedRows.length) { this.unselectRows(this.selectedRows[0]); } this.selectRows(this.selectionStart, this.selectionEnd); if (this.selectedRows.length) { this.selectedRows[0].first = true; this.selectedRows[this.selectedRows.length - 1].last = true; } } @action selectRows(startIndex : number, endIndex : number) { this.unselectAllRows(); this.selectedRows = []; if (endIndex < startIndex) { let temp = endIndex; endIndex = startIndex; startIndex = temp; } const numRows = this.rows.length; for (let index = 0; index < numRows; index++) { const select = (index >= startIndex) && (index <= endIndex); this.rows[index].setSelected(select); if (select) { this.selectedRows.push(this.rows[index]); } } } private scrollElementIntoView(selector : string, alignTop : boolean = false) { const element = document.querySelector(selector) as HTMLElement; if (!element) { return; } if (element.scrollIntoView) { const gridWrapperEl = document.querySelector(this.selector).parentElement.parentElement; const wrapperTop = gridWrapperEl.scrollTop; const wrapperHeight = gridWrapperEl.clientHeight - element.clientHeight; const elementTop = element.offsetTop; const elementBottom = elementTop + element.clientHeight; if (elementBottom > wrapperTop + wrapperHeight || elementTop < wrapperTop) { element.scrollIntoView(alignTop); } } } @action selectFirstRow() { this.unselectAllRows(); this.selectionStart = 0; this.selectionEnd = 0; const firstRow = this.rows[0]; this.selectedRows = [firstRow]; firstRow.select(); this.scrollElementIntoView(this.selector + " tr:first-of-type"); } @action selectLastRow() { this.unselectAllRows(); this.selectionStart = this.rows.length - 1; this.selectionEnd = this.rows.length - 1; const lastRow = this.rows[this.rows.length - 1]; if (lastRow) { this.selectedRows = [lastRow]; lastRow.select(); } this.scrollElementIntoView(this.selector + " tr:last-of-type"); } @action selectNextRow() { const length = this.rows.length - 1; if (this.selectionStart >= length) { return; } this.unselectAllRows(); const nextRow = this.rows[++this.selectionStart]; this.selectionEnd = this.selectionStart; if (nextRow) { this.selectedRows = [nextRow]; nextRow.select(); } this.scrollElementIntoView(`${this.selector} tr:nth-of-type(${this.selectionStart})`, true); } @action selectPreviousRow() { if (this.selectionStart < 0) { this.selectionStart = 0; return; } if (this.selectionStart === 0) { return; } this.unselectAllRows(); this.rows[this.selectionStart].deselect(); const prevRow = this.rows[--this.selectionStart]; this.selectionEnd = this.selectionStart; this.selectedRows = [prevRow]; prevRow.select(); if (this.selectionStart === 0) { this.selectFirstRow(); } else { this.scrollElementIntoView(`${this.selector} tr:nth-of-type(${this.selectionStart})`); } } @action unselectRows(except : GridRowModel) { const length = this.selectedRows.length; for (let i = 0; i < length; i++) { if (this.selectedRows[i] !== except) { this.selectedRows[i].deselect(); } } } @action unselectAllRows() { const length = this.rows.length; for (let i = 0; i < length; i++) { this.rows[i].deselect(); } } @action addRow(row : Row) { this.rows.push(new GridRowModel(row, this.columnProperties, this.onRowEvent)); this.triggerEvent(GridEvent.AddRow, { row: toJS(this.rows[this.rows.length - 1]) }); } @action removeRows(startIndex : number, count : number = 1) { // console.log(`removingRow, start: ${startIndex}, count: ${count}`); const removed = this.rows.splice(startIndex, count); this.sortRows(); this.triggerEvent(GridEvent.RemoveRow, { removedIds: removed.map(function(removedRow : GridRowModel) { return { uniqueId: removedRow.uniqueId, index: removedRow.getIndex() }; }) }); } @action setGroupBy(col : string, noEvent? : boolean) { this.groupByColumn = col; this.sortRows(); if (noEvent) { return; } this.triggerEvent(GridEvent.GroupByChanged, { column: col }); } @action setShowGroups(show : boolean) { if (this.showGroups === show) { return; } this.showGroups = show; this.triggerEvent(GridEvent.ShowGroupsChanged, { show: show }); } @action endSelect() { this.isSelecting = false; } @action setSelectedColumnData(value : any) { for (const row of this.selectedRows) { row.setValue(this.selectedColumn.property, value); } } @action editCell(row : GridRowModel, col : string) { if (!row.data[col]) { return; } if (row.data[col].type && row.data[col].type.readOnly) { return; } if (this.editingRow) { this.editingRow.setEditing(false, this.selectedColumn.property); } // console.log(`Store: edit ${col}`); this.editingRow = row; row.setEditing(true, col); } getColumnDescriptors() : SelectOption[] { const properties = this.columnProperties; const size = properties.length; const descriptions : SelectOption[] = new Array(size); for (let i = 0; i < size; i++) { descriptions[i] = { value: properties[i], description: this.columns[properties[i]].title }; } return descriptions; } onInitialized() { } protected triggerEvent(event : GridEvent, args : EventArguments) { if (event !== GridEvent.Initializing && !this.isInitialized) { return; } super.triggerEvent(event, args); } private onColumnEvent(column : GridColumnModel, event : number, args : EventArguments) { this.triggerEvent(GridEvent.ColumnChanged, { columnEvent: event, column: column.property, arguments: args }); } private onRowEvent(row : GridRowModel, event : number, args : EventArguments) { this.triggerEvent(GridEvent.RowChanged, { rowEvent: event, uniqueId: row.uniqueId, arguments: args }); } private onGroupEvent(group : GridGroupModel, event : number, args : EventArguments) { this.triggerEvent(GridEvent.GroupsChanged, { groupEvent: event, group: group.name, arguments: args }); } /* * C++ event handlers */ protected onEvent(event : GridEvent, name : string, ...args : any[]) { if (name !== this.name) { return; } switch (event) { case GridEvent.ColumnChanged: const [colProp, colEvent, colArgs] = args; const col = this.columns[colProp]; if (col) { col.onEvent(colEvent, ...colArgs); } break; case GridEvent.RowChanged: const [rowIdx, rowEvent, rowArgs] = args; (this.rows[rowIdx].onEvent as any)(rowEvent, ...rowArgs); break; case GridEvent.GroupsChanged: const [groupName, groupEvent, groupArgs] = args; const [groupEventName, groupArguments] = groupArgs; this.rowGroups[groupName].onEvent(groupEvent, ...groupArguments); break; case GridEvent.GroupByChanged: this.setGroupBy(args[0], true); break; case GridEvent.SetColumn: this.setColumn(args[0], args[1]); break; case GridEvent.AddRow: this.onAddRow(args[0], args[1], args[2]); break; case GridEvent.RemoveRow: this.removeRows(args[0]); break; case GridEvent.SelectRow: // this.selectSingleRowByIndex(args[0]); break; case GridEvent.SelectRows: this.selectRows(args[0], args[1]); break; case GridEvent.UnselectRows: this.unselectAllRows(); break; case GridEvent.Clear: this.rows = []; break; default: break; } } private onAddRow(rowIndex : number, addedRow : AddRow, uniqueId : string) { this.rows.splice(rowIndex, 0, new GridRowModel(addedRow, this.columnProperties, uniqueId, this.onRowEvent)); } private setColumn(property : string, column : any) { } } interface AddRow { rowIndex : number; data : any; }