@dewesoft-web/grid
Version:
Dewesoft WebUI Grid
807 lines (633 loc) • 22.3 kB
text/typescript
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,
}
export class DSGridModel extends ControlModel<GridEvent> {
rows : GridRowModel[];
columns : GridColumns;
showGroups : boolean = true;
groupByColumn : string;
private sortedColumn : string;
private sortDirection : SortDirecton;
searchQuery : string;
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();
}
}
});
}
get searchIcon() {
if (this.searchQuery.length === 0) {
return "search";
}
else {
return "highlight_off";
}
}
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;
}
get columnProperties() : string[] {
return Object.keys(this.columns);
}
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);
}
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);
}
get groups() : GridGroupModel[] {
return this.groupNames.map((group) => {
return new GridGroupModel(group, this.onGroupEvent);
});
}
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
});
}
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");
// }
}
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();
}
}
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();
}
}
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;
}
}
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);
}
}
}
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");
}
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");
}
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);
}
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})`);
}
}
unselectRows(except : GridRowModel) {
const length = this.selectedRows.length;
for (let i = 0; i < length; i++) {
if (this.selectedRows[i] !== except) {
this.selectedRows[i].deselect();
}
}
}
unselectAllRows() {
const length = this.rows.length;
for (let i = 0; i < length; i++) {
this.rows[i].deselect();
}
}
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])
});
}
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()
};
})
});
}
setGroupBy(col : string, noEvent? : boolean) {
this.groupByColumn = col;
this.sortRows();
if (noEvent) {
return;
}
this.triggerEvent(GridEvent.GroupByChanged, {
column: col
});
}
setShowGroups(show : boolean) {
if (this.showGroups === show) {
return;
}
this.showGroups = show;
this.triggerEvent(GridEvent.ShowGroupsChanged, {
show: show
});
}
endSelect() {
this.isSelecting = false;
}
setSelectedColumnData(value : any) {
for (const row of this.selectedRows) {
row.setValue(this.selectedColumn.property, value);
}
}
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;
}