From 9e012de20f00d0315054c9ae0b501aec6331e5cd Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 10 Oct 2019 14:41:41 -0400 Subject: [PATCH 01/36] cell editing initial commit --- packages/datagrid/src/basickeyhandler.ts | 32 ++ packages/datagrid/src/basicmousehandler.ts | 49 +- packages/datagrid/src/basicselectionmodel.ts | 22 + packages/datagrid/src/celleditor.ts | 471 +++++++++++++++++++ packages/datagrid/src/datagrid.ts | 47 ++ packages/datagrid/src/datamodel.ts | 5 + packages/datagrid/src/selectionmodel.ts | 2 + packages/datagrid/tsconfig.json | 1 + packages/default-theme/style/datagrid.css | 13 + 9 files changed, 640 insertions(+), 2 deletions(-) create mode 100644 packages/datagrid/src/celleditor.ts diff --git a/packages/datagrid/src/basickeyhandler.ts b/packages/datagrid/src/basickeyhandler.ts index 8adc0108d..890d67f57 100644 --- a/packages/datagrid/src/basickeyhandler.ts +++ b/packages/datagrid/src/basickeyhandler.ts @@ -20,6 +20,9 @@ import { import { SelectionModel } from './selectionmodel'; +import { ICellEditResponse } from './celleditor'; + +import { MutableDataModel } from './datamodel'; /** @@ -55,6 +58,29 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { * This will not be called if the mouse button is pressed. */ onKeyDown(grid: DataGrid, event: KeyboardEvent): void { + if (!event.shiftKey && !Platform.accelKey(event)) { + const inp = String.fromCharCode(event.keyCode); + if (/[a-zA-Z0-9-_ ]/.test(inp)) { + if (grid.selectionModel) { + const row = grid.selectionModel.cursorRow; + const column = grid.selectionModel.cursorColumn; + if (row != -1 && column != -1) { + grid.cellEditorController.edit({ + grid: grid, + row: row, + column: column, + metadata: grid.dataModel!.metadata('body', row, column) + }).then((response: ICellEditResponse) => { + if (grid.dataModel instanceof MutableDataModel) { + const dataModel = grid.dataModel as MutableDataModel; + dataModel.setData('body', row, column, response.value); + } + }); + } + } + } + } + switch (getKeyboardLayout().keyForKeydownEvent(event)) { case 'ArrowLeft': this.onArrowLeft(grid, event); @@ -80,6 +106,12 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { case 'C': this.onKeyC(grid, event); break; + case 'Enter': + if (grid.selectionModel) { + grid.selectionModel.incrementCursor(); + grid.scrollToCursor(); + } + break; } } diff --git a/packages/datagrid/src/basicmousehandler.ts b/packages/datagrid/src/basicmousehandler.ts index 79feea8ed..19c76461d 100644 --- a/packages/datagrid/src/basicmousehandler.ts +++ b/packages/datagrid/src/basicmousehandler.ts @@ -22,13 +22,13 @@ import { } from './datagrid'; import { - DataModel + DataModel, MutableDataModel } from './datamodel'; import { SelectionModel } from './selectionmodel'; - +import { ICellEditResponse } from './celleditor'; /** * A basic implementation of a data grid mouse handler. @@ -488,6 +488,51 @@ class BasicMouseHandler implements DataGrid.IMouseHandler { this.release(); } + /** + * Handle the mouse double click event for the data grid. + * + * @param grid - The data grid of interest. + * + * @param event - The mouse up event of interest. + */ + onMouseDoubleClick(grid: DataGrid, event: MouseEvent): void { + if (!grid.dataModel) { + this.release(); + return; + } + + // Unpack the event. + let { clientX, clientY } = event; + + // Hit test the grid. + let hit = grid.hitTest(clientX, clientY); + + // Unpack the hit test. + let { region, row, column } = hit; + + if (region === 'void') { + this.release(); + return; + } + + if (region === 'body') { + grid.cellEditorController + .edit({ + grid: grid, + row: row, + column: column, + metadata: grid.dataModel!.metadata('body', row, column)}) + .then((response: ICellEditResponse) => { + if (grid.dataModel instanceof MutableDataModel) { + const dataModel = grid.dataModel as MutableDataModel; + dataModel.setData('body', row, column, response.value); + } + }); + } + + this.release(); + } + /** * Handle the context menu event for the data grid. * diff --git a/packages/datagrid/src/basicselectionmodel.ts b/packages/datagrid/src/basicselectionmodel.ts index a01592819..d9ebbc82f 100644 --- a/packages/datagrid/src/basicselectionmodel.ts +++ b/packages/datagrid/src/basicselectionmodel.ts @@ -48,6 +48,28 @@ class BasicSelectionModel extends SelectionModel { return this._cursorColumn; } + incrementCursor(): void { + const newRow = this._cursorRow + 1; + if (this._selections.length === 1 && ( + this._selections[0].r1 === this._cursorRow && + this._selections[0].c1 === this._cursorColumn && + this._selections[0].r2 === this._cursorRow && + this._selections[0].c2 === this._cursorColumn + )) { + this._selections[0] = { + r1: newRow, + c1: this._cursorColumn, + r2: newRow, + c2: this._cursorColumn + }; + } + + this._cursorRow = newRow; + + // Emit the changed signal. + this.emitChanged(); + } + /** * Get the current selection in the selection model. * diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts new file mode 100644 index 000000000..199e94c70 --- /dev/null +++ b/packages/datagrid/src/celleditor.ts @@ -0,0 +1,471 @@ +import { + Widget +} from '@phosphor/widgets'; + +import { + DataGrid +} from './datagrid'; +import { DataModel } from './datamodel'; + +export +interface ICellEditResponse { + cell: CellEditor.CellConfig; + value: any; +}; + +export +class CellEditorController { + registerEditor(editor: CellEditor) { + this._editors.push(editor); + } + + edit(cell: CellEditor.CellConfig, validator?: any): Promise { + const numEditors = this._editors.length; + for (let i = numEditors - 1; i >= 0; --i) { + const editor = this._editors[i]; + if (editor.canEdit(cell)) { + return editor.edit(cell); + } + } + + const data = cell.grid.dataModel ? cell.grid.dataModel.data('body', cell.row, cell.column) : undefined; + if (!data || typeof data !== 'object') { + const editor = new TextCellEditor(); + return editor.edit(cell); + } + + return new Promise((resolve, reject) => { + reject('Editor not found'); + }); + } + + private _editors: CellEditor[] = []; +} + +export +abstract class CellEditor { + abstract canEdit(cell: CellEditor.CellConfig): boolean; + abstract edit(cell: CellEditor.CellConfig): Promise; + + getCellInfo(cell: CellEditor.CellConfig) { + const { grid, row, column } = cell; + const data = grid.dataModel!.data('body', row, column); + + const columnX = grid.headerWidth - grid.scrollX + grid.columnOffset('body', column); + const rowY = grid.headerHeight - grid.scrollY + grid.rowOffset('body', row); + const width = grid.columnSize('body', column); + const height = grid.rowSize('body', row); + + return { + grid: grid, + row: row, + column: column, + data: data, + x: columnX, + y: rowY, + width: width, + height: height + }; + } + + updatePosition() { + } + + endEditing() { + } +} + +export +class TextCellEditor extends CellEditor { + canEdit(cell: CellEditor.CellConfig): boolean { + const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : {}; + return metadata.type === 'string'; + } + + edit(cell: CellEditor.CellConfig): Promise { + return new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + + this._cell = cell; + const grid = cell.grid; + const cellInfo = this.getCellInfo(cell); + + const input = document.createElement('input'); + input.classList.add('cell-editor'); + input.classList.add('input-cell-editor'); + input.spellcheck = false; + + input.value = cellInfo.data; + + this._input = input; + + grid.node.appendChild(input); + + this.updatePosition(); + + input.select(); + + input.addEventListener("keydown", (event) => { + this._onKeyDown(event); + }); + + input.addEventListener("blur", (event) => { + this._saveInput(); + this.endEditing(); + }); + + grid.node.addEventListener('wheel', () => { + this.updatePosition(); + }); + }); + } + + _onKeyDown(event: KeyboardEvent) { + switch (event.keyCode) { + case 13: // return + this._saveInput(); + this.endEditing(); + this._cell.grid.selectionModel!.incrementCursor(); + this._cell.grid.scrollToCursor(); + break; + case 27: // escape + this.endEditing(); + break; + default: + break; + } + } + + _isCellFullyVisible(cellInfo: any): boolean { + const grid = cellInfo.grid; + + return cellInfo.x >= grid.headerWidth && (cellInfo.x + cellInfo.width) <= grid.viewportWidth + 1 && + cellInfo.y >= grid.headerHeight && (cellInfo.y + cellInfo.height) <= grid.viewportHeight + 1; + } + + _saveInput() { + if (!this._input) { + return; + } + + this._resolve({ cell: this._cell, value: this._input!.value }); + } + + updatePosition() { + if (!this._input) { + return; + } + + const cellInfo = this.getCellInfo(this._cell); + const input = this._input; + + if (!this._isCellFullyVisible(cellInfo)) { + input.style.visibility = 'hidden'; + } else { + input.style.left = (cellInfo.x - 1) + 'px'; + input.style.top = (cellInfo.y - 1) + 'px'; + input.style.width = (cellInfo.width + 1) + 'px'; + input.style.height = (cellInfo.height + 1) + 'px'; + input.style.visibility = 'visible'; + input.focus(); + } + } + + endEditing() { + if (!this._input) { + return; + } + + this._input.style.display = 'none'; + //this._input.remove(); + this._input = null; + this._cell.grid.viewport.node.focus(); + } + + _resolve: {(response: ICellEditResponse): void }; + _reject: {(reason: any): void }; + _cell: CellEditor.CellConfig; + _input: HTMLInputElement | null; +} + +export +class EnumCellEditor extends CellEditor { + canEdit(cell: CellEditor.CellConfig): boolean { + const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : {}; + return metadata.constraint && metadata.constraint.enum; + } + + edit(cell: CellEditor.CellConfig): Promise { + return new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + + this._cell = cell; + const grid = cell.grid; + const cellInfo = this.getCellInfo(cell); + const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); + const items = metadata.constraint.enum; + + const select = document.createElement('select'); + select.classList.add('cell-editor'); + select.classList.add('select-cell-editor'); + for (let item of items) { + const option = document.createElement("option"); + option.value = item; + option.text = item; + select.appendChild(option); + } + + select.value = cellInfo.data; + + this._select = select; + + grid.node.appendChild(select); + + this.updatePosition(); + + select.addEventListener("keydown", (event) => { + this._onKeyDown(event); + }); + + select.addEventListener("blur", (event) => { + this._saveInput(); + this.endEditing(); + }); + + select.addEventListener("change", (event) => { + this._saveInput(); + this.endEditing(); + }); + + grid.node.addEventListener('wheel', () => { + this.updatePosition(); + }); + }); + } + + _onKeyDown(event: KeyboardEvent) { + switch (event.keyCode) { + case 13: // return + this._saveInput(); + this.endEditing(); + this._cell.grid.selectionModel!.incrementCursor(); + this._cell.grid.scrollToCursor(); + break; + case 27: // escape + this.endEditing(); + break; + default: + break; + } + } + + _isCellFullyVisible(cellInfo: any): boolean { + const grid = cellInfo.grid; + + return cellInfo.x >= grid.headerWidth && (cellInfo.x + cellInfo.width) <= grid.viewportWidth + 1 && + cellInfo.y >= grid.headerHeight && (cellInfo.y + cellInfo.height) <= grid.viewportHeight + 1; + } + + _saveInput() { + if (!this._select) { + return; + } + + this._resolve({ cell: this._cell, value: this._select!.value }); + } + + updatePosition() { + if (!this._select) { + return; + } + + const cellInfo = this.getCellInfo(this._cell); + const input = this._select; + + if (!this._isCellFullyVisible(cellInfo)) { + input.style.visibility = 'hidden'; + } else { + input.style.left = (cellInfo.x - 1) + 'px'; + input.style.top = (cellInfo.y - 1) + 'px'; + input.style.width = (cellInfo.width + 1) + 'px'; + input.style.height = (cellInfo.height + 1) + 'px'; + input.style.visibility = 'visible'; + input.focus(); + } + } + + endEditing() { + if (!this._select) { + return; + } + + this._select.style.display = 'none'; + //this._select.remove(); + this._select = null; + this._cell.grid.viewport.node.focus(); + } + + _resolve: {(response: ICellEditResponse): void }; + _reject: {(reason: any): void }; + _cell: CellEditor.CellConfig; + _select: HTMLSelectElement | null; +} + +export +class DateCellEditor extends CellEditor { + canEdit(cell: CellEditor.CellConfig): boolean { + const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : {}; + return metadata.type === 'date'; + } + + edit(cell: CellEditor.CellConfig): Promise { + return new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + + this._cell = cell; + const grid = cell.grid; + const cellInfo = this.getCellInfo(cell); + + const input = document.createElement('input'); + input.type = 'date'; + input.pattern = "\d{4}-\d{2}-\d{2}"; + input.classList.add('cell-editor'); + input.classList.add('input-cell-editor'); + input.spellcheck = false; + + input.value = cellInfo.data; + + this._input = input; + + grid.node.appendChild(input); + + this.updatePosition(); + + input.select(); + + input.addEventListener("keydown", (event) => { + this._onKeyDown(event); + }); + + input.addEventListener("blur", (event) => { + this._saveInput(); + this.endEditing(); + }); + + input.addEventListener("change", (event) => { + this._saveInput(); + //this.endEditing(); + }); + + grid.node.addEventListener('wheel', () => { + this.updatePosition(); + }); + }); + } + + _onKeyDown(event: KeyboardEvent) { + switch (event.keyCode) { + case 13: // return + this._saveInput(); + this.endEditing(); + this._cell.grid.selectionModel!.incrementCursor(); + this._cell.grid.scrollToCursor(); + break; + case 27: // escape + this.endEditing(); + break; + default: + break; + } + } + + _isCellFullyVisible(cellInfo: any): boolean { + const grid = cellInfo.grid; + + return cellInfo.x >= grid.headerWidth && (cellInfo.x + cellInfo.width) <= grid.viewportWidth + 1 && + cellInfo.y >= grid.headerHeight && (cellInfo.y + cellInfo.height) <= grid.viewportHeight + 1; + } + + _saveInput() { + if (!this._input) { + return; + } + + this._resolve({ cell: this._cell, value: this._input!.value }); + } + + updatePosition() { + if (!this._input) { + return; + } + + const cellInfo = this.getCellInfo(this._cell); + const input = this._input; + + if (!this._isCellFullyVisible(cellInfo)) { + input.style.visibility = 'hidden'; + } else { + input.style.left = (cellInfo.x - 1) + 'px'; + input.style.top = (cellInfo.y - 1) + 'px'; + input.style.width = (cellInfo.width + 1) + 'px'; + input.style.height = (cellInfo.height + 1) + 'px'; + input.style.visibility = 'visible'; + input.focus(); + } + } + + endEditing() { + if (!this._input) { + return; + } + + this._input.style.display = 'none'; + //this._input.remove(); + this._input = null; + this._cell.grid.viewport.node.focus(); + } + + _resolve: {(response: ICellEditResponse): void }; + _reject: {(reason: any): void }; + _cell: CellEditor.CellConfig; + _input: HTMLInputElement | null; +} + +export +namespace CellEditor { + export + interface IOptions extends Widget.IOptions { + grid: DataGrid; + row: number; + column: number; + } + + export + interface IInputOptions extends IOptions { + } + + /** + * An object which holds the configuration data for a cell. + */ + export + type CellConfig = { + readonly grid: DataGrid; + /** + * The row index of the cell. + */ + readonly row: number; + + /** + * The column index of the cell. + */ + readonly column: number; + + /** + * The metadata for the cell. + */ + readonly metadata: DataModel.Metadata; + }; +} diff --git a/packages/datagrid/src/datagrid.ts b/packages/datagrid/src/datagrid.ts index 475e9f8c1..717dcf964 100644 --- a/packages/datagrid/src/datagrid.ts +++ b/packages/datagrid/src/datagrid.ts @@ -48,6 +48,7 @@ import { import { SelectionModel } from './selectionmodel'; +import { CellEditorController, TextCellEditor, EnumCellEditor, DateCellEditor } from './celleditor'; /** @@ -127,6 +128,11 @@ class DataGrid extends Widget { this._hScrollBar = new ScrollBar({ orientation: 'horizontal' }); this._scrollCorner = new Widget(); + this._cellEditorController = new CellEditorController(); + this._cellEditorController.registerEditor(new TextCellEditor()); + this._cellEditorController.registerEditor(new EnumCellEditor()); + this._cellEditorController.registerEditor(new DateCellEditor()); + // Add the extra class names to the child widgets. this._viewport.addClass('p-DataGrid-viewport'); this._vScrollBar.addClass('p-DataGrid-scrollBar'); @@ -1751,6 +1757,9 @@ class DataGrid extends Widget { case 'mouseup': this._evtMouseUp(event as MouseEvent); break; + case 'dblclick': + this._evtMouseDoubleClick(event as MouseEvent); + break; case 'mouseleave': this._evtMouseLeave(event as MouseEvent); break; @@ -1782,6 +1791,7 @@ class DataGrid extends Widget { this._viewport.node.addEventListener('keydown', this); this._viewport.node.addEventListener('mousedown', this); this._viewport.node.addEventListener('mousemove', this); + this._viewport.node.addEventListener('dblclick', this); this._viewport.node.addEventListener('mouseleave', this); this._viewport.node.addEventListener('contextmenu', this); this._repaintContent(); @@ -1798,6 +1808,7 @@ class DataGrid extends Widget { this._viewport.node.removeEventListener('mousedown', this); this._viewport.node.removeEventListener('mousemove', this); this._viewport.node.removeEventListener('mouseleave', this); + this._viewport.node.removeEventListener('dblclick', this); this._viewport.node.removeEventListener('contextmenu', this); this._releaseMouse(); } @@ -2761,6 +2772,28 @@ class DataGrid extends Widget { this._releaseMouse(); } + /** + * Handle the `'dblclick'` event for the data grid. + */ + private _evtMouseDoubleClick(event: MouseEvent): void { + // Ignore everything except the left mouse button. + if (event.button !== 0) { + return; + } + + // Stop the event propagation. + event.preventDefault(); + event.stopPropagation(); + + // Dispatch to the mouse handler. + if (this._mouseHandler) { + this._mouseHandler.onMouseDoubleClick(this, event); + } + + // Release the mouse. + this._releaseMouse(); + } + /** * Handle the `'mouseleave'` event for the data grid. */ @@ -4965,6 +4998,10 @@ class DataGrid extends Widget { gc.restore(); } + get cellEditorController(): CellEditorController { + return this._cellEditorController; + } + private _viewport: Widget; private _vScrollBar: ScrollBar; private _hScrollBar: ScrollBar; @@ -5005,6 +5042,7 @@ class DataGrid extends Widget { private _cellRenderers: RendererMap; private _copyConfig: DataGrid.CopyConfig; private _headerVisibility: DataGrid.HeaderVisibility; + private _cellEditorController: CellEditorController; } @@ -5390,6 +5428,15 @@ namespace DataGrid { */ onMouseUp(grid: DataGrid, event: MouseEvent): void; + /** + * Handle the mouse double click event for the data grid. + * + * @param grid - The data grid of interest. + * + * @param event - The mouse double click event of interest. + */ + onMouseDoubleClick(grid: DataGrid, event: MouseEvent): void; + /** * Handle the context menu event for the data grid. * diff --git a/packages/datagrid/src/datamodel.ts b/packages/datagrid/src/datamodel.ts index 36b63f3ca..9275fca89 100644 --- a/packages/datagrid/src/datamodel.ts +++ b/packages/datagrid/src/datamodel.ts @@ -104,6 +104,11 @@ abstract class DataModel { private _changed = new Signal(this); } +export +abstract class MutableDataModel extends DataModel { + abstract setData(region: DataModel.CellRegion, row: number, column: number, value: any): boolean; +} + /** * The namespace for the `DataModel` class statics. diff --git a/packages/datagrid/src/selectionmodel.ts b/packages/datagrid/src/selectionmodel.ts index 3e0dc553e..e770d4db3 100644 --- a/packages/datagrid/src/selectionmodel.ts +++ b/packages/datagrid/src/selectionmodel.ts @@ -60,6 +60,8 @@ abstract class SelectionModel { */ abstract readonly cursorColumn: number; + abstract incrementCursor(): void; + /** * Get the current selection in the selection model. * diff --git a/packages/datagrid/tsconfig.json b/packages/datagrid/tsconfig.json index 7d06928c3..51d3c4738 100644 --- a/packages/datagrid/tsconfig.json +++ b/packages/datagrid/tsconfig.json @@ -12,6 +12,7 @@ "outDir": "lib", "lib": [ "ES5", + "es2015", "DOM" ], "types": [], diff --git a/packages/default-theme/style/datagrid.css b/packages/default-theme/style/datagrid.css index 4916da380..59f8afdda 100644 --- a/packages/default-theme/style/datagrid.css +++ b/packages/default-theme/style/datagrid.css @@ -27,3 +27,16 @@ height: 1px; background-color: #A0A0A0; } + +.cell-editor { + position: absolute; + box-sizing: border-box; + outline: none; + box-shadow: 0px 0px 6px #006bf7; + border: 2px solid #006bf7; +} + +.input-cell-editor { + padding: 0 0 1px 0; + background-color: #ffffff; +} From 3c9a6d6ca62eb1aeb983dc1de25e1eaaaa07ad64 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 10 Oct 2019 16:22:55 -0400 Subject: [PATCH 02/36] validator support, refactor --- packages/datagrid/src/celleditor.ts | 264 ++++++++++++---------- packages/default-theme/style/datagrid.css | 5 + 2 files changed, 145 insertions(+), 124 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 199e94c70..47dc99a50 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -13,25 +13,30 @@ interface ICellEditResponse { value: any; }; +export +interface ICellInputValidator { + validate(cell: CellEditor.CellConfig, value: any): boolean; +}; + export class CellEditorController { registerEditor(editor: CellEditor) { this._editors.push(editor); } - edit(cell: CellEditor.CellConfig, validator?: any): Promise { + edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): Promise { const numEditors = this._editors.length; for (let i = numEditors - 1; i >= 0; --i) { const editor = this._editors[i]; if (editor.canEdit(cell)) { - return editor.edit(cell); + return editor.edit(cell, validator); } } const data = cell.grid.dataModel ? cell.grid.dataModel.data('body', cell.row, cell.column) : undefined; if (!data || typeof data !== 'object') { const editor = new TextCellEditor(); - return editor.edit(cell); + return editor.edit(cell, validator); } return new Promise((resolve, reject) => { @@ -45,9 +50,23 @@ class CellEditorController { export abstract class CellEditor { abstract canEdit(cell: CellEditor.CellConfig): boolean; - abstract edit(cell: CellEditor.CellConfig): Promise; - getCellInfo(cell: CellEditor.CellConfig) { + edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): Promise { + return new Promise((resolve, reject) => { + this._cell = cell; + this._validator = validator; + this._resolve = resolve; + this._reject = reject; + + cell.grid.node.addEventListener('wheel', () => { + this.updatePosition(); + }); + + this.startEdit(); + }); + } + + protected getCellInfo(cell: CellEditor.CellConfig) { const { grid, row, column } = cell; const data = grid.dataModel!.data('body', row, column); @@ -68,11 +87,14 @@ abstract class CellEditor { }; } - updatePosition() { - } + protected abstract startEdit(): void; + protected abstract updatePosition(): void; + protected abstract endEditing(): void; - endEditing() { - } + protected _resolve: {(response: ICellEditResponse): void }; + protected _reject: {(reason: any): void }; + protected _cell: CellEditor.CellConfig; + protected _validator: ICellInputValidator | undefined; } export @@ -82,52 +104,54 @@ class TextCellEditor extends CellEditor { return metadata.type === 'string'; } - edit(cell: CellEditor.CellConfig): Promise { - return new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; + startEdit() { + const cell = this._cell; + const grid = cell.grid; + const cellInfo = this.getCellInfo(cell); - this._cell = cell; - const grid = cell.grid; - const cellInfo = this.getCellInfo(cell); - - const input = document.createElement('input'); - input.classList.add('cell-editor'); - input.classList.add('input-cell-editor'); - input.spellcheck = false; + const form = document.createElement('form'); + const input = document.createElement('input'); + input.classList.add('cell-editor'); + input.classList.add('input-cell-editor'); + input.spellcheck = false; + input.required = false; - input.value = cellInfo.data; + input.value = cellInfo.data; - this._input = input; + this._input = input; - grid.node.appendChild(input); + form.appendChild(input); + grid.node.appendChild(form); - this.updatePosition(); + this.updatePosition(); - input.select(); + input.select(); - input.addEventListener("keydown", (event) => { - this._onKeyDown(event); - }); + input.addEventListener("keydown", (event) => { + this._onKeyDown(event); + }); - input.addEventListener("blur", (event) => { - this._saveInput(); + input.addEventListener("blur", (event) => { + if (this._saveInput()) { this.endEditing(); - }); + } else { + this._input!.focus(); + } + }); - grid.node.addEventListener('wheel', () => { - this.updatePosition(); - }); + input.addEventListener("input", (event) => { + this._input!.setCustomValidity(""); }); } _onKeyDown(event: KeyboardEvent) { switch (event.keyCode) { case 13: // return - this._saveInput(); - this.endEditing(); - this._cell.grid.selectionModel!.incrementCursor(); - this._cell.grid.scrollToCursor(); + if (this._saveInput()) { + this.endEditing(); + this._cell.grid.selectionModel!.incrementCursor(); + this._cell.grid.scrollToCursor(); + } break; case 27: // escape this.endEditing(); @@ -144,12 +168,21 @@ class TextCellEditor extends CellEditor { cellInfo.y >= grid.headerHeight && (cellInfo.y + cellInfo.height) <= grid.viewportHeight + 1; } - _saveInput() { + _saveInput(): boolean { if (!this._input) { - return; + return false; + } + + const value = this._input.value; + if (this._validator && ! this._validator.validate(this._cell, value)) { + this._input.setCustomValidity("Invalid input"); + this._input.checkValidity(); + return false; } this._resolve({ cell: this._cell, value: this._input!.value }); + + return true; } updatePosition() { @@ -183,9 +216,6 @@ class TextCellEditor extends CellEditor { this._cell.grid.viewport.node.focus(); } - _resolve: {(response: ICellEditResponse): void }; - _reject: {(reason: any): void }; - _cell: CellEditor.CellConfig; _input: HTMLInputElement | null; } @@ -196,52 +226,43 @@ class EnumCellEditor extends CellEditor { return metadata.constraint && metadata.constraint.enum; } - edit(cell: CellEditor.CellConfig): Promise { - return new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - - this._cell = cell; - const grid = cell.grid; - const cellInfo = this.getCellInfo(cell); - const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); - const items = metadata.constraint.enum; - - const select = document.createElement('select'); - select.classList.add('cell-editor'); - select.classList.add('select-cell-editor'); - for (let item of items) { - const option = document.createElement("option"); - option.value = item; - option.text = item; - select.appendChild(option); - } - - select.value = cellInfo.data; + startEdit() { + const cell = this._cell; + const grid = cell.grid; + const cellInfo = this.getCellInfo(cell); + const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); + const items = metadata.constraint.enum; + + const select = document.createElement('select'); + select.classList.add('cell-editor'); + select.classList.add('select-cell-editor'); + for (let item of items) { + const option = document.createElement("option"); + option.value = item; + option.text = item; + select.appendChild(option); + } - this._select = select; + select.value = cellInfo.data; - grid.node.appendChild(select); + this._select = select; - this.updatePosition(); + grid.node.appendChild(select); - select.addEventListener("keydown", (event) => { - this._onKeyDown(event); - }); + this.updatePosition(); - select.addEventListener("blur", (event) => { - this._saveInput(); - this.endEditing(); - }); + select.addEventListener("keydown", (event) => { + this._onKeyDown(event); + }); - select.addEventListener("change", (event) => { - this._saveInput(); - this.endEditing(); - }); + select.addEventListener("blur", (event) => { + this._saveInput(); + this.endEditing(); + }); - grid.node.addEventListener('wheel', () => { - this.updatePosition(); - }); + select.addEventListener("change", (event) => { + this._saveInput(); + this.endEditing(); }); } @@ -273,7 +294,12 @@ class EnumCellEditor extends CellEditor { return; } - this._resolve({ cell: this._cell, value: this._select!.value }); + const value = this._select.value; + if (this._validator && ! this._validator.validate(this._cell, value)) { + return; + } + + this._resolve({ cell: this._cell, value: value }); } updatePosition() { @@ -307,9 +333,6 @@ class EnumCellEditor extends CellEditor { this._cell.grid.viewport.node.focus(); } - _resolve: {(response: ICellEditResponse): void }; - _reject: {(reason: any): void }; - _cell: CellEditor.CellConfig; _select: HTMLSelectElement | null; } @@ -320,49 +343,40 @@ class DateCellEditor extends CellEditor { return metadata.type === 'date'; } - edit(cell: CellEditor.CellConfig): Promise { - return new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - - this._cell = cell; - const grid = cell.grid; - const cellInfo = this.getCellInfo(cell); - - const input = document.createElement('input'); - input.type = 'date'; - input.pattern = "\d{4}-\d{2}-\d{2}"; - input.classList.add('cell-editor'); - input.classList.add('input-cell-editor'); - input.spellcheck = false; + startEdit() { + const cell = this._cell; + const grid = cell.grid; + const cellInfo = this.getCellInfo(cell); - input.value = cellInfo.data; + const input = document.createElement('input'); + input.type = 'date'; + input.pattern = "\d{4}-\d{2}-\d{2}"; + input.classList.add('cell-editor'); + input.classList.add('input-cell-editor'); + input.spellcheck = false; - this._input = input; + input.value = cellInfo.data; - grid.node.appendChild(input); + this._input = input; - this.updatePosition(); + grid.node.appendChild(input); - input.select(); + this.updatePosition(); - input.addEventListener("keydown", (event) => { - this._onKeyDown(event); - }); + input.select(); - input.addEventListener("blur", (event) => { - this._saveInput(); - this.endEditing(); - }); + input.addEventListener("keydown", (event) => { + this._onKeyDown(event); + }); - input.addEventListener("change", (event) => { - this._saveInput(); - //this.endEditing(); - }); + input.addEventListener("blur", (event) => { + this._saveInput(); + this.endEditing(); + }); - grid.node.addEventListener('wheel', () => { - this.updatePosition(); - }); + input.addEventListener("change", (event) => { + this._saveInput(); + //this.endEditing(); }); } @@ -394,6 +408,11 @@ class DateCellEditor extends CellEditor { return; } + const value = this._input.value; + if (this._validator && !this._validator.validate(this._cell, value)) { + return; + } + this._resolve({ cell: this._cell, value: this._input!.value }); } @@ -428,9 +447,6 @@ class DateCellEditor extends CellEditor { this._cell.grid.viewport.node.focus(); } - _resolve: {(response: ICellEditResponse): void }; - _reject: {(reason: any): void }; - _cell: CellEditor.CellConfig; _input: HTMLInputElement | null; } diff --git a/packages/default-theme/style/datagrid.css b/packages/default-theme/style/datagrid.css index 59f8afdda..9c1ca3396 100644 --- a/packages/default-theme/style/datagrid.css +++ b/packages/default-theme/style/datagrid.css @@ -36,6 +36,11 @@ border: 2px solid #006bf7; } +input.cell-editor:invalid { + box-shadow: 0px 0px 6px red; + border: 2px solid red; +} + .input-cell-editor { padding: 0 0 1px 0; background-color: #ffffff; From 9ffc533b8c1e6d7f14d33336f5ca9fce7046a41b Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 11 Oct 2019 15:33:28 -0400 Subject: [PATCH 03/36] viewport occluder, input validator, refactoring --- packages/datagrid/src/basickeyhandler.ts | 5 + packages/datagrid/src/basicmousehandler.ts | 16 +- packages/datagrid/src/celleditor.ts | 202 ++++++++------------- packages/default-theme/style/datagrid.css | 19 +- 4 files changed, 115 insertions(+), 127 deletions(-) diff --git a/packages/datagrid/src/basickeyhandler.ts b/packages/datagrid/src/basickeyhandler.ts index 890d67f57..5609a41dd 100644 --- a/packages/datagrid/src/basickeyhandler.ts +++ b/packages/datagrid/src/basickeyhandler.ts @@ -75,6 +75,11 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { const dataModel = grid.dataModel as MutableDataModel; dataModel.setData('body', row, column, response.value); } + grid.viewport.node.focus(); + if (response.returnPressed) { + grid.selectionModel!.incrementCursor(); + grid.scrollToCursor(); + } }); } } diff --git a/packages/datagrid/src/basicmousehandler.ts b/packages/datagrid/src/basicmousehandler.ts index 19c76461d..7802a4ab2 100644 --- a/packages/datagrid/src/basicmousehandler.ts +++ b/packages/datagrid/src/basicmousehandler.ts @@ -28,7 +28,7 @@ import { import { SelectionModel } from './selectionmodel'; -import { ICellEditResponse } from './celleditor'; +import { ICellEditResponse, ICellInputValidatorResponse } from './celleditor'; /** * A basic implementation of a data grid mouse handler. @@ -521,12 +521,24 @@ class BasicMouseHandler implements DataGrid.IMouseHandler { grid: grid, row: row, column: column, - metadata: grid.dataModel!.metadata('body', row, column)}) + metadata: grid.dataModel!.metadata('body', row, column)}, + { + validate: (value: any): ICellInputValidatorResponse => { + return { + valid: true + }; + } + }) .then((response: ICellEditResponse) => { if (grid.dataModel instanceof MutableDataModel) { const dataModel = grid.dataModel as MutableDataModel; dataModel.setData('body', row, column, response.value); } + grid.viewport.node.focus(); + if (response.returnPressed) { + grid.selectionModel!.incrementCursor(); + grid.scrollToCursor(); + } }); } diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 47dc99a50..414304d44 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -11,13 +11,22 @@ export interface ICellEditResponse { cell: CellEditor.CellConfig; value: any; + returnPressed: boolean; +}; + +export +interface ICellInputValidatorResponse { + valid: boolean; + message?: string; }; export interface ICellInputValidator { - validate(cell: CellEditor.CellConfig, value: any): boolean; + validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse; }; +const DEFAULT_INVALID_INPUT_MESSAGE = "Invalid input!"; + export class CellEditorController { registerEditor(editor: CellEditor) { @@ -62,7 +71,9 @@ abstract class CellEditor { this.updatePosition(); }); - this.startEdit(); + this._addContainer(); + + this.startEditing(); }); } @@ -87,14 +98,46 @@ abstract class CellEditor { }; } - protected abstract startEdit(): void; - protected abstract updatePosition(): void; - protected abstract endEditing(): void; + private _addContainer() { + this._viewportOccluder = document.createElement('div'); + this._viewportOccluder.className = 'cell-editor-occluder'; + this._cell.grid.node.appendChild(this._viewportOccluder); + + this._cellContainer = document.createElement('div'); + this._cellContainer.className = 'cell-editor-container'; + this._viewportOccluder.appendChild(this._cellContainer); + } + + protected abstract startEditing(): void; + + protected updatePosition(): void { + const grid = this._cell.grid; + const cellInfo = this.getCellInfo(this._cell); + const headerHeight = grid.headerHeight; + const headerWidth = grid.headerWidth; + + this._viewportOccluder.style.top = headerHeight + 'px'; + this._viewportOccluder.style.left = headerWidth + 'px'; + this._viewportOccluder.style.width = (grid.viewportWidth - headerWidth) + 'px'; + this._viewportOccluder.style.height = (grid.viewportHeight - headerHeight) + 'px'; + + this._cellContainer.style.left = (cellInfo.x - 1 - headerWidth) + 'px'; + this._cellContainer.style.top = (cellInfo.y - 1 - headerHeight) + 'px'; + this._cellContainer.style.width = (cellInfo.width + 1) + 'px'; + this._cellContainer.style.height = (cellInfo.height + 1) + 'px'; + this._cellContainer.style.visibility = 'visible'; + } + + protected endEditing(): void { + this._cell.grid.node.removeChild(this._viewportOccluder); + } protected _resolve: {(response: ICellEditResponse): void }; protected _reject: {(reason: any): void }; protected _cell: CellEditor.CellConfig; protected _validator: ICellInputValidator | undefined; + protected _viewportOccluder: HTMLDivElement; + protected _cellContainer: HTMLDivElement; } export @@ -104,9 +147,8 @@ class TextCellEditor extends CellEditor { return metadata.type === 'string'; } - startEdit() { + startEditing() { const cell = this._cell; - const grid = cell.grid; const cellInfo = this.getCellInfo(cell); const form = document.createElement('form'); @@ -121,11 +163,12 @@ class TextCellEditor extends CellEditor { this._input = input; form.appendChild(input); - grid.node.appendChild(form); + this._cellContainer.appendChild(form); this.updatePosition(); input.select(); + input.focus(); input.addEventListener("keydown", (event) => { this._onKeyDown(event); @@ -134,8 +177,6 @@ class TextCellEditor extends CellEditor { input.addEventListener("blur", (event) => { if (this._saveInput()) { this.endEditing(); - } else { - this._input!.focus(); } }); @@ -147,10 +188,8 @@ class TextCellEditor extends CellEditor { _onKeyDown(event: KeyboardEvent) { switch (event.keyCode) { case 13: // return - if (this._saveInput()) { + if (this._saveInput(true)) { this.endEditing(); - this._cell.grid.selectionModel!.incrementCursor(); - this._cell.grid.scrollToCursor(); } break; case 27: // escape @@ -161,59 +200,34 @@ class TextCellEditor extends CellEditor { } } - _isCellFullyVisible(cellInfo: any): boolean { - const grid = cellInfo.grid; - - return cellInfo.x >= grid.headerWidth && (cellInfo.x + cellInfo.width) <= grid.viewportWidth + 1 && - cellInfo.y >= grid.headerHeight && (cellInfo.y + cellInfo.height) <= grid.viewportHeight + 1; - } - - _saveInput(): boolean { + _saveInput(returnPressed: boolean = false): boolean { if (!this._input) { return false; } const value = this._input.value; - if (this._validator && ! this._validator.validate(this._cell, value)) { - this._input.setCustomValidity("Invalid input"); - this._input.checkValidity(); - return false; + if (this._validator) { + const result = this._validator.validate(this._cell, value); + if (!result.valid) { + this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); + this._input.checkValidity(); + return false; + } } - this._resolve({ cell: this._cell, value: this._input!.value }); + this._resolve({ cell: this._cell, value: this._input!.value, returnPressed: returnPressed }); return true; } - updatePosition() { - if (!this._input) { - return; - } - - const cellInfo = this.getCellInfo(this._cell); - const input = this._input; - - if (!this._isCellFullyVisible(cellInfo)) { - input.style.visibility = 'hidden'; - } else { - input.style.left = (cellInfo.x - 1) + 'px'; - input.style.top = (cellInfo.y - 1) + 'px'; - input.style.width = (cellInfo.width + 1) + 'px'; - input.style.height = (cellInfo.height + 1) + 'px'; - input.style.visibility = 'visible'; - input.focus(); - } - } - endEditing() { if (!this._input) { return; } - this._input.style.display = 'none'; - //this._input.remove(); this._input = null; - this._cell.grid.viewport.node.focus(); + + super.endEditing(); } _input: HTMLInputElement | null; @@ -226,9 +240,8 @@ class EnumCellEditor extends CellEditor { return metadata.constraint && metadata.constraint.enum; } - startEdit() { + startEditing() { const cell = this._cell; - const grid = cell.grid; const cellInfo = this.getCellInfo(cell); const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); const items = metadata.constraint.enum; @@ -247,9 +260,10 @@ class EnumCellEditor extends CellEditor { this._select = select; - grid.node.appendChild(select); + this._cellContainer.appendChild(select); this.updatePosition(); + select.focus(); select.addEventListener("keydown", (event) => { this._onKeyDown(event); @@ -269,10 +283,8 @@ class EnumCellEditor extends CellEditor { _onKeyDown(event: KeyboardEvent) { switch (event.keyCode) { case 13: // return - this._saveInput(); + this._saveInput(true); this.endEditing(); - this._cell.grid.selectionModel!.incrementCursor(); - this._cell.grid.scrollToCursor(); break; case 27: // escape this.endEditing(); @@ -282,14 +294,7 @@ class EnumCellEditor extends CellEditor { } } - _isCellFullyVisible(cellInfo: any): boolean { - const grid = cellInfo.grid; - - return cellInfo.x >= grid.headerWidth && (cellInfo.x + cellInfo.width) <= grid.viewportWidth + 1 && - cellInfo.y >= grid.headerHeight && (cellInfo.y + cellInfo.height) <= grid.viewportHeight + 1; - } - - _saveInput() { + _saveInput(returnPressed: boolean = false) { if (!this._select) { return; } @@ -299,27 +304,7 @@ class EnumCellEditor extends CellEditor { return; } - this._resolve({ cell: this._cell, value: value }); - } - - updatePosition() { - if (!this._select) { - return; - } - - const cellInfo = this.getCellInfo(this._cell); - const input = this._select; - - if (!this._isCellFullyVisible(cellInfo)) { - input.style.visibility = 'hidden'; - } else { - input.style.left = (cellInfo.x - 1) + 'px'; - input.style.top = (cellInfo.y - 1) + 'px'; - input.style.width = (cellInfo.width + 1) + 'px'; - input.style.height = (cellInfo.height + 1) + 'px'; - input.style.visibility = 'visible'; - input.focus(); - } + this._resolve({ cell: this._cell, value: value, returnPressed: returnPressed }); } endEditing() { @@ -327,10 +312,9 @@ class EnumCellEditor extends CellEditor { return; } - this._select.style.display = 'none'; - //this._select.remove(); this._select = null; - this._cell.grid.viewport.node.focus(); + + super.endEditing(); } _select: HTMLSelectElement | null; @@ -343,9 +327,8 @@ class DateCellEditor extends CellEditor { return metadata.type === 'date'; } - startEdit() { + startEditing() { const cell = this._cell; - const grid = cell.grid; const cellInfo = this.getCellInfo(cell); const input = document.createElement('input'); @@ -359,11 +342,12 @@ class DateCellEditor extends CellEditor { this._input = input; - grid.node.appendChild(input); + this._cellContainer.appendChild(input); this.updatePosition(); input.select(); + input.focus(); input.addEventListener("keydown", (event) => { this._onKeyDown(event); @@ -385,8 +369,6 @@ class DateCellEditor extends CellEditor { case 13: // return this._saveInput(); this.endEditing(); - this._cell.grid.selectionModel!.incrementCursor(); - this._cell.grid.scrollToCursor(); break; case 27: // escape this.endEditing(); @@ -396,14 +378,7 @@ class DateCellEditor extends CellEditor { } } - _isCellFullyVisible(cellInfo: any): boolean { - const grid = cellInfo.grid; - - return cellInfo.x >= grid.headerWidth && (cellInfo.x + cellInfo.width) <= grid.viewportWidth + 1 && - cellInfo.y >= grid.headerHeight && (cellInfo.y + cellInfo.height) <= grid.viewportHeight + 1; - } - - _saveInput() { + _saveInput(returnPressed: boolean = false) { if (!this._input) { return; } @@ -413,27 +388,7 @@ class DateCellEditor extends CellEditor { return; } - this._resolve({ cell: this._cell, value: this._input!.value }); - } - - updatePosition() { - if (!this._input) { - return; - } - - const cellInfo = this.getCellInfo(this._cell); - const input = this._input; - - if (!this._isCellFullyVisible(cellInfo)) { - input.style.visibility = 'hidden'; - } else { - input.style.left = (cellInfo.x - 1) + 'px'; - input.style.top = (cellInfo.y - 1) + 'px'; - input.style.width = (cellInfo.width + 1) + 'px'; - input.style.height = (cellInfo.height + 1) + 'px'; - input.style.visibility = 'visible'; - input.focus(); - } + this._resolve({ cell: this._cell, value: this._input!.value, returnPressed: returnPressed }); } endEditing() { @@ -441,10 +396,9 @@ class DateCellEditor extends CellEditor { return; } - this._input.style.display = 'none'; - //this._input.remove(); this._input = null; - this._cell.grid.viewport.node.focus(); + + super.endEditing(); } _input: HTMLInputElement | null; diff --git a/packages/default-theme/style/datagrid.css b/packages/default-theme/style/datagrid.css index 9c1ca3396..820df1c36 100644 --- a/packages/default-theme/style/datagrid.css +++ b/packages/default-theme/style/datagrid.css @@ -28,8 +28,25 @@ background-color: #A0A0A0; } -.cell-editor { +.cell-editor-occluder { + width: 100%; + height: 100%; position: absolute; + overflow: hidden; +} + +.cell-editor-container { + position: absolute; +} + +.cell-editor-container > form { + width: 100%; + height: 100%; +} + +.cell-editor { + width: 100%; + height: 100%; box-sizing: border-box; outline: none; box-shadow: 0px 0px 6px #006bf7; From aa1aa916673044c12593bd64b3b8ac7b2aa86b23 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 11 Oct 2019 23:31:19 -0400 Subject: [PATCH 04/36] BooleanCellEditor, IntegerCellEditor, refactoring --- packages/datagrid/src/celleditor.ts | 252 +++++++++++++++++++--- packages/datagrid/src/datagrid.ts | 5 +- packages/default-theme/style/datagrid.css | 1 + 3 files changed, 226 insertions(+), 32 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 414304d44..da15ba5cf 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -27,24 +27,61 @@ interface ICellInputValidator { const DEFAULT_INVALID_INPUT_MESSAGE = "Invalid input!"; +//type CellDataType = 'string' | 'integer' | 'number' | 'boolean' | 'date' | string; + export class CellEditorController { - registerEditor(editor: CellEditor) { - this._editors.push(editor); + private _getKey(cell: CellEditor.CellConfig): string { + const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : null; + + if (!metadata) { + return 'undefined'; + } + + let key = ''; + + if (metadata) { + key = metadata.type; + } + + if (metadata.constraint && metadata.constraint.enum) { + key += ':enum'; + } + + return key; } - edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): Promise { - const numEditors = this._editors.length; - for (let i = numEditors - 1; i >= 0; --i) { - const editor = this._editors[i]; - if (editor.canEdit(cell)) { - return editor.edit(cell, validator); - } + private _getEditor(cell: CellEditor.CellConfig): CellEditor | null { + const key = this._getKey(cell); + + switch (key) { + case 'string': + case 'number': + return new TextCellEditor(); + case 'integer': + return new IntegerCellEditor(); + case 'boolean': + return new BooleanCellEditor(); + case 'date': + return new DateCellEditor(); + case 'string:enum': + case 'number:enum': + case 'integer:enum': + case 'date:enum': + return new SelectCellEditor(); } const data = cell.grid.dataModel ? cell.grid.dataModel.data('body', cell.row, cell.column) : undefined; if (!data || typeof data !== 'object') { - const editor = new TextCellEditor(); + return new TextCellEditor(); + } + + return null; + } + + edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): Promise { + const editor = this._getEditor(cell); + if (editor) { return editor.edit(cell, validator); } @@ -52,14 +89,10 @@ class CellEditorController { reject('Editor not found'); }); } - - private _editors: CellEditor[] = []; } export abstract class CellEditor { - abstract canEdit(cell: CellEditor.CellConfig): boolean; - edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): Promise { return new Promise((resolve, reject) => { this._cell = cell; @@ -142,11 +175,94 @@ abstract class CellEditor { export class TextCellEditor extends CellEditor { - canEdit(cell: CellEditor.CellConfig): boolean { - const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : {}; - return metadata.type === 'string'; + startEditing() { + const cell = this._cell; + const cellInfo = this.getCellInfo(cell); + + const form = document.createElement('form'); + const input = document.createElement('input'); + input.classList.add('cell-editor'); + input.classList.add('input-cell-editor'); + input.spellcheck = false; + input.required = false; + + input.value = cellInfo.data; + + this._input = input; + + form.appendChild(input); + this._cellContainer.appendChild(form); + + this.updatePosition(); + + input.select(); + input.focus(); + + input.addEventListener("keydown", (event) => { + this._onKeyDown(event); + }); + + input.addEventListener("blur", (event) => { + if (this._saveInput()) { + this.endEditing(); + } + }); + + input.addEventListener("input", (event) => { + this._input!.setCustomValidity(""); + }); + } + + _onKeyDown(event: KeyboardEvent) { + switch (event.keyCode) { + case 13: // return + if (this._saveInput(true)) { + this.endEditing(); + } + break; + case 27: // escape + this.endEditing(); + break; + default: + break; + } + } + + _saveInput(returnPressed: boolean = false): boolean { + if (!this._input) { + return false; + } + + const value = this._input.value; + if (this._validator) { + const result = this._validator.validate(this._cell, value); + if (!result.valid) { + this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); + this._input.checkValidity(); + return false; + } + } + + this._resolve({ cell: this._cell, value: value, returnPressed: returnPressed }); + + return true; + } + + endEditing() { + if (!this._input) { + return; + } + + this._input = null; + + super.endEditing(); } + _input: HTMLInputElement | null; +} + +export +class IntegerCellEditor extends CellEditor { startEditing() { const cell = this._cell; const cellInfo = this.getCellInfo(cell); @@ -155,6 +271,7 @@ class TextCellEditor extends CellEditor { const input = document.createElement('input'); input.classList.add('cell-editor'); input.classList.add('input-cell-editor'); + input.type = 'number'; input.spellcheck = false; input.required = false; @@ -215,7 +332,7 @@ class TextCellEditor extends CellEditor { } } - this._resolve({ cell: this._cell, value: this._input!.value, returnPressed: returnPressed }); + this._resolve({ cell: this._cell, value: value, returnPressed: returnPressed }); return true; } @@ -234,12 +351,96 @@ class TextCellEditor extends CellEditor { } export -class EnumCellEditor extends CellEditor { - canEdit(cell: CellEditor.CellConfig): boolean { - const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : {}; - return metadata.constraint && metadata.constraint.enum; +class BooleanCellEditor extends CellEditor { + startEditing() { + const cell = this._cell; + const cellInfo = this.getCellInfo(cell); + + const form = document.createElement('form'); + const input = document.createElement('input'); + input.classList.add('cell-editor'); + input.classList.add('input-cell-editor'); + input.type = 'checkbox'; + input.spellcheck = false; + input.required = false; + + input.checked = cellInfo.data == true; + + this._input = input; + + form.appendChild(input); + this._cellContainer.appendChild(form); + + this.updatePosition(); + + input.select(); + input.focus(); + + input.addEventListener("keydown", (event) => { + this._onKeyDown(event); + }); + + input.addEventListener("blur", (event) => { + if (this._saveInput()) { + this.endEditing(); + } + }); + + input.addEventListener("input", (event) => { + this._input!.setCustomValidity(""); + }); } + _onKeyDown(event: KeyboardEvent) { + switch (event.keyCode) { + case 13: // return + if (this._saveInput(true)) { + this.endEditing(); + } + break; + case 27: // escape + this.endEditing(); + break; + default: + break; + } + } + + _saveInput(returnPressed: boolean = false): boolean { + if (!this._input) { + return false; + } + + const value = this._input.checked; + if (this._validator) { + const result = this._validator.validate(this._cell, value); + if (!result.valid) { + this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); + this._input.checkValidity(); + return false; + } + } + + this._resolve({ cell: this._cell, value: value, returnPressed: returnPressed }); + + return true; + } + + endEditing() { + if (!this._input) { + return; + } + + this._input = null; + + super.endEditing(); + } + + _input: HTMLInputElement | null; +} + +export +class SelectCellEditor extends CellEditor { startEditing() { const cell = this._cell; const cellInfo = this.getCellInfo(cell); @@ -322,11 +523,6 @@ class EnumCellEditor extends CellEditor { export class DateCellEditor extends CellEditor { - canEdit(cell: CellEditor.CellConfig): boolean { - const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : {}; - return metadata.type === 'date'; - } - startEditing() { const cell = this._cell; const cellInfo = this.getCellInfo(cell); @@ -388,7 +584,7 @@ class DateCellEditor extends CellEditor { return; } - this._resolve({ cell: this._cell, value: this._input!.value, returnPressed: returnPressed }); + this._resolve({ cell: this._cell, value: value, returnPressed: returnPressed }); } endEditing() { diff --git a/packages/datagrid/src/datagrid.ts b/packages/datagrid/src/datagrid.ts index 717dcf964..036ebd6f6 100644 --- a/packages/datagrid/src/datagrid.ts +++ b/packages/datagrid/src/datagrid.ts @@ -48,7 +48,7 @@ import { import { SelectionModel } from './selectionmodel'; -import { CellEditorController, TextCellEditor, EnumCellEditor, DateCellEditor } from './celleditor'; +import { CellEditorController } from './celleditor'; /** @@ -129,9 +129,6 @@ class DataGrid extends Widget { this._scrollCorner = new Widget(); this._cellEditorController = new CellEditorController(); - this._cellEditorController.registerEditor(new TextCellEditor()); - this._cellEditorController.registerEditor(new EnumCellEditor()); - this._cellEditorController.registerEditor(new DateCellEditor()); // Add the extra class names to the child widgets. this._viewport.addClass('p-DataGrid-viewport'); diff --git a/packages/default-theme/style/datagrid.css b/packages/default-theme/style/datagrid.css index 820df1c36..6b6039eda 100644 --- a/packages/default-theme/style/datagrid.css +++ b/packages/default-theme/style/datagrid.css @@ -37,6 +37,7 @@ .cell-editor-container { position: absolute; + background-color: #ffffff; } .cell-editor-container > form { From 45d78472b73dacdf900a70abd71ff38b56a2b9e9 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sat, 12 Oct 2019 18:31:25 -0400 Subject: [PATCH 05/36] styling improvements --- packages/datagrid/src/celleditor.ts | 33 +++++++++++++++++++++-- packages/default-theme/style/datagrid.css | 22 +++++++++------ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index da15ba5cf..c1009d8de 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -165,12 +165,26 @@ abstract class CellEditor { this._cell.grid.node.removeChild(this._viewportOccluder); } + protected set validInput(value: boolean) { + this._validInput = value; + if (this._validInput) { + this._cellContainer.classList.remove('invalid'); + } else { + this._cellContainer.classList.add('invalid'); + } + } + + protected get validInput(): boolean { + return this._validInput; + } + protected _resolve: {(response: ICellEditResponse): void }; protected _reject: {(reason: any): void }; protected _cell: CellEditor.CellConfig; protected _validator: ICellInputValidator | undefined; protected _viewportOccluder: HTMLDivElement; protected _cellContainer: HTMLDivElement; + private _validInput: boolean = true; } export @@ -189,6 +203,7 @@ class TextCellEditor extends CellEditor { input.value = cellInfo.data; this._input = input; + this._form = form; form.appendChild(input); this._cellContainer.appendChild(form); @@ -205,11 +220,15 @@ class TextCellEditor extends CellEditor { input.addEventListener("blur", (event) => { if (this._saveInput()) { this.endEditing(); + event.preventDefault(); + event.stopPropagation(); + this._input!.focus(); } }); input.addEventListener("input", (event) => { this._input!.setCustomValidity(""); + this.validInput = true; }); } @@ -218,6 +237,8 @@ class TextCellEditor extends CellEditor { case 13: // return if (this._saveInput(true)) { this.endEditing(); + event.preventDefault(); + event.stopPropagation(); } break; case 27: // escape @@ -237,8 +258,9 @@ class TextCellEditor extends CellEditor { if (this._validator) { const result = this._validator.validate(this._cell, value); if (!result.valid) { + this.validInput = false; this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); - this._input.checkValidity(); + this._form.reportValidity(); return false; } } @@ -259,6 +281,7 @@ class TextCellEditor extends CellEditor { } _input: HTMLInputElement | null; + _form: HTMLFormElement; } export @@ -299,6 +322,7 @@ class IntegerCellEditor extends CellEditor { input.addEventListener("input", (event) => { this._input!.setCustomValidity(""); + this.validInput = true; }); } @@ -326,6 +350,7 @@ class IntegerCellEditor extends CellEditor { if (this._validator) { const result = this._validator.validate(this._cell, value); if (!result.valid) { + this.validInput = false; this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); this._input.checkValidity(); return false; @@ -359,7 +384,7 @@ class BooleanCellEditor extends CellEditor { const form = document.createElement('form'); const input = document.createElement('input'); input.classList.add('cell-editor'); - input.classList.add('input-cell-editor'); + input.classList.add('boolean-cell-editor'); input.type = 'checkbox'; input.spellcheck = false; input.required = false; @@ -388,6 +413,7 @@ class BooleanCellEditor extends CellEditor { input.addEventListener("input", (event) => { this._input!.setCustomValidity(""); + this.validInput = true; }); } @@ -415,6 +441,7 @@ class BooleanCellEditor extends CellEditor { if (this._validator) { const result = this._validator.validate(this._cell, value); if (!result.valid) { + this.validInput = false; this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); this._input.checkValidity(); return false; @@ -502,6 +529,7 @@ class SelectCellEditor extends CellEditor { const value = this._select.value; if (this._validator && ! this._validator.validate(this._cell, value)) { + this.validInput = false; return; } @@ -581,6 +609,7 @@ class DateCellEditor extends CellEditor { const value = this._input.value; if (this._validator && !this._validator.validate(this._cell, value)) { + this.validInput = false; return; } diff --git a/packages/default-theme/style/datagrid.css b/packages/default-theme/style/datagrid.css index 6b6039eda..147dab18b 100644 --- a/packages/default-theme/style/datagrid.css +++ b/packages/default-theme/style/datagrid.css @@ -38,6 +38,14 @@ .cell-editor-container { position: absolute; background-color: #ffffff; + box-sizing: border-box; + box-shadow: 0px 0px 6px #006bf7; + border: 2px solid #006bf7; +} + +.cell-editor-container.invalid { + box-shadow: 0px 0px 6px red; + border: 2px solid red; } .cell-editor-container > form { @@ -48,18 +56,16 @@ .cell-editor { width: 100%; height: 100%; - box-sizing: border-box; outline: none; - box-shadow: 0px 0px 6px #006bf7; - border: 2px solid #006bf7; -} - -input.cell-editor:invalid { - box-shadow: 0px 0px 6px red; - border: 2px solid red; + box-sizing: border-box; } .input-cell-editor { padding: 0 0 1px 0; background-color: #ffffff; + border: 0; +} + +.boolean-cell-editor { + margin: 0; } From 2d3a637a1fc3c294085ca9eec9502e742f55a56f Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Mon, 14 Oct 2019 14:49:09 -0400 Subject: [PATCH 06/36] move cursor within selection rectangles and in reverse direction as well --- packages/datagrid/src/basickeyhandler.ts | 8 +-- packages/datagrid/src/basicmousehandler.ts | 15 ++-- packages/datagrid/src/basicselectionmodel.ts | 74 ++++++++++++++++---- packages/datagrid/src/celleditor.ts | 31 ++++---- packages/datagrid/src/datagrid.ts | 55 +++++++++++++++ packages/datagrid/src/selectionmodel.ts | 4 +- 6 files changed, 141 insertions(+), 46 deletions(-) diff --git a/packages/datagrid/src/basickeyhandler.ts b/packages/datagrid/src/basickeyhandler.ts index 5609a41dd..4bf36cecc 100644 --- a/packages/datagrid/src/basickeyhandler.ts +++ b/packages/datagrid/src/basickeyhandler.ts @@ -58,7 +58,7 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { * This will not be called if the mouse button is pressed. */ onKeyDown(grid: DataGrid, event: KeyboardEvent): void { - if (!event.shiftKey && !Platform.accelKey(event)) { + if (!Platform.accelKey(event)) { const inp = String.fromCharCode(event.keyCode); if (/[a-zA-Z0-9-_ ]/.test(inp)) { if (grid.selectionModel) { @@ -76,8 +76,8 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { dataModel.setData('body', row, column, response.value); } grid.viewport.node.focus(); - if (response.returnPressed) { - grid.selectionModel!.incrementCursor(); + if (response.cursorMovement) { + grid.incrementCursor(event.shiftKey ? 'up' : 'down'); grid.scrollToCursor(); } }); @@ -113,7 +113,7 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { break; case 'Enter': if (grid.selectionModel) { - grid.selectionModel.incrementCursor(); + grid.incrementCursor(event.shiftKey ? 'up' : 'down'); grid.scrollToCursor(); } break; diff --git a/packages/datagrid/src/basicmousehandler.ts b/packages/datagrid/src/basicmousehandler.ts index 7802a4ab2..aea285f12 100644 --- a/packages/datagrid/src/basicmousehandler.ts +++ b/packages/datagrid/src/basicmousehandler.ts @@ -28,7 +28,7 @@ import { import { SelectionModel } from './selectionmodel'; -import { ICellEditResponse, ICellInputValidatorResponse } from './celleditor'; +import { ICellEditResponse } from './celleditor'; /** * A basic implementation of a data grid mouse handler. @@ -521,22 +521,15 @@ class BasicMouseHandler implements DataGrid.IMouseHandler { grid: grid, row: row, column: column, - metadata: grid.dataModel!.metadata('body', row, column)}, - { - validate: (value: any): ICellInputValidatorResponse => { - return { - valid: true - }; - } - }) + metadata: grid.dataModel!.metadata('body', row, column)}) .then((response: ICellEditResponse) => { if (grid.dataModel instanceof MutableDataModel) { const dataModel = grid.dataModel as MutableDataModel; dataModel.setData('body', row, column, response.value); } grid.viewport.node.focus(); - if (response.returnPressed) { - grid.selectionModel!.incrementCursor(); + if (response.cursorMovement !== 'none') { + grid.incrementCursor(response.cursorMovement); grid.scrollToCursor(); } }); diff --git a/packages/datagrid/src/basicselectionmodel.ts b/packages/datagrid/src/basicselectionmodel.ts index d9ebbc82f..62b79f607 100644 --- a/packages/datagrid/src/basicselectionmodel.ts +++ b/packages/datagrid/src/basicselectionmodel.ts @@ -48,23 +48,64 @@ class BasicSelectionModel extends SelectionModel { return this._cursorColumn; } - incrementCursor(): void { - const newRow = this._cursorRow + 1; - if (this._selections.length === 1 && ( - this._selections[0].r1 === this._cursorRow && - this._selections[0].c1 === this._cursorColumn && - this._selections[0].r2 === this._cursorRow && - this._selections[0].c2 === this._cursorColumn - )) { - this._selections[0] = { - r1: newRow, - c1: this._cursorColumn, - r2: newRow, - c2: this._cursorColumn - }; + /** + * Move cursor down/up while making sure it remains + * within the bounds of selected rectangles + */ + incrementCursorWithinSelections(direction: SelectionModel.CursorMoveDirection): void { + // Bail early if there are no selections or no existing cursor + if (this.isEmpty || this.cursorRow === -1 || this._cursorColumn === -1) { + return; + } + + // Bail early if only single cell is selected + const firstSelection = this._selections[0]; + if (this._selections.length === 1 && + firstSelection.r1 === firstSelection.r2 && + firstSelection.c1 === firstSelection.c2) { + return; + } + + // start from last selection rectangle + if (this._cursorRectIndex === -1) { + this._cursorRectIndex = this._selections.length - 1; + } + + let cursorRect = this._selections[this._cursorRectIndex]; + let newRow = this._cursorRow + (direction === 'down' ? 1 : -1); + let newColumn = this._cursorColumn; + const r1 = Math.min(cursorRect.r1, cursorRect.r2); + const r2 = Math.max(cursorRect.r1, cursorRect.r2); + const c1 = Math.min(cursorRect.c1, cursorRect.c2); + const c2 = Math.max(cursorRect.c1, cursorRect.c2); + + if (newRow > r2) { + newRow = r1; + newColumn += 1; + } else if (newRow < r1) { + newRow = r2; + newColumn -= 1; } - + + // if going downward and the last cell in the selection rectangle visited, + // move to next rectangle + if (newColumn > c2) { + this._cursorRectIndex = (this._cursorRectIndex + 1) % this._selections.length; + cursorRect = this._selections[this._cursorRectIndex]; + newRow = Math.min(cursorRect.r1, cursorRect.r2); + newColumn = Math.min(cursorRect.c1, cursorRect.c2); + } + // if going upward and the first cell in the selection rectangle visited, + // move to previous rectangle + else if (newColumn < c1) { + this._cursorRectIndex = this._cursorRectIndex === 0 ? this._selections.length - 1 : this._cursorRectIndex - 1; + cursorRect = this._selections[this._cursorRectIndex]; + newRow = Math.max(cursorRect.r1, cursorRect.r2); + newColumn = Math.max(cursorRect.c1, cursorRect.c2); + } + this._cursorRow = newRow; + this._cursorColumn = newColumn; // Emit the changed signal. this.emitChanged(); @@ -149,6 +190,7 @@ class BasicSelectionModel extends SelectionModel { // Update the cursor. this._cursorRow = cr; this._cursorColumn = cc; + this._cursorRectIndex = this._selections.length; // Add the new selection. this._selections.push({ r1, c1, r2, c2 }); @@ -169,6 +211,7 @@ class BasicSelectionModel extends SelectionModel { // Reset the internal state. this._cursorRow = -1; this._cursorColumn = -1; + this._cursorRectIndex = -1; this._selections.length = 0; // Emit the changed signal. @@ -254,5 +297,6 @@ class BasicSelectionModel extends SelectionModel { private _cursorRow = -1; private _cursorColumn = -1; + private _cursorRectIndex = -1; private _selections: SelectionModel.Selection[] = []; } diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index c1009d8de..f94968cd1 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -6,12 +6,13 @@ import { DataGrid } from './datagrid'; import { DataModel } from './datamodel'; +import { SelectionModel } from './selectionmodel'; export interface ICellEditResponse { cell: CellEditor.CellConfig; value: any; - returnPressed: boolean; + cursorMovement: SelectionModel.CursorMoveDirection; }; export @@ -235,7 +236,7 @@ class TextCellEditor extends CellEditor { _onKeyDown(event: KeyboardEvent) { switch (event.keyCode) { case 13: // return - if (this._saveInput(true)) { + if (this._saveInput(event.shiftKey ? 'up' : 'down')) { this.endEditing(); event.preventDefault(); event.stopPropagation(); @@ -249,7 +250,7 @@ class TextCellEditor extends CellEditor { } } - _saveInput(returnPressed: boolean = false): boolean { + _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none'): boolean { if (!this._input) { return false; } @@ -265,7 +266,7 @@ class TextCellEditor extends CellEditor { } } - this._resolve({ cell: this._cell, value: value, returnPressed: returnPressed }); + this._resolve({ cell: this._cell, value: value, cursorMovement: cursorMovement }); return true; } @@ -329,7 +330,7 @@ class IntegerCellEditor extends CellEditor { _onKeyDown(event: KeyboardEvent) { switch (event.keyCode) { case 13: // return - if (this._saveInput(true)) { + if (this._saveInput('down')) { this.endEditing(); } break; @@ -341,7 +342,7 @@ class IntegerCellEditor extends CellEditor { } } - _saveInput(returnPressed: boolean = false): boolean { + _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none'): boolean { if (!this._input) { return false; } @@ -357,7 +358,7 @@ class IntegerCellEditor extends CellEditor { } } - this._resolve({ cell: this._cell, value: value, returnPressed: returnPressed }); + this._resolve({ cell: this._cell, value: value, cursorMovement: cursorMovement }); return true; } @@ -420,7 +421,7 @@ class BooleanCellEditor extends CellEditor { _onKeyDown(event: KeyboardEvent) { switch (event.keyCode) { case 13: // return - if (this._saveInput(true)) { + if (this._saveInput('down')) { this.endEditing(); } break; @@ -432,7 +433,7 @@ class BooleanCellEditor extends CellEditor { } } - _saveInput(returnPressed: boolean = false): boolean { + _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none'): boolean { if (!this._input) { return false; } @@ -448,7 +449,7 @@ class BooleanCellEditor extends CellEditor { } } - this._resolve({ cell: this._cell, value: value, returnPressed: returnPressed }); + this._resolve({ cell: this._cell, value: value, cursorMovement: cursorMovement }); return true; } @@ -511,7 +512,7 @@ class SelectCellEditor extends CellEditor { _onKeyDown(event: KeyboardEvent) { switch (event.keyCode) { case 13: // return - this._saveInput(true); + this._saveInput('down'); this.endEditing(); break; case 27: // escape @@ -522,7 +523,7 @@ class SelectCellEditor extends CellEditor { } } - _saveInput(returnPressed: boolean = false) { + _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none') { if (!this._select) { return; } @@ -533,7 +534,7 @@ class SelectCellEditor extends CellEditor { return; } - this._resolve({ cell: this._cell, value: value, returnPressed: returnPressed }); + this._resolve({ cell: this._cell, value: value, cursorMovement: cursorMovement }); } endEditing() { @@ -602,7 +603,7 @@ class DateCellEditor extends CellEditor { } } - _saveInput(returnPressed: boolean = false) { + _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none') { if (!this._input) { return; } @@ -613,7 +614,7 @@ class DateCellEditor extends CellEditor { return; } - this._resolve({ cell: this._cell, value: value, returnPressed: returnPressed }); + this._resolve({ cell: this._cell, value: value, cursorMovement: cursorMovement }); } endEditing() { diff --git a/packages/datagrid/src/datagrid.ts b/packages/datagrid/src/datagrid.ts index 036ebd6f6..023011369 100644 --- a/packages/datagrid/src/datagrid.ts +++ b/packages/datagrid/src/datagrid.ts @@ -825,6 +825,61 @@ class DataGrid extends Widget { this.scrollBy(dx, dy); } + /** + * Move cursor down/up while making sure it remains + * within the bounds of selected rectangles + */ + incrementCursor(direction: SelectionModel.CursorMoveDirection): void { + // Bail early if there is no selection + if (!this.dataModel || + !this._selectionModel || + this._selectionModel.isEmpty) { + return; + } + + const iter = this._selectionModel.selections(); + const onlyOne = iter.next() && !iter.next(); + + // if there is a single selection that is a single cell selection + // then move the selection and cursor within grid bounds + if (onlyOne) { + const currentSel = this._selectionModel.currentSelection()!; + if (currentSel.r1 === currentSel.r2 && + currentSel.c1 === currentSel.c2 + ) { + let newRow = currentSel.r1 + (direction === 'down' ? 1 : -1); + let newColumn = currentSel.c1; + const rowCount = this.dataModel.rowCount('body'); + const columnCount = this.dataModel.columnCount('body'); + if (newRow >= rowCount) { + newRow = 0; + newColumn += 1; + } else if (newRow === -1) { + newRow = rowCount - 1; + newColumn -= 1; + } + if (newColumn >= columnCount) { + newColumn = 0; + } else if (newColumn === -1) { + newColumn = columnCount - 1; + } + + this._selectionModel.select({ + r1: newRow, c1: newColumn, + r2: newRow, c2: newColumn, + cursorRow: newRow, cursorColumn: newColumn, + clear: 'all' + }); + + return; + } + } + + // if there are multiple selections, move cursor + // within selection rectangles + this._selectionModel!.incrementCursorWithinSelections(direction); + } + /** * Scroll the grid to the current cursor position. * diff --git a/packages/datagrid/src/selectionmodel.ts b/packages/datagrid/src/selectionmodel.ts index e770d4db3..9896e3d89 100644 --- a/packages/datagrid/src/selectionmodel.ts +++ b/packages/datagrid/src/selectionmodel.ts @@ -60,7 +60,7 @@ abstract class SelectionModel { */ abstract readonly cursorColumn: number; - abstract incrementCursor(): void; + abstract incrementCursorWithinSelections(direction: SelectionModel.CursorMoveDirection): void; /** * Get the current selection in the selection model. @@ -216,6 +216,8 @@ namespace SelectionModel { export type SelectionMode = 'row' | 'column' | 'cell'; + export type CursorMoveDirection = 'up' | 'down' | 'none'; + /** * A type alias for the clear mode. */ From 50d807e454d4d135ee1057c1fbe83ca56b27ef86 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Mon, 14 Oct 2019 17:12:03 -0400 Subject: [PATCH 07/36] default validator implementations --- packages/datagrid/src/celleditor.ts | 161 +++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 4 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index f94968cd1..14cd79eb4 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -42,7 +42,7 @@ class CellEditorController { let key = ''; if (metadata) { - key = metadata.type; + key = metadata.type; } if (metadata.constraint && metadata.constraint.enum) { @@ -52,7 +52,7 @@ class CellEditorController { return key; } - private _getEditor(cell: CellEditor.CellConfig): CellEditor | null { + private _createEditor(cell: CellEditor.CellConfig): CellEditor | null { const key = this._getKey(cell); switch (key) { @@ -81,7 +81,7 @@ class CellEditorController { } edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): Promise { - const editor = this._getEditor(cell); + const editor = this._createEditor(cell); if (editor) { return editor.edit(cell, validator); } @@ -92,15 +92,157 @@ class CellEditorController { } } +export +class TextInputValidator implements ICellInputValidator { + validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { + if (typeof value !== 'string') { + return { + valid: false, + message: 'Input must be valid text' + }; + } + + return { + valid: true + }; + } +}; + +export +class IntegerInputValidator implements ICellInputValidator { + validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { + const parsed = Number.parseInt(value); + if (parsed === Number.NaN) { + return { + valid: false, + message: 'Input must be valid integer' + }; + } + if (this.min !== Number.NaN && parsed < this.min) { + return { + valid: false, + message: `Input must be greater than ${this.min}` + }; + } + + if (this.max !== Number.NaN && parsed > this.max) { + return { + valid: false, + message: `Input must be less than ${this.max}` + }; + } + + return { + valid: true + }; + } + + min: number = Number.NaN; + max: number = Number.NaN; +}; + +export +class NumberInputValidator implements ICellInputValidator { + validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { + const parsed = Number.parseFloat(value); + if (parsed === Number.NaN) { + return { + valid: false, + message: 'Input must be valid number' + }; + } + if (this.min !== Number.NaN && parsed < this.min) { + return { + valid: false, + message: `Input must be greater than ${this.min}` + }; + } + + if (this.max !== Number.NaN && parsed > this.max) { + return { + valid: false, + message: `Input must be less than ${this.max}` + }; + } + + return { + valid: true + }; + } + + min: number = Number.NaN; + max: number = Number.NaN; +}; + +export +class BooleanInputValidator implements ICellInputValidator { + validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { + if (typeof value !== 'boolean') { + return { + valid: false, + message: 'Input must be a valid boolean' + }; + } + + return { + valid: true + }; + } +}; + + export abstract class CellEditor { edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): Promise { return new Promise((resolve, reject) => { this._cell = cell; - this._validator = validator; this._resolve = resolve; this._reject = reject; + if (validator) { + this._validator = validator; + } else { + const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : null; + + switch (metadata && metadata.type) { + case 'string': + this._validator = new TextInputValidator(); + break; + case 'number': + { + const validator = new NumberInputValidator(); + if (metadata!.constraint) { + if (metadata!.constraint.minimum) { + validator.min = metadata!.constraint.minimum; + } + if (metadata!.constraint.maximum) { + validator.max = metadata!.constraint.maximum; + } + } + this._validator = validator; + } + break; + case 'integer': + { + const validator = new IntegerInputValidator(); + if (metadata!.constraint) { + if (metadata!.constraint.minimum) { + validator.min = metadata!.constraint.minimum; + } + if (metadata!.constraint.maximum) { + validator.max = metadata!.constraint.maximum; + } + } + this._validator = validator; + } + break; + case 'boolean': + this._validator = new BooleanInputValidator(); + break; + } + + } + cell.grid.node.addEventListener('wheel', () => { this.updatePosition(); }); @@ -299,6 +441,17 @@ class IntegerCellEditor extends CellEditor { input.spellcheck = false; input.required = false; + const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); + const constraint = metadata.constraint; + if (constraint) { + if (constraint.minimum) { + input.min = constraint.minimum; + } + if (constraint.maximum) { + input.max = constraint.maximum; + } + } + input.value = cellInfo.data; this._input = input; From f22d0063f0d43482a807055d46f55de10e945839 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 15 Oct 2019 10:46:49 -0400 Subject: [PATCH 08/36] fix NaN checks --- packages/datagrid/src/celleditor.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 14cd79eb4..79bfc7502 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -112,20 +112,20 @@ export class IntegerInputValidator implements ICellInputValidator { validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { const parsed = Number.parseInt(value); - if (parsed === Number.NaN) { + if (Number.isNaN(parsed)) { return { valid: false, message: 'Input must be valid integer' }; } - if (this.min !== Number.NaN && parsed < this.min) { + if (!Number.isNaN(this.min) && parsed < this.min) { return { valid: false, message: `Input must be greater than ${this.min}` }; } - if (this.max !== Number.NaN && parsed > this.max) { + if (!Number.isNaN(this.max) && parsed > this.max) { return { valid: false, message: `Input must be less than ${this.max}` @@ -145,20 +145,20 @@ export class NumberInputValidator implements ICellInputValidator { validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { const parsed = Number.parseFloat(value); - if (parsed === Number.NaN) { + if (Number.isNaN(parsed)) { return { valid: false, message: 'Input must be valid number' }; } - if (this.min !== Number.NaN && parsed < this.min) { + if (!Number.isNaN(this.min) && parsed < this.min) { return { valid: false, message: `Input must be greater than ${this.min}` }; } - if (this.max !== Number.NaN && parsed > this.max) { + if (!Number.isNaN(this.max) && parsed > this.max) { return { valid: false, message: `Input must be less than ${this.max}` From 263cb41cf1062b9f10bf995da7eb7f304fc17d4d Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 15 Oct 2019 13:28:40 -0400 Subject: [PATCH 09/36] use signals instead of promise --- packages/datagrid/src/basickeyhandler.ts | 29 ++-- packages/datagrid/src/basicmousehandler.ts | 32 ++-- packages/datagrid/src/celleditor.ts | 189 +++++++++++---------- 3 files changed, 134 insertions(+), 116 deletions(-) diff --git a/packages/datagrid/src/basickeyhandler.ts b/packages/datagrid/src/basickeyhandler.ts index 4bf36cecc..13e01ca65 100644 --- a/packages/datagrid/src/basickeyhandler.ts +++ b/packages/datagrid/src/basickeyhandler.ts @@ -65,22 +65,27 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { const row = grid.selectionModel.cursorRow; const column = grid.selectionModel.cursorColumn; if (row != -1 && column != -1) { - grid.cellEditorController.edit({ + const cell = { grid: grid, row: row, column: column, metadata: grid.dataModel!.metadata('body', row, column) - }).then((response: ICellEditResponse) => { - if (grid.dataModel instanceof MutableDataModel) { - const dataModel = grid.dataModel as MutableDataModel; - dataModel.setData('body', row, column, response.value); - } - grid.viewport.node.focus(); - if (response.cursorMovement) { - grid.incrementCursor(event.shiftKey ? 'up' : 'down'); - grid.scrollToCursor(); - } - }); + }; + const editor = grid.cellEditorController.createEditor(cell); + if (editor) { + editor.onCommit.connect((_, args: ICellEditResponse) => { + if (grid.dataModel instanceof MutableDataModel) { + const dataModel = grid.dataModel as MutableDataModel; + dataModel.setData('body', row, column, args.value); + } + grid.viewport.node.focus(); + if (args.cursorMovement) { + grid.incrementCursor(event.shiftKey ? 'up' : 'down'); + grid.scrollToCursor(); + } + }); + editor.edit(cell); + } } } } diff --git a/packages/datagrid/src/basicmousehandler.ts b/packages/datagrid/src/basicmousehandler.ts index aea285f12..1869ecc05 100644 --- a/packages/datagrid/src/basicmousehandler.ts +++ b/packages/datagrid/src/basicmousehandler.ts @@ -516,23 +516,27 @@ class BasicMouseHandler implements DataGrid.IMouseHandler { } if (region === 'body') { - grid.cellEditorController - .edit({ + const cell = { grid: grid, row: row, column: column, - metadata: grid.dataModel!.metadata('body', row, column)}) - .then((response: ICellEditResponse) => { - if (grid.dataModel instanceof MutableDataModel) { - const dataModel = grid.dataModel as MutableDataModel; - dataModel.setData('body', row, column, response.value); - } - grid.viewport.node.focus(); - if (response.cursorMovement !== 'none') { - grid.incrementCursor(response.cursorMovement); - grid.scrollToCursor(); - } - }); + metadata: grid.dataModel!.metadata('body', row, column) + }; + const editor = grid.cellEditorController.createEditor(cell); + if (editor) { + editor.onCommit.connect((_, args: ICellEditResponse) => { + if (grid.dataModel instanceof MutableDataModel) { + const dataModel = grid.dataModel as MutableDataModel; + dataModel.setData('body', row, column, args.value); + } + grid.viewport.node.focus(); + if (args.cursorMovement !== 'none') { + grid.incrementCursor(args.cursorMovement); + grid.scrollToCursor(); + } + }); + editor.edit(cell); + } } this.release(); diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 79bfc7502..8df04ba74 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -7,24 +7,32 @@ import { } from './datagrid'; import { DataModel } from './datamodel'; import { SelectionModel } from './selectionmodel'; - -export -interface ICellEditResponse { - cell: CellEditor.CellConfig; - value: any; - cursorMovement: SelectionModel.CursorMoveDirection; -}; +import { Signal, ISignal } from '@phosphor/signaling'; export interface ICellInputValidatorResponse { valid: boolean; message?: string; -}; +} export interface ICellInputValidator { validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse; -}; +} + +export +interface ICellEditResponse { + cell: CellEditor.CellConfig; + value: any; + cursorMovement: SelectionModel.CursorMoveDirection; +} + +export +interface ICellEditor { + edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): void; + readonly onCommit: ISignal; + readonly onCancel?: ISignal; +} const DEFAULT_INVALID_INPUT_MESSAGE = "Invalid input!"; @@ -52,7 +60,7 @@ class CellEditorController { return key; } - private _createEditor(cell: CellEditor.CellConfig): CellEditor | null { + createEditor(cell: CellEditor.CellConfig): ICellEditor | null { const key = this._getKey(cell); switch (key) { @@ -80,15 +88,21 @@ class CellEditorController { return null; } - edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): Promise { - const editor = this._createEditor(cell); + edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): boolean { + const editor = this.createEditor(cell); if (editor) { - return editor.edit(cell, validator); + editor.edit(cell, validator); + return true; } - return new Promise((resolve, reject) => { - reject('Editor not found'); - }); + return false; + } +} + +export +class PassInputValidator implements ICellInputValidator { + validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { + return { valid: true }; } } @@ -102,11 +116,9 @@ class TextInputValidator implements ICellInputValidator { }; } - return { - valid: true - }; + return { valid: true }; } -}; +} export class IntegerInputValidator implements ICellInputValidator { @@ -132,14 +144,12 @@ class IntegerInputValidator implements ICellInputValidator { }; } - return { - valid: true - }; + return { valid: true }; } min: number = Number.NaN; max: number = Number.NaN; -}; +} export class NumberInputValidator implements ICellInputValidator { @@ -165,14 +175,12 @@ class NumberInputValidator implements ICellInputValidator { }; } - return { - valid: true - }; + return { valid: true }; } min: number = Number.NaN; max: number = Number.NaN; -}; +} export class BooleanInputValidator implements ICellInputValidator { @@ -184,73 +192,66 @@ class BooleanInputValidator implements ICellInputValidator { }; } - return { - valid: true - }; + return { valid: true }; } -}; - +} export -abstract class CellEditor { - edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): Promise { - return new Promise((resolve, reject) => { - this._cell = cell; - this._resolve = resolve; - this._reject = reject; - - if (validator) { - this._validator = validator; - } else { - const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : null; - - switch (metadata && metadata.type) { - case 'string': - this._validator = new TextInputValidator(); - break; - case 'number': - { - const validator = new NumberInputValidator(); - if (metadata!.constraint) { - if (metadata!.constraint.minimum) { - validator.min = metadata!.constraint.minimum; - } - if (metadata!.constraint.maximum) { - validator.max = metadata!.constraint.maximum; - } +abstract class CellEditor implements ICellEditor { + edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator) { + this._cell = cell; + + if (validator) { + this._validator = validator; + } else { + const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : null; + + switch (metadata && metadata.type) { + case 'string': + this._validator = new TextInputValidator(); + break; + case 'number': + { + const validator = new NumberInputValidator(); + if (metadata!.constraint) { + if (metadata!.constraint.minimum) { + validator.min = metadata!.constraint.minimum; + } + if (metadata!.constraint.maximum) { + validator.max = metadata!.constraint.maximum; } - this._validator = validator; } - break; - case 'integer': - { - const validator = new IntegerInputValidator(); - if (metadata!.constraint) { - if (metadata!.constraint.minimum) { - validator.min = metadata!.constraint.minimum; - } - if (metadata!.constraint.maximum) { - validator.max = metadata!.constraint.maximum; - } + this._validator = validator; + } + break; + case 'integer': + { + const validator = new IntegerInputValidator(); + if (metadata!.constraint) { + if (metadata!.constraint.minimum) { + validator.min = metadata!.constraint.minimum; + } + if (metadata!.constraint.maximum) { + validator.max = metadata!.constraint.maximum; } - this._validator = validator; } + this._validator = validator; + } + break; + case 'boolean': + this._validator = new BooleanInputValidator(); break; - case 'boolean': - this._validator = new BooleanInputValidator(); - break; - } - } - cell.grid.node.addEventListener('wheel', () => { - this.updatePosition(); - }); - - this._addContainer(); + } - this.startEditing(); + cell.grid.node.addEventListener('wheel', () => { + this.updatePosition(); }); + + this._addContainer(); + + this.startEditing(); } protected getCellInfo(cell: CellEditor.CellConfig) { @@ -321,8 +322,16 @@ abstract class CellEditor { return this._validInput; } - protected _resolve: {(response: ICellEditResponse): void }; - protected _reject: {(reason: any): void }; + get onCommit(): ISignal { + return this._onCommit; + } + + get onCancel(): ISignal { + return this._onCancel; + } + + protected _onCommit = new Signal(this); + protected _onCancel = new Signal(this); protected _cell: CellEditor.CellConfig; protected _validator: ICellInputValidator | undefined; protected _viewportOccluder: HTMLDivElement; @@ -408,7 +417,7 @@ class TextCellEditor extends CellEditor { } } - this._resolve({ cell: this._cell, value: value, cursorMovement: cursorMovement }); + this._onCommit.emit({ cell: this._cell, value: value, cursorMovement: cursorMovement }); return true; } @@ -511,7 +520,7 @@ class IntegerCellEditor extends CellEditor { } } - this._resolve({ cell: this._cell, value: value, cursorMovement: cursorMovement }); + this._onCommit.emit({ cell: this._cell, value: value, cursorMovement: cursorMovement }); return true; } @@ -602,7 +611,7 @@ class BooleanCellEditor extends CellEditor { } } - this._resolve({ cell: this._cell, value: value, cursorMovement: cursorMovement }); + this._onCommit.emit({ cell: this._cell, value: value, cursorMovement: cursorMovement }); return true; } @@ -687,7 +696,7 @@ class SelectCellEditor extends CellEditor { return; } - this._resolve({ cell: this._cell, value: value, cursorMovement: cursorMovement }); + this._onCommit.emit({ cell: this._cell, value: value, cursorMovement: cursorMovement }); } endEditing() { @@ -767,7 +776,7 @@ class DateCellEditor extends CellEditor { return; } - this._resolve({ cell: this._cell, value: value, cursorMovement: cursorMovement }); + this._onCommit.emit({ cell: this._cell, value: value, cursorMovement: cursorMovement }); } endEditing() { From 9e6ad27a42b5c79d89ade6e429901bd325521c2e Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 15 Oct 2019 15:25:46 -0400 Subject: [PATCH 10/36] refactoring --- packages/datagrid/src/celleditor.ts | 194 +++++++++++++++++++++------- 1 file changed, 146 insertions(+), 48 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 8df04ba74..16aa49bf7 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -5,8 +5,11 @@ import { import { DataGrid } from './datagrid'; + import { DataModel } from './datamodel'; + import { SelectionModel } from './selectionmodel'; + import { Signal, ISignal } from '@phosphor/signaling'; export @@ -36,7 +39,6 @@ interface ICellEditor { const DEFAULT_INVALID_INPUT_MESSAGE = "Invalid input!"; -//type CellDataType = 'string' | 'integer' | 'number' | 'boolean' | 'date' | string; export class CellEditorController { @@ -65,8 +67,9 @@ class CellEditorController { switch (key) { case 'string': - case 'number': return new TextCellEditor(); + case 'number': + return new NumberCellEditor(); case 'integer': return new IntegerCellEditor(); case 'boolean': @@ -108,7 +111,7 @@ class PassInputValidator implements ICellInputValidator { export class TextInputValidator implements ICellInputValidator { - validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { + validate(cell: CellEditor.CellConfig, value: string): ICellInputValidatorResponse { if (typeof value !== 'string') { return { valid: false, @@ -122,22 +125,21 @@ class TextInputValidator implements ICellInputValidator { export class IntegerInputValidator implements ICellInputValidator { - validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { - const parsed = Number.parseInt(value); - if (Number.isNaN(parsed)) { + validate(cell: CellEditor.CellConfig, value: number): ICellInputValidatorResponse { + if (Number.isNaN(value) || (value % 1 !== 0)) { return { valid: false, message: 'Input must be valid integer' }; } - if (!Number.isNaN(this.min) && parsed < this.min) { + if (!Number.isNaN(this.min) && value < this.min) { return { valid: false, message: `Input must be greater than ${this.min}` }; } - if (!Number.isNaN(this.max) && parsed > this.max) { + if (!Number.isNaN(this.max) && value > this.max) { return { valid: false, message: `Input must be less than ${this.max}` @@ -153,22 +155,21 @@ class IntegerInputValidator implements ICellInputValidator { export class NumberInputValidator implements ICellInputValidator { - validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { - const parsed = Number.parseFloat(value); - if (Number.isNaN(parsed)) { + validate(cell: CellEditor.CellConfig, value: number): ICellInputValidatorResponse { + if (Number.isNaN(value)) { return { valid: false, message: 'Input must be valid number' }; } - if (!Number.isNaN(this.min) && parsed < this.min) { + if (!Number.isNaN(this.min) && value < this.min) { return { valid: false, message: `Input must be greater than ${this.min}` }; } - if (!Number.isNaN(this.max) && parsed > this.max) { + if (!Number.isNaN(this.max) && value > this.max) { return { valid: false, message: `Input must be less than ${this.max}` @@ -182,19 +183,6 @@ class NumberInputValidator implements ICellInputValidator { max: number = Number.NaN; } -export -class BooleanInputValidator implements ICellInputValidator { - validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { - if (typeof value !== 'boolean') { - return { - valid: false, - message: 'Input must be a valid boolean' - }; - } - - return { valid: true }; - } -} export abstract class CellEditor implements ICellEditor { @@ -238,9 +226,6 @@ abstract class CellEditor implements ICellEditor { this._validator = validator; } break; - case 'boolean': - this._validator = new BooleanInputValidator(); - break; } } @@ -283,6 +268,10 @@ abstract class CellEditor implements ICellEditor { this._cellContainer = document.createElement('div'); this._cellContainer.className = 'cell-editor-container'; this._viewportOccluder.appendChild(this._cellContainer); + + this._form = document.createElement('form'); + this._form.className = 'cell-editor-form'; + this._cellContainer.appendChild(this._form); } protected abstract startEditing(): void; @@ -336,6 +325,7 @@ abstract class CellEditor implements ICellEditor { protected _validator: ICellInputValidator | undefined; protected _viewportOccluder: HTMLDivElement; protected _cellContainer: HTMLDivElement; + protected _form: HTMLFormElement; private _validInput: boolean = true; } @@ -345,7 +335,6 @@ class TextCellEditor extends CellEditor { const cell = this._cell; const cellInfo = this.getCellInfo(cell); - const form = document.createElement('form'); const input = document.createElement('input'); input.classList.add('cell-editor'); input.classList.add('input-cell-editor'); @@ -355,10 +344,8 @@ class TextCellEditor extends CellEditor { input.value = cellInfo.data; this._input = input; - this._form = form; - form.appendChild(input); - this._cellContainer.appendChild(form); + this._form.appendChild(input); this.updatePosition(); @@ -433,7 +420,114 @@ class TextCellEditor extends CellEditor { } _input: HTMLInputElement | null; - _form: HTMLFormElement; +} + +export +class NumberCellEditor extends CellEditor { + startEditing() { + const cell = this._cell; + const cellInfo = this.getCellInfo(cell); + + const input = document.createElement('input'); + input.classList.add('cell-editor'); + input.classList.add('input-cell-editor'); + input.spellcheck = false; + input.required = false; + + const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); + const constraint = metadata.constraint; + if (constraint) { + if (constraint.minimum) { + input.min = constraint.minimum; + } + if (constraint.maximum) { + input.max = constraint.maximum; + } + } + + input.value = cellInfo.data; + + this._input = input; + + this._form.appendChild(input); + + this.updatePosition(); + + input.select(); + input.focus(); + + input.addEventListener("keydown", (event) => { + this._onKeyDown(event); + }); + + input.addEventListener("blur", (event) => { + if (this._saveInput()) { + this.endEditing(); + } + }); + + input.addEventListener("input", (event) => { + this._input!.setCustomValidity(""); + this.validInput = true; + }); + } + + _onKeyDown(event: KeyboardEvent) { + switch (event.keyCode) { + case 13: // return + if (this._saveInput('down')) { + this.endEditing(); + } + break; + case 27: // escape + this.endEditing(); + break; + default: + break; + } + } + + _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none'): boolean { + if (!this._input) { + return false; + } + + let value = this._input.value; + let floatValue = Number.parseFloat(value); + if (Number.isNaN(floatValue)) { + this._input.setCustomValidity('Input must be valid integer'); + this._form.reportValidity(); + return false; + } + + this._input.value = floatValue.toString(); + + if (this._validator) { + const result = this._validator.validate(this._cell, floatValue); + if (!result.valid) { + this.validInput = false; + this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); + this._form.reportValidity(); + return false; + } + } + + this._onCommit.emit({ cell: this._cell, value: floatValue, cursorMovement: cursorMovement }); + + return true; + } + + endEditing() { + if (!this._input) { + return; + } + + this._input = null; + + super.endEditing(); + } + + _input: HTMLInputElement | null; } export @@ -442,7 +536,6 @@ class IntegerCellEditor extends CellEditor { const cell = this._cell; const cellInfo = this.getCellInfo(cell); - const form = document.createElement('form'); const input = document.createElement('input'); input.classList.add('cell-editor'); input.classList.add('input-cell-editor'); @@ -465,8 +558,7 @@ class IntegerCellEditor extends CellEditor { this._input = input; - form.appendChild(input); - this._cellContainer.appendChild(form); + this._form.appendChild(input); this.updatePosition(); @@ -509,18 +601,27 @@ class IntegerCellEditor extends CellEditor { return false; } - const value = this._input.value; + let value = this._input.value; + let intValue = Number.parseInt(value); + if (Number.isNaN(intValue)) { + this._input.setCustomValidity('Input must be valid integer'); + this._form.reportValidity(); + return false; + } + + this._input.value = intValue.toString(); + if (this._validator) { - const result = this._validator.validate(this._cell, value); + const result = this._validator.validate(this._cell, intValue); if (!result.valid) { this.validInput = false; this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); - this._input.checkValidity(); + this._form.reportValidity(); return false; } } - this._onCommit.emit({ cell: this._cell, value: value, cursorMovement: cursorMovement }); + this._onCommit.emit({ cell: this._cell, value: intValue, cursorMovement: cursorMovement }); return true; } @@ -544,7 +645,6 @@ class BooleanCellEditor extends CellEditor { const cell = this._cell; const cellInfo = this.getCellInfo(cell); - const form = document.createElement('form'); const input = document.createElement('input'); input.classList.add('cell-editor'); input.classList.add('boolean-cell-editor'); @@ -555,9 +655,7 @@ class BooleanCellEditor extends CellEditor { input.checked = cellInfo.data == true; this._input = input; - - form.appendChild(input); - this._cellContainer.appendChild(form); + this._form.appendChild(input); this.updatePosition(); @@ -606,7 +704,7 @@ class BooleanCellEditor extends CellEditor { if (!result.valid) { this.validInput = false; this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); - this._input.checkValidity(); + this._form.reportValidity(); return false; } } @@ -651,7 +749,7 @@ class SelectCellEditor extends CellEditor { this._select = select; - this._cellContainer.appendChild(select); + this._form.appendChild(select); this.updatePosition(); select.focus(); @@ -729,7 +827,7 @@ class DateCellEditor extends CellEditor { this._input = input; - this._cellContainer.appendChild(input); + this._form.appendChild(input); this.updatePosition(); From 0559b55cc3a0e820c15714aef78f31497989a476 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Wed, 16 Oct 2019 16:47:06 -0400 Subject: [PATCH 11/36] support deleting cell values using keyboard --- packages/datagrid/src/basickeyhandler.ts | 41 ++++++++++++++++++++++ packages/datagrid/src/basicmousehandler.ts | 3 ++ 2 files changed, 44 insertions(+) diff --git a/packages/datagrid/src/basickeyhandler.ts b/packages/datagrid/src/basickeyhandler.ts index 13e01ca65..5f2fbc9bc 100644 --- a/packages/datagrid/src/basickeyhandler.ts +++ b/packages/datagrid/src/basickeyhandler.ts @@ -84,6 +84,9 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { grid.scrollToCursor(); } }); + editor.onCancel.connect((_, args: void) => { + grid.viewport.node.focus(); + }); editor.edit(cell); } } @@ -113,6 +116,9 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { case 'Escape': this.onEscape(grid, event); break; + case 'Delete': + this.onDelete(grid, event); + break; case 'C': this.onKeyC(grid, event); break; @@ -748,6 +754,41 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { } } + /** + * Handle the `'Delete'` key press for the data grid. + * + * @param grid - The data grid of interest. + * + * @param event - The keyboard event of interest. + */ + protected onDelete(grid: DataGrid, event: KeyboardEvent): void { + if (grid.selectionModel && + !grid.selectionModel.isEmpty && + grid.dataModel instanceof MutableDataModel) { + + const dataModel = grid.dataModel as MutableDataModel; + // Fetch the max row and column. + let maxRow = dataModel.rowCount('body') - 1; + let maxColumn = dataModel.columnCount('body') - 1; + + const it = grid.selectionModel.selections(); + let s: SelectionModel.Selection | undefined; + while ((s = it.next()) !== undefined) { + // Clamp the cell to the model bounds. + let sr1 = Math.max(0, Math.min(s.r1, maxRow)); + let sc1 = Math.max(0, Math.min(s.c1, maxColumn)); + let sr2 = Math.max(0, Math.min(s.r2, maxRow)); + let sc2 = Math.max(0, Math.min(s.c2, maxColumn)); + + for (let r = sr1; r <= sr2; ++r) { + for (let c = sc1; c <= sc2; ++c) { + dataModel.setData('body', r, c, null); + } + } + } + } + } + /** * Handle the `'C'` key press for the data grid. * diff --git a/packages/datagrid/src/basicmousehandler.ts b/packages/datagrid/src/basicmousehandler.ts index 1869ecc05..ed6a0529e 100644 --- a/packages/datagrid/src/basicmousehandler.ts +++ b/packages/datagrid/src/basicmousehandler.ts @@ -535,6 +535,9 @@ class BasicMouseHandler implements DataGrid.IMouseHandler { grid.scrollToCursor(); } }); + editor.onCancel.connect((_, args: void) => { + grid.viewport.node.focus(); + }); editor.edit(cell); } } From cc6ba2f390780e2495296a2d54a8fdc11258859f Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Wed, 16 Oct 2019 16:47:33 -0400 Subject: [PATCH 12/36] refactoring --- packages/datagrid/src/celleditor.ts | 687 ++++++++++++------------ packages/datagrid/src/selectionmodel.ts | 2 +- 2 files changed, 336 insertions(+), 353 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 16aa49bf7..1bf40a156 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -2,6 +2,10 @@ import { Widget } from '@phosphor/widgets'; +import { + IDisposable +} from '@phosphor/disposable'; + import { DataGrid } from './datagrid'; @@ -12,6 +16,8 @@ import { SelectionModel } from './selectionmodel'; import { Signal, ISignal } from '@phosphor/signaling'; +import { getKeyboardLayout } from '@phosphor/keyboard'; + export interface ICellInputValidatorResponse { valid: boolean; @@ -34,7 +40,7 @@ export interface ICellEditor { edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): void; readonly onCommit: ISignal; - readonly onCancel?: ISignal; + readonly onCancel: ISignal; } const DEFAULT_INVALID_INPUT_MESSAGE = "Invalid input!"; @@ -185,7 +191,26 @@ class NumberInputValidator implements ICellInputValidator { export -abstract class CellEditor implements ICellEditor { +abstract class CellEditor implements ICellEditor, IDisposable { + protected abstract startEditing(): void; + protected abstract serialize(): any; + protected abstract deserialize(value: any): any; + + /** + * Whether the cell editor is disposed. + */ + get isDisposed(): boolean { + return this._disposed; + } + + /** + * Dispose of the resources held by cell editor handler. + */ + dispose(): void { + this._disposed = true; + this._cell.grid.node.removeChild(this._viewportOccluder); + } + edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator) { this._cell = cell; @@ -227,7 +252,6 @@ abstract class CellEditor implements ICellEditor { } break; } - } cell.grid.node.addEventListener('wheel', () => { @@ -236,6 +260,7 @@ abstract class CellEditor implements ICellEditor { this._addContainer(); + this.updatePosition(); this.startEditing(); } @@ -274,8 +299,6 @@ abstract class CellEditor implements ICellEditor { this._cellContainer.appendChild(this._form); } - protected abstract startEditing(): void; - protected updatePosition(): void { const grid = this._cell.grid; const cellInfo = this.getCellInfo(this._cell); @@ -294,10 +317,6 @@ abstract class CellEditor implements ICellEditor { this._cellContainer.style.visibility = 'visible'; } - protected endEditing(): void { - this._cell.grid.node.removeChild(this._viewportOccluder); - } - protected set validInput(value: boolean) { this._validInput = value; if (this._validInput) { @@ -311,6 +330,25 @@ abstract class CellEditor implements ICellEditor { return this._validInput; } + protected commit(cursorMovement: SelectionModel.CursorMoveDirection = 'none'): boolean { + let value; + try { + value = this.serialize(); + } catch (error) { + console.error(error); + return false; + } + + this.dispose(); + this._onCommit.emit({ + cell: this._cell, + value: value, + cursorMovement: cursorMovement + }); + + return true; + } + get onCommit(): ISignal { return this._onCommit; } @@ -327,411 +365,425 @@ abstract class CellEditor implements ICellEditor { protected _cellContainer: HTMLDivElement; protected _form: HTMLFormElement; private _validInput: boolean = true; + private _disposed = false; } export class TextCellEditor extends CellEditor { startEditing() { + this._createWidget(); + const cell = this._cell; const cellInfo = this.getCellInfo(cell); + this._input.value = this.deserialize(cellInfo.data); + this._form.appendChild(this._input); + this._input.select(); + this._input.focus(); + + this._bindEvents(); + } + _createWidget() { const input = document.createElement('input'); input.classList.add('cell-editor'); input.classList.add('input-cell-editor'); input.spellcheck = false; - input.required = false; - - input.value = cellInfo.data; this._input = input; + } - this._form.appendChild(input); - - this.updatePosition(); - - input.select(); - input.focus(); - - input.addEventListener("keydown", (event) => { - this._onKeyDown(event); - }); + _bindEvents() { + this._input.addEventListener('keydown', this); + this._input.addEventListener('blur', this); + this._input.addEventListener('input', this); + } - input.addEventListener("blur", (event) => { - if (this._saveInput()) { - this.endEditing(); - event.preventDefault(); - event.stopPropagation(); - this._input!.focus(); - } - }); + _unbindEvents() { + this._input.removeEventListener('keydown', this); + this._input.removeEventListener('blur', this); + this._input.removeEventListener('input', this); + } - input.addEventListener("input", (event) => { - this._input!.setCustomValidity(""); - this.validInput = true; - }); + handleEvent(event: Event): void { + switch (event.type) { + case 'keydown': + this._onKeyDown(event as KeyboardEvent); + break; + case 'blur': + this._onBlur(event as FocusEvent); + break; + case 'input': + this._onInput(event); + break; + } } _onKeyDown(event: KeyboardEvent) { - switch (event.keyCode) { - case 13: // return - if (this._saveInput(event.shiftKey ? 'up' : 'down')) { - this.endEditing(); - event.preventDefault(); - event.stopPropagation(); - } + switch (getKeyboardLayout().keyForKeydownEvent(event)) { + case 'Enter': + this.commit(event.shiftKey ? 'up' : 'down'); break; - case 27: // escape - this.endEditing(); + case 'Tab': + this.commit(event.shiftKey ? 'left' : 'right'); + break; + case 'Escape': + this.dispose(); break; default: break; } } - _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none'): boolean { - if (!this._input) { - return false; + _onBlur(event: FocusEvent) { + if (!this.commit()) { + event.preventDefault(); + event.stopPropagation(); + this._input.focus(); } + } + + _onInput(event: Event) { + this._input.setCustomValidity(""); + this.validInput = true; + } + protected serialize(): any { const value = this._input.value; + + if (value.trim() === '') { + return null; + } + if (this._validator) { const result = this._validator.validate(this._cell, value); if (!result.valid) { this.validInput = false; this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); this._form.reportValidity(); - return false; + throw new Error('Invalid input'); } } - this._onCommit.emit({ cell: this._cell, value: value, cursorMovement: cursorMovement }); + return value; + } - return true; + protected deserialize(value: any): any { + if (value === null || value === undefined) { + return ''; + } + + return value.toString(); } - endEditing() { - if (!this._input) { + dispose() { + if (this.isDisposed) { return; } - this._input = null; + this._unbindEvents(); - super.endEditing(); + super.dispose(); } - _input: HTMLInputElement | null; + protected _input: HTMLInputElement; +} + +export +class NumberCellEditor extends TextCellEditor { + protected serialize(): any { + let value = this._input.value; + if (value.trim() === '') { + return null; + } + + let floatValue = Number.parseFloat(value); + if (Number.isNaN(floatValue)) { + this._input.setCustomValidity('Input must be valid number'); + this._form.reportValidity(); + throw new Error('Invalid input'); + } + + this._input.value = floatValue.toString(); + + if (this._validator) { + const result = this._validator.validate(this._cell, floatValue); + if (!result.valid) { + this.validInput = false; + this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); + this._form.reportValidity(); + throw new Error('Invalid input'); + } + } + + return floatValue; + } } export -class NumberCellEditor extends CellEditor { +class IntegerCellEditor extends NumberCellEditor { startEditing() { + this._createWidget(); + this._input.type = 'number'; + const cell = this._cell; const cellInfo = this.getCellInfo(cell); - - const input = document.createElement('input'); - input.classList.add('cell-editor'); - input.classList.add('input-cell-editor'); - input.spellcheck = false; - input.required = false; + this._input.value = this.deserialize(cellInfo.data); const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); const constraint = metadata.constraint; if (constraint) { if (constraint.minimum) { - input.min = constraint.minimum; + this._input.min = constraint.minimum; } if (constraint.maximum) { - input.max = constraint.maximum; + this._input.max = constraint.maximum; } } - input.value = cellInfo.data; + this._form.appendChild(this._input); + this._input.select(); + this._input.focus(); - this._input = input; - - this._form.appendChild(input); - - this.updatePosition(); - - input.select(); - input.focus(); - - input.addEventListener("keydown", (event) => { - this._onKeyDown(event); - }); - - input.addEventListener("blur", (event) => { - if (this._saveInput()) { - this.endEditing(); - } - }); - - input.addEventListener("input", (event) => { - this._input!.setCustomValidity(""); - this.validInput = true; - }); + this._bindEvents(); } - _onKeyDown(event: KeyboardEvent) { - switch (event.keyCode) { - case 13: // return - if (this._saveInput('down')) { - this.endEditing(); - } - break; - case 27: // escape - this.endEditing(); - break; - default: - break; - } - } - - _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none'): boolean { - if (!this._input) { - return false; + protected serialize(): any { + let value = this._input.value; + if (value.trim() === '') { + return null; } - let value = this._input.value; - let floatValue = Number.parseFloat(value); - if (Number.isNaN(floatValue)) { - this._input.setCustomValidity('Input must be valid integer'); + let intValue = Number.parseInt(value); + if (Number.isNaN(intValue)) { + this._input.setCustomValidity('Input must be valid number'); this._form.reportValidity(); - return false; + throw new Error('Invalid input'); } - this._input.value = floatValue.toString(); + this._input.value = intValue.toString(); if (this._validator) { - const result = this._validator.validate(this._cell, floatValue); + const result = this._validator.validate(this._cell, intValue); if (!result.valid) { this.validInput = false; this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); this._form.reportValidity(); - return false; + throw new Error('Invalid input'); } } - this._onCommit.emit({ cell: this._cell, value: floatValue, cursorMovement: cursorMovement }); - - return true; + return intValue; } - - endEditing() { - if (!this._input) { - return; - } - - this._input = null; - - super.endEditing(); - } - - _input: HTMLInputElement | null; } export -class IntegerCellEditor extends CellEditor { +class DateCellEditor extends CellEditor { startEditing() { + this._createWidget(); + const cell = this._cell; const cellInfo = this.getCellInfo(cell); + this._input.value = this.deserialize(cellInfo.data); + this._form.appendChild(this._input); + this._input.focus(); + + this._bindEvents(); + } + _createWidget() { const input = document.createElement('input'); + input.type = 'date'; + input.pattern = "\d{4}-\d{2}-\d{2}"; input.classList.add('cell-editor'); input.classList.add('input-cell-editor'); - input.type = 'number'; - input.spellcheck = false; - input.required = false; - - const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); - const constraint = metadata.constraint; - if (constraint) { - if (constraint.minimum) { - input.min = constraint.minimum; - } - if (constraint.maximum) { - input.max = constraint.maximum; - } - } - - input.value = cellInfo.data; this._input = input; + } - this._form.appendChild(input); - - this.updatePosition(); - - input.select(); - input.focus(); - - input.addEventListener("keydown", (event) => { - this._onKeyDown(event); - }); + _bindEvents() { + this._input.addEventListener('keydown', this); + this._input.addEventListener('blur', this); + } - input.addEventListener("blur", (event) => { - if (this._saveInput()) { - this.endEditing(); - } - }); + _unbindEvents() { + this._input.removeEventListener('keydown', this); + this._input.removeEventListener('blur', this); + } - input.addEventListener("input", (event) => { - this._input!.setCustomValidity(""); - this.validInput = true; - }); + handleEvent(event: Event): void { + switch (event.type) { + case 'keydown': + this._onKeyDown(event as KeyboardEvent); + break; + case 'blur': + this._onBlur(event as FocusEvent); + break; + } } _onKeyDown(event: KeyboardEvent) { - switch (event.keyCode) { - case 13: // return - if (this._saveInput('down')) { - this.endEditing(); - } + switch (getKeyboardLayout().keyForKeydownEvent(event)) { + case 'Enter': + this.commit(event.shiftKey ? 'up' : 'down'); + break; + case 'Tab': + this.commit(event.shiftKey ? 'left' : 'right'); break; - case 27: // escape - this.endEditing(); + case 'Escape': + this.dispose(); break; default: break; } } - _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none'): boolean { - if (!this._input) { - return false; - } - - let value = this._input.value; - let intValue = Number.parseInt(value); - if (Number.isNaN(intValue)) { - this._input.setCustomValidity('Input must be valid integer'); - this._form.reportValidity(); - return false; + _onBlur(event: FocusEvent) { + if (!this.commit()) { + event.preventDefault(); + event.stopPropagation(); + this._input.focus(); } + } - this._input.value = intValue.toString(); + protected serialize(): any { + const value = this._input.value; if (this._validator) { - const result = this._validator.validate(this._cell, intValue); + const result = this._validator.validate(this._cell, value); if (!result.valid) { this.validInput = false; this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); this._form.reportValidity(); - return false; + throw new Error('Invalid input'); } } - this._onCommit.emit({ cell: this._cell, value: intValue, cursorMovement: cursorMovement }); - - return true; + return value; } - endEditing() { - if (!this._input) { - return; + protected deserialize(value: any): any { + if (value === null || value === undefined) { + return ''; } - this._input = null; - - super.endEditing(); + return value.toString(); } - _input: HTMLInputElement | null; + _input: HTMLInputElement; } export class BooleanCellEditor extends CellEditor { startEditing() { + this._createWidget(); + const cell = this._cell; const cellInfo = this.getCellInfo(cell); + this._input.checked = this.deserialize(cellInfo.data); + this._form.appendChild(this._input); + this._input.focus(); + this._bindEvents(); + } + + _createWidget() { const input = document.createElement('input'); input.classList.add('cell-editor'); input.classList.add('boolean-cell-editor'); input.type = 'checkbox'; input.spellcheck = false; - input.required = false; - - input.checked = cellInfo.data == true; this._input = input; - this._form.appendChild(input); - - this.updatePosition(); - - input.select(); - input.focus(); + } - input.addEventListener("keydown", (event) => { - this._onKeyDown(event); - }); + _bindEvents() { + this._input.addEventListener('keydown', this); + this._input.addEventListener('blur', this); + } - input.addEventListener("blur", (event) => { - if (this._saveInput()) { - this.endEditing(); - } - }); + _unbindEvents() { + this._input.removeEventListener('keydown', this); + this._input.removeEventListener('blur', this); + } - input.addEventListener("input", (event) => { - this._input!.setCustomValidity(""); - this.validInput = true; - }); + handleEvent(event: Event): void { + switch (event.type) { + case 'keydown': + this._onKeyDown(event as KeyboardEvent); + break; + case 'blur': + this._onBlur(event as FocusEvent); + break; + } } _onKeyDown(event: KeyboardEvent) { - switch (event.keyCode) { - case 13: // return - if (this._saveInput('down')) { - this.endEditing(); - } + switch (getKeyboardLayout().keyForKeydownEvent(event)) { + case 'Enter': + this.commit(event.shiftKey ? 'up' : 'down'); break; - case 27: // escape - this.endEditing(); + case 'Tab': + this.commit(event.shiftKey ? 'left' : 'right'); + break; + case 'Escape': + this.dispose(); break; default: break; } } - _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none'): boolean { - if (!this._input) { - return false; + _onBlur(event: FocusEvent) { + if (!this.commit()) { + event.preventDefault(); + event.stopPropagation(); + this._input.focus(); } + } + protected serialize(): any { const value = this._input.checked; + if (this._validator) { const result = this._validator.validate(this._cell, value); if (!result.valid) { this.validInput = false; this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); this._form.reportValidity(); - return false; + throw new Error('Invalid input'); } } - this._onCommit.emit({ cell: this._cell, value: value, cursorMovement: cursorMovement }); - - return true; + return value; } - endEditing() { - if (!this._input) { - return; + protected deserialize(value: any): any { + if (value === null || value === undefined) { + return false; } - this._input = null; - - super.endEditing(); + return value == true; } - _input: HTMLInputElement | null; + _input: HTMLInputElement; } export class SelectCellEditor extends CellEditor { startEditing() { + this._createWidget(); + const cell = this._cell; const cellInfo = this.getCellInfo(cell); + this._select.value = this.deserialize(cellInfo.data); + this._form.appendChild(this._select); + this._select.focus(); + + this._bindEvents(); + } + + _createWidget() { + const cell = this._cell; const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); const items = metadata.constraint.enum; @@ -745,151 +797,82 @@ class SelectCellEditor extends CellEditor { select.appendChild(option); } - select.value = cellInfo.data; - this._select = select; - - this._form.appendChild(select); - - this.updatePosition(); - select.focus(); - - select.addEventListener("keydown", (event) => { - this._onKeyDown(event); - }); - - select.addEventListener("blur", (event) => { - this._saveInput(); - this.endEditing(); - }); - - select.addEventListener("change", (event) => { - this._saveInput(); - this.endEditing(); - }); } - _onKeyDown(event: KeyboardEvent) { - switch (event.keyCode) { - case 13: // return - this._saveInput('down'); - this.endEditing(); - break; - case 27: // escape - this.endEditing(); - break; - default: - break; - } + _bindEvents() { + this._select.addEventListener('keydown', this); + this._select.addEventListener('blur', this); } - _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none') { - if (!this._select) { - return; - } - - const value = this._select.value; - if (this._validator && ! this._validator.validate(this._cell, value)) { - this.validInput = false; - return; - } - - this._onCommit.emit({ cell: this._cell, value: value, cursorMovement: cursorMovement }); + _unbindEvents() { + this._select.removeEventListener('keydown', this); + this._select.removeEventListener('blur', this); } - endEditing() { - if (!this._select) { - return; + handleEvent(event: Event): void { + switch (event.type) { + case 'keydown': + this._onKeyDown(event as KeyboardEvent); + break; + case 'blur': + this._onBlur(event as FocusEvent); + break; } - - this._select = null; - - super.endEditing(); - } - - _select: HTMLSelectElement | null; -} - -export -class DateCellEditor extends CellEditor { - startEditing() { - const cell = this._cell; - const cellInfo = this.getCellInfo(cell); - - const input = document.createElement('input'); - input.type = 'date'; - input.pattern = "\d{4}-\d{2}-\d{2}"; - input.classList.add('cell-editor'); - input.classList.add('input-cell-editor'); - input.spellcheck = false; - - input.value = cellInfo.data; - - this._input = input; - - this._form.appendChild(input); - - this.updatePosition(); - - input.select(); - input.focus(); - - input.addEventListener("keydown", (event) => { - this._onKeyDown(event); - }); - - input.addEventListener("blur", (event) => { - this._saveInput(); - this.endEditing(); - }); - - input.addEventListener("change", (event) => { - this._saveInput(); - //this.endEditing(); - }); } _onKeyDown(event: KeyboardEvent) { - switch (event.keyCode) { - case 13: // return - this._saveInput(); - this.endEditing(); + switch (getKeyboardLayout().keyForKeydownEvent(event)) { + case 'Enter': + this.commit(event.shiftKey ? 'up' : 'down'); + break; + case 'Tab': + this.commit(event.shiftKey ? 'left' : 'right'); break; - case 27: // escape - this.endEditing(); + case 'Escape': + this.dispose(); break; default: break; } } - _saveInput(cursorMovement: SelectionModel.CursorMoveDirection = 'none') { - if (!this._input) { - return; + _onBlur(event: FocusEvent) { + if (!this.commit()) { + event.preventDefault(); + event.stopPropagation(); + this._select.focus(); } + } - const value = this._input.value; - if (this._validator && !this._validator.validate(this._cell, value)) { - this.validInput = false; - return; + protected serialize(): any { + const value = this._select.value; + + if (this._validator) { + const result = this._validator.validate(this._cell, value); + if (!result.valid) { + this.validInput = false; + this._select.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); + this._form.reportValidity(); + throw new Error('Invalid input'); + } } - this._onCommit.emit({ cell: this._cell, value: value, cursorMovement: cursorMovement }); + return value; } - endEditing() { - if (!this._input) { - return; + protected deserialize(value: any): any { + if (value === null || value === undefined) { + return ''; } - this._input = null; - - super.endEditing(); + return value.toString(); } - _input: HTMLInputElement | null; + _select: HTMLSelectElement; } + export namespace CellEditor { export diff --git a/packages/datagrid/src/selectionmodel.ts b/packages/datagrid/src/selectionmodel.ts index 9896e3d89..23348103d 100644 --- a/packages/datagrid/src/selectionmodel.ts +++ b/packages/datagrid/src/selectionmodel.ts @@ -216,7 +216,7 @@ namespace SelectionModel { export type SelectionMode = 'row' | 'column' | 'cell'; - export type CursorMoveDirection = 'up' | 'down' | 'none'; + export type CursorMoveDirection = 'up' | 'down' | 'left' | 'right' | 'none'; /** * A type alias for the clear mode. From ac1bd3a9f15e3bd68fda5c7b6b25aceb54f9137b Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 17 Oct 2019 10:31:13 -0400 Subject: [PATCH 13/36] fix styling issues with small cells --- packages/default-theme/style/datagrid.css | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/default-theme/style/datagrid.css b/packages/default-theme/style/datagrid.css index 147dab18b..a16e6909a 100644 --- a/packages/default-theme/style/datagrid.css +++ b/packages/default-theme/style/datagrid.css @@ -29,8 +29,6 @@ } .cell-editor-occluder { - width: 100%; - height: 100%; position: absolute; overflow: hidden; } @@ -51,6 +49,7 @@ .cell-editor-container > form { width: 100%; height: 100%; + overflow: hidden; } .cell-editor { @@ -61,7 +60,6 @@ } .input-cell-editor { - padding: 0 0 1px 0; background-color: #ffffff; border: 0; } From b348051aa58fe6160d884826cbc3a3c0069d95c6 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 17 Oct 2019 10:59:53 -0400 Subject: [PATCH 14/36] fix blur related isses --- packages/datagrid/src/celleditor.ts | 16 ++++++++++++++++ packages/default-theme/style/datagrid.css | 3 +++ 2 files changed, 19 insertions(+) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 1bf40a156..d1ae2e37f 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -435,6 +435,10 @@ class TextCellEditor extends CellEditor { } _onBlur(event: FocusEvent) { + if (this.isDisposed) { + return; + } + if (!this.commit()) { event.preventDefault(); event.stopPropagation(); @@ -638,6 +642,10 @@ class DateCellEditor extends CellEditor { } _onBlur(event: FocusEvent) { + if (this.isDisposed) { + return; + } + if (!this.commit()) { event.preventDefault(); event.stopPropagation(); @@ -734,6 +742,10 @@ class BooleanCellEditor extends CellEditor { } _onBlur(event: FocusEvent) { + if (this.isDisposed) { + return; + } + if (!this.commit()) { event.preventDefault(); event.stopPropagation(); @@ -838,6 +850,10 @@ class SelectCellEditor extends CellEditor { } _onBlur(event: FocusEvent) { + if (this.isDisposed) { + return; + } + if (!this.commit()) { event.preventDefault(); event.stopPropagation(); diff --git a/packages/default-theme/style/datagrid.css b/packages/default-theme/style/datagrid.css index a16e6909a..fb1e180a7 100644 --- a/packages/default-theme/style/datagrid.css +++ b/packages/default-theme/style/datagrid.css @@ -29,11 +29,13 @@ } .cell-editor-occluder { + pointer-events: none; position: absolute; overflow: hidden; } .cell-editor-container { + pointer-events: auto; position: absolute; background-color: #ffffff; box-sizing: border-box; @@ -53,6 +55,7 @@ } .cell-editor { + pointer-events: auto; width: 100%; height: 100%; outline: none; From 23db01515fbd66bdfeeaee331325de530a4e924c Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 17 Oct 2019 13:24:53 -0400 Subject: [PATCH 15/36] left & right cursor move implementation --- packages/datagrid/src/basickeyhandler.ts | 12 ++++++++++-- packages/datagrid/src/basicmousehandler.ts | 4 ++-- packages/datagrid/src/basicselectionmodel.ts | 20 +++++++++++++++----- packages/datagrid/src/celleditor.ts | 8 ++++++++ packages/datagrid/src/datagrid.ts | 18 ++++++++++++++---- packages/datagrid/src/selectionmodel.ts | 2 +- 6 files changed, 50 insertions(+), 14 deletions(-) diff --git a/packages/datagrid/src/basickeyhandler.ts b/packages/datagrid/src/basickeyhandler.ts index 5f2fbc9bc..9f0a35d13 100644 --- a/packages/datagrid/src/basickeyhandler.ts +++ b/packages/datagrid/src/basickeyhandler.ts @@ -80,7 +80,7 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { } grid.viewport.node.focus(); if (args.cursorMovement) { - grid.incrementCursor(event.shiftKey ? 'up' : 'down'); + grid.moveCursor(args.cursorMovement); grid.scrollToCursor(); } }); @@ -124,10 +124,18 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { break; case 'Enter': if (grid.selectionModel) { - grid.incrementCursor(event.shiftKey ? 'up' : 'down'); + grid.moveCursor(event.shiftKey ? 'up' : 'down'); grid.scrollToCursor(); } break; + case 'Tab': + if (grid.selectionModel) { + grid.moveCursor(event.shiftKey ? 'left' : 'right'); + grid.scrollToCursor(); + event.stopPropagation(); + event.preventDefault(); + } + break; } } diff --git a/packages/datagrid/src/basicmousehandler.ts b/packages/datagrid/src/basicmousehandler.ts index ed6a0529e..b0dc081a7 100644 --- a/packages/datagrid/src/basicmousehandler.ts +++ b/packages/datagrid/src/basicmousehandler.ts @@ -520,7 +520,7 @@ class BasicMouseHandler implements DataGrid.IMouseHandler { grid: grid, row: row, column: column, - metadata: grid.dataModel!.metadata('body', row, column) + metadata: grid.dataModel.metadata('body', row, column) }; const editor = grid.cellEditorController.createEditor(cell); if (editor) { @@ -531,7 +531,7 @@ class BasicMouseHandler implements DataGrid.IMouseHandler { } grid.viewport.node.focus(); if (args.cursorMovement !== 'none') { - grid.incrementCursor(args.cursorMovement); + grid.moveCursor(args.cursorMovement); grid.scrollToCursor(); } }); diff --git a/packages/datagrid/src/basicselectionmodel.ts b/packages/datagrid/src/basicselectionmodel.ts index 62b79f607..d16e083c7 100644 --- a/packages/datagrid/src/basicselectionmodel.ts +++ b/packages/datagrid/src/basicselectionmodel.ts @@ -52,7 +52,7 @@ class BasicSelectionModel extends SelectionModel { * Move cursor down/up while making sure it remains * within the bounds of selected rectangles */ - incrementCursorWithinSelections(direction: SelectionModel.CursorMoveDirection): void { + moveCursorWithinSelections(direction: SelectionModel.CursorMoveDirection): void { // Bail early if there are no selections or no existing cursor if (this.isEmpty || this.cursorRow === -1 || this._cursorColumn === -1) { return; @@ -72,8 +72,10 @@ class BasicSelectionModel extends SelectionModel { } let cursorRect = this._selections[this._cursorRectIndex]; - let newRow = this._cursorRow + (direction === 'down' ? 1 : -1); - let newColumn = this._cursorColumn; + const dr = direction === 'down' ? 1 : direction === 'up' ? -1 : 0; + const dc = direction === 'right' ? 1 : direction === 'left' ? -1 : 0; + let newRow = this._cursorRow + dr; + let newColumn = this._cursorColumn + dc; const r1 = Math.min(cursorRect.r1, cursorRect.r2); const r2 = Math.max(cursorRect.r1, cursorRect.r2); const c1 = Math.min(cursorRect.c1, cursorRect.c2); @@ -87,7 +89,15 @@ class BasicSelectionModel extends SelectionModel { newColumn -= 1; } - // if going downward and the last cell in the selection rectangle visited, + if (newColumn > c2 && newRow != r2) { + newColumn = c1; + newRow += 1; + } else if (newColumn < c1 && newRow != r1) { + newColumn = c2; + newRow -= 1; + } + + // if going downward/right and the last cell in the selection rectangle visited, // move to next rectangle if (newColumn > c2) { this._cursorRectIndex = (this._cursorRectIndex + 1) % this._selections.length; @@ -95,7 +105,7 @@ class BasicSelectionModel extends SelectionModel { newRow = Math.min(cursorRect.r1, cursorRect.r2); newColumn = Math.min(cursorRect.c1, cursorRect.c2); } - // if going upward and the first cell in the selection rectangle visited, + // if going upward/left and the first cell in the selection rectangle visited, // move to previous rectangle else if (newColumn < c1) { this._cursorRectIndex = this._cursorRectIndex === 0 ? this._selections.length - 1 : this._cursorRectIndex - 1; diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index d1ae2e37f..bbeb4c5c9 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -425,6 +425,8 @@ class TextCellEditor extends CellEditor { break; case 'Tab': this.commit(event.shiftKey ? 'left' : 'right'); + event.stopPropagation(); + event.preventDefault(); break; case 'Escape': this.dispose(); @@ -632,6 +634,8 @@ class DateCellEditor extends CellEditor { break; case 'Tab': this.commit(event.shiftKey ? 'left' : 'right'); + event.stopPropagation(); + event.preventDefault(); break; case 'Escape': this.dispose(); @@ -732,6 +736,8 @@ class BooleanCellEditor extends CellEditor { break; case 'Tab': this.commit(event.shiftKey ? 'left' : 'right'); + event.stopPropagation(); + event.preventDefault(); break; case 'Escape': this.dispose(); @@ -840,6 +846,8 @@ class SelectCellEditor extends CellEditor { break; case 'Tab': this.commit(event.shiftKey ? 'left' : 'right'); + event.stopPropagation(); + event.preventDefault(); break; case 'Escape': this.dispose(); diff --git a/packages/datagrid/src/datagrid.ts b/packages/datagrid/src/datagrid.ts index 023011369..eafb88caf 100644 --- a/packages/datagrid/src/datagrid.ts +++ b/packages/datagrid/src/datagrid.ts @@ -829,7 +829,7 @@ class DataGrid extends Widget { * Move cursor down/up while making sure it remains * within the bounds of selected rectangles */ - incrementCursor(direction: SelectionModel.CursorMoveDirection): void { + moveCursor(direction: SelectionModel.CursorMoveDirection): void { // Bail early if there is no selection if (!this.dataModel || !this._selectionModel || @@ -847,8 +847,10 @@ class DataGrid extends Widget { if (currentSel.r1 === currentSel.r2 && currentSel.c1 === currentSel.c2 ) { - let newRow = currentSel.r1 + (direction === 'down' ? 1 : -1); - let newColumn = currentSel.c1; + const dr = direction === 'down' ? 1 : direction === 'up' ? -1 : 0; + const dc = direction === 'right' ? 1 : direction === 'left' ? -1 : 0; + let newRow = currentSel.r1 + dr; + let newColumn = currentSel.c1 + dc; const rowCount = this.dataModel.rowCount('body'); const columnCount = this.dataModel.columnCount('body'); if (newRow >= rowCount) { @@ -860,8 +862,16 @@ class DataGrid extends Widget { } if (newColumn >= columnCount) { newColumn = 0; + newRow += 1; + if (newRow >= rowCount) { + newRow = 0; + } } else if (newColumn === -1) { newColumn = columnCount - 1; + newRow -= 1; + if (newRow === -1) { + newRow = rowCount - 1; + } } this._selectionModel.select({ @@ -877,7 +887,7 @@ class DataGrid extends Widget { // if there are multiple selections, move cursor // within selection rectangles - this._selectionModel!.incrementCursorWithinSelections(direction); + this._selectionModel.moveCursorWithinSelections(direction); } /** diff --git a/packages/datagrid/src/selectionmodel.ts b/packages/datagrid/src/selectionmodel.ts index 23348103d..cd1878461 100644 --- a/packages/datagrid/src/selectionmodel.ts +++ b/packages/datagrid/src/selectionmodel.ts @@ -60,7 +60,7 @@ abstract class SelectionModel { */ abstract readonly cursorColumn: number; - abstract incrementCursorWithinSelections(direction: SelectionModel.CursorMoveDirection): void; + abstract moveCursorWithinSelections(direction: SelectionModel.CursorMoveDirection): void; /** * Get the current selection in the selection model. From 026bd04a9275333cea1ad5ba279ed5cc92d90d03 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 17 Oct 2019 13:30:01 -0400 Subject: [PATCH 16/36] remove metadata from CellConfig --- packages/datagrid/src/basickeyhandler.ts | 8 ++++---- packages/datagrid/src/basicmousehandler.ts | 7 +++---- packages/datagrid/src/celleditor.ts | 10 +++------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/datagrid/src/basickeyhandler.ts b/packages/datagrid/src/basickeyhandler.ts index 9f0a35d13..eff858abf 100644 --- a/packages/datagrid/src/basickeyhandler.ts +++ b/packages/datagrid/src/basickeyhandler.ts @@ -20,7 +20,8 @@ import { import { SelectionModel } from './selectionmodel'; -import { ICellEditResponse } from './celleditor'; + +import { ICellEditResponse, CellEditor } from './celleditor'; import { MutableDataModel } from './datamodel'; @@ -65,11 +66,10 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { const row = grid.selectionModel.cursorRow; const column = grid.selectionModel.cursorColumn; if (row != -1 && column != -1) { - const cell = { + const cell: CellEditor.CellConfig = { grid: grid, row: row, - column: column, - metadata: grid.dataModel!.metadata('body', row, column) + column: column }; const editor = grid.cellEditorController.createEditor(cell); if (editor) { diff --git a/packages/datagrid/src/basicmousehandler.ts b/packages/datagrid/src/basicmousehandler.ts index b0dc081a7..58dafc1b0 100644 --- a/packages/datagrid/src/basicmousehandler.ts +++ b/packages/datagrid/src/basicmousehandler.ts @@ -28,7 +28,7 @@ import { import { SelectionModel } from './selectionmodel'; -import { ICellEditResponse } from './celleditor'; +import { ICellEditResponse, CellEditor } from './celleditor'; /** * A basic implementation of a data grid mouse handler. @@ -516,11 +516,10 @@ class BasicMouseHandler implements DataGrid.IMouseHandler { } if (region === 'body') { - const cell = { + const cell: CellEditor.CellConfig = { grid: grid, row: row, - column: column, - metadata: grid.dataModel.metadata('body', row, column) + column: column }; const editor = grid.cellEditorController.createEditor(cell); if (editor) { diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index bbeb4c5c9..5748b0123 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -10,8 +10,6 @@ import { DataGrid } from './datagrid'; -import { DataModel } from './datamodel'; - import { SelectionModel } from './selectionmodel'; import { Signal, ISignal } from '@phosphor/signaling'; @@ -915,6 +913,9 @@ namespace CellEditor { */ export type CellConfig = { + /** + * The grid containing the cell. + */ readonly grid: DataGrid; /** * The row index of the cell. @@ -925,10 +926,5 @@ namespace CellEditor { * The column index of the cell. */ readonly column: number; - - /** - * The metadata for the cell. - */ - readonly metadata: DataModel.Metadata; }; } From 089ab89766f632e17be7a48c610b79aeab50f653 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 17 Oct 2019 14:21:59 -0400 Subject: [PATCH 17/36] ability to cancel an edit, emit oncancel signal --- packages/datagrid/src/celleditor.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 5748b0123..af0dea173 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -37,6 +37,7 @@ interface ICellEditResponse { export interface ICellEditor { edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): void; + cancel(): void; readonly onCommit: ISignal; readonly onCancel: ISignal; } @@ -262,6 +263,11 @@ abstract class CellEditor implements ICellEditor, IDisposable { this.startEditing(); } + cancel() { + this.dispose(); + this._onCancel.emit(void 0); + } + protected getCellInfo(cell: CellEditor.CellConfig) { const { grid, row, column } = cell; const data = grid.dataModel!.data('body', row, column); @@ -427,7 +433,7 @@ class TextCellEditor extends CellEditor { event.preventDefault(); break; case 'Escape': - this.dispose(); + this.cancel(); break; default: break; @@ -636,7 +642,7 @@ class DateCellEditor extends CellEditor { event.preventDefault(); break; case 'Escape': - this.dispose(); + this.cancel(); break; default: break; @@ -738,7 +744,7 @@ class BooleanCellEditor extends CellEditor { event.preventDefault(); break; case 'Escape': - this.dispose(); + this.cancel(); break; default: break; @@ -848,7 +854,7 @@ class SelectCellEditor extends CellEditor { event.preventDefault(); break; case 'Escape': - this.dispose(); + this.cancel(); break; default: break; From 9bd666860850a8c9ec933a6f24a4ce26c989f2be Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 18 Oct 2019 10:32:18 -0400 Subject: [PATCH 18/36] minLength, maxLength and pattern constraint support for text input --- packages/datagrid/src/celleditor.ts | 49 ++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index af0dea173..62227ce26 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -124,8 +124,33 @@ class TextInputValidator implements ICellInputValidator { }; } + if (!Number.isNaN(this.minLength) && value.length < this.minLength) { + return { + valid: false, + message: `Text length must be greater than ${this.minLength}` + }; + } + + if (!Number.isNaN(this.maxLength) && value.length > this.maxLength) { + return { + valid: false, + message: `Text length must be less than ${this.maxLength}` + }; + } + + if (this.pattern && !this.pattern.test(value)) { + return { + valid: false, + message: `Text doesn't match the required pattern` + }; + } + return { valid: true }; } + + minLength: number = Number.NaN; + maxLength: number = Number.NaN; + pattern: RegExp | null = null; } export @@ -220,16 +245,30 @@ abstract class CellEditor implements ICellEditor, IDisposable { switch (metadata && metadata.type) { case 'string': - this._validator = new TextInputValidator(); + { + const validator = new TextInputValidator(); + if (metadata!.constraint) { + if (metadata!.constraint.minLength !== undefined) { + validator.minLength = metadata!.constraint.minLength; + } + if (metadata!.constraint.maxLength !== undefined) { + validator.maxLength = metadata!.constraint.maxLength; + } + if (typeof(metadata!.constraint.pattern) === 'string') { + validator.pattern = new RegExp(metadata!.constraint.pattern); + } + } + this._validator = validator; + } break; case 'number': { const validator = new NumberInputValidator(); if (metadata!.constraint) { - if (metadata!.constraint.minimum) { + if (metadata!.constraint.minimum !== undefined) { validator.min = metadata!.constraint.minimum; } - if (metadata!.constraint.maximum) { + if (metadata!.constraint.maximum !== undefined) { validator.max = metadata!.constraint.maximum; } } @@ -240,10 +279,10 @@ abstract class CellEditor implements ICellEditor, IDisposable { { const validator = new IntegerInputValidator(); if (metadata!.constraint) { - if (metadata!.constraint.minimum) { + if (metadata!.constraint.minimum !== undefined) { validator.min = metadata!.constraint.minimum; } - if (metadata!.constraint.maximum) { + if (metadata!.constraint.maximum !== undefined) { validator.max = metadata!.constraint.maximum; } } From 254b81fa2461a438bcb8f3f9563e6b181484c9cb Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 18 Oct 2019 11:11:08 -0400 Subject: [PATCH 19/36] update validity in exceptions --- packages/datagrid/src/celleditor.ts | 2 ++ packages/datagrid/src/index.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 62227ce26..f29a0f485 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -547,6 +547,7 @@ class NumberCellEditor extends TextCellEditor { let floatValue = Number.parseFloat(value); if (Number.isNaN(floatValue)) { + this.validInput = false; this._input.setCustomValidity('Input must be valid number'); this._form.reportValidity(); throw new Error('Invalid input'); @@ -604,6 +605,7 @@ class IntegerCellEditor extends NumberCellEditor { let intValue = Number.parseInt(value); if (Number.isNaN(intValue)) { + this.validInput = false; this._input.setCustomValidity('Input must be valid number'); this._form.reportValidity(); throw new Error('Invalid input'); diff --git a/packages/datagrid/src/index.ts b/packages/datagrid/src/index.ts index 18b9fae3e..44b94a9fb 100644 --- a/packages/datagrid/src/index.ts +++ b/packages/datagrid/src/index.ts @@ -9,6 +9,7 @@ export * from './basickeyhandler'; export * from './basicmousehandler'; export * from './basicselectionmodel'; export * from './cellrenderer'; +export * from './celleditor'; export * from './datagrid'; export * from './datamodel'; export * from './graphicscontext'; From 2225c813622f8592544413cec2b4d5a44e90f6b3 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 18 Oct 2019 13:16:44 -0400 Subject: [PATCH 20/36] metadata format property support --- packages/datagrid/src/celleditor.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index f29a0f485..07e54fe93 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -247,6 +247,24 @@ abstract class CellEditor implements ICellEditor, IDisposable { case 'string': { const validator = new TextInputValidator(); + if (typeof(metadata!.format) === 'string') { + const format = metadata!.format; + switch (format) { + case 'email': + validator.pattern = new RegExp("^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$"); + break; + case 'uuid': + validator.pattern = new RegExp("[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}"); + break; + case 'uri': + // TODO + break; + case 'binary': + // TODO + break; + } + } + if (metadata!.constraint) { if (metadata!.constraint.minLength !== undefined) { validator.minLength = metadata!.constraint.minLength; From e17c76da2af4e431c1632857ceadacb4c2a5e779 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 18 Oct 2019 14:06:58 -0400 Subject: [PATCH 21/36] dynamic option cell editor --- packages/datagrid/src/celleditor.ts | 139 +++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 3 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 07e54fe93..b0069f454 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -61,7 +61,11 @@ class CellEditorController { } if (metadata.constraint && metadata.constraint.enum) { - key += ':enum'; + if (metadata.constraint.enum === 'dynamic') { + key += ':dynamic-enum'; + } else { + key += ':enum'; + } } return key; @@ -85,7 +89,12 @@ class CellEditorController { case 'number:enum': case 'integer:enum': case 'date:enum': - return new SelectCellEditor(); + return new OptionCellEditor(); + case 'string:dynamic-enum': + case 'number:dynamic-enum': + case 'integer:dynamic-enum': + case 'date:dynamic-enum': + return new DynamicOptionCellEditor(); } const data = cell.grid.dataModel ? cell.grid.dataModel.data('body', cell.row, cell.column) : undefined; @@ -850,7 +859,7 @@ class BooleanCellEditor extends CellEditor { } export -class SelectCellEditor extends CellEditor { +class OptionCellEditor extends CellEditor { startEditing() { this._createWidget(); @@ -959,6 +968,130 @@ class SelectCellEditor extends CellEditor { _select: HTMLSelectElement; } +export +class DynamicOptionCellEditor extends CellEditor { + startEditing() { + this._createWidget(); + + const cell = this._cell; + const cellInfo = this.getCellInfo(cell); + this._input.value = this.deserialize(cellInfo.data); + this._form.appendChild(this._input); + this._input.select(); + this._input.focus(); + + this._bindEvents(); + } + + _createWidget() { + const cell = this._cell; + const grid = cell.grid; + const dataModel = grid.dataModel!; + const rowCount = dataModel.rowCount('body'); + + const listId = 'cell-editor-list'; + const list = document.createElement('datalist'); + list.id = listId; + const input = document.createElement('input'); + input.classList.add('cell-editor'); + input.classList.add('input-cell-editor'); + const valueSet = new Set(); + for (let r = 0; r < rowCount; ++r) { + const data = dataModel.data('body', r, cell.column); + if (data) { + valueSet.add(data); + } + } + valueSet.forEach((value: string) => { + const option = document.createElement("option"); + option.value = value; + option.text = value; + list.appendChild(option); + }); + this._form.appendChild(list); + input.setAttribute('list', listId); + + this._input = input; + } + + _bindEvents() { + this._input.addEventListener('keydown', this); + this._input.addEventListener('blur', this); + } + + _unbindEvents() { + this._input.removeEventListener('keydown', this); + this._input.removeEventListener('blur', this); + } + + handleEvent(event: Event): void { + switch (event.type) { + case 'keydown': + this._onKeyDown(event as KeyboardEvent); + break; + case 'blur': + this._onBlur(event as FocusEvent); + break; + } + } + + _onKeyDown(event: KeyboardEvent) { + switch (getKeyboardLayout().keyForKeydownEvent(event)) { + case 'Enter': + this.commit(event.shiftKey ? 'up' : 'down'); + break; + case 'Tab': + this.commit(event.shiftKey ? 'left' : 'right'); + event.stopPropagation(); + event.preventDefault(); + break; + case 'Escape': + this.cancel(); + break; + default: + break; + } + } + + _onBlur(event: FocusEvent) { + if (this.isDisposed) { + return; + } + + if (!this.commit()) { + event.preventDefault(); + event.stopPropagation(); + this._input.focus(); + } + } + + protected serialize(): any { + const value = this._input.value; + + if (this._validator) { + const result = this._validator.validate(this._cell, value); + if (!result.valid) { + this.validInput = false; + this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); + this._form.reportValidity(); + throw new Error('Invalid input'); + } + } + + return value; + } + + protected deserialize(value: any): any { + if (value === null || value === undefined) { + return ''; + } + + return value.toString(); + } + + _input: HTMLInputElement; +} + export namespace CellEditor { From f2532d7701ac44ea6f6ec727d0a1a4f7c9ed7fa9 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sun, 20 Oct 2019 08:14:03 -0400 Subject: [PATCH 22/36] grid editable flag, refactoring --- packages/datagrid/src/basickeyhandler.ts | 53 ++++++----- packages/datagrid/src/basicmousehandler.ts | 36 ++++---- packages/datagrid/src/celleditor.ts | 102 +++++++++++++-------- packages/datagrid/src/datagrid.ts | 32 +++++-- 4 files changed, 135 insertions(+), 88 deletions(-) diff --git a/packages/datagrid/src/basickeyhandler.ts b/packages/datagrid/src/basickeyhandler.ts index eff858abf..a40e9a2ee 100644 --- a/packages/datagrid/src/basickeyhandler.ts +++ b/packages/datagrid/src/basickeyhandler.ts @@ -59,38 +59,37 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { * This will not be called if the mouse button is pressed. */ onKeyDown(grid: DataGrid, event: KeyboardEvent): void { - if (!Platform.accelKey(event)) { + if (grid.editable && + grid.selectionModel!.cursorRow !== -1 && + grid.selectionModel!.cursorColumn !== -1) { const inp = String.fromCharCode(event.keyCode); if (/[a-zA-Z0-9-_ ]/.test(inp)) { - if (grid.selectionModel) { - const row = grid.selectionModel.cursorRow; - const column = grid.selectionModel.cursorColumn; - if (row != -1 && column != -1) { - const cell: CellEditor.CellConfig = { - grid: grid, - row: row, - column: column - }; - const editor = grid.cellEditorController.createEditor(cell); - if (editor) { - editor.onCommit.connect((_, args: ICellEditResponse) => { - if (grid.dataModel instanceof MutableDataModel) { - const dataModel = grid.dataModel as MutableDataModel; - dataModel.setData('body', row, column, args.value); - } - grid.viewport.node.focus(); - if (args.cursorMovement) { - grid.moveCursor(args.cursorMovement); - grid.scrollToCursor(); - } - }); - editor.onCancel.connect((_, args: void) => { - grid.viewport.node.focus(); - }); - editor.edit(cell); + const row = grid.selectionModel!.cursorRow; + const column = grid.selectionModel!.cursorColumn; + const cell: CellEditor.CellConfig = { + grid: grid, + row: row, + column: column + }; + grid.editorController!.edit(cell, { + onCommit: (response: ICellEditResponse) => { + const dataModel = grid.dataModel as MutableDataModel; + dataModel.setData('body', row, column, response.value); + grid.viewport.node.focus(); + if (response.cursorMovement) { + grid.moveCursor(response.cursorMovement); + grid.scrollToCursor(); } + }, + onCancel: () => { + grid.viewport.node.focus(); } + }); + if (getKeyboardLayout().keyForKeydownEvent(event) === 'Space') { + event.stopPropagation(); + event.preventDefault(); } + return; } } diff --git a/packages/datagrid/src/basicmousehandler.ts b/packages/datagrid/src/basicmousehandler.ts index 58dafc1b0..33ed9cf50 100644 --- a/packages/datagrid/src/basicmousehandler.ts +++ b/packages/datagrid/src/basicmousehandler.ts @@ -516,28 +516,26 @@ class BasicMouseHandler implements DataGrid.IMouseHandler { } if (region === 'body') { - const cell: CellEditor.CellConfig = { - grid: grid, - row: row, - column: column - }; - const editor = grid.cellEditorController.createEditor(cell); - if (editor) { - editor.onCommit.connect((_, args: ICellEditResponse) => { - if (grid.dataModel instanceof MutableDataModel) { + if (grid.editable) { + const cell: CellEditor.CellConfig = { + grid: grid, + row: row, + column: column + }; + grid.editorController!.edit(cell, { + onCommit: (response: ICellEditResponse) => { const dataModel = grid.dataModel as MutableDataModel; - dataModel.setData('body', row, column, args.value); - } - grid.viewport.node.focus(); - if (args.cursorMovement !== 'none') { - grid.moveCursor(args.cursorMovement); - grid.scrollToCursor(); + dataModel.setData('body', row, column, response.value); + grid.viewport.node.focus(); + if (response.cursorMovement !== 'none') { + grid.moveCursor(response.cursorMovement); + grid.scrollToCursor(); + } + }, + onCancel: () => { + grid.viewport.node.focus(); } }); - editor.onCancel.connect((_, args: void) => { - grid.viewport.node.focus(); - }); - editor.edit(cell); } } diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index b0069f454..845643622 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -12,8 +12,6 @@ import { import { SelectionModel } from './selectionmodel'; -import { Signal, ISignal } from '@phosphor/signaling'; - import { getKeyboardLayout } from '@phosphor/keyboard'; export @@ -36,17 +34,59 @@ interface ICellEditResponse { export interface ICellEditor { - edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): void; + edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): void; cancel(): void; - readonly onCommit: ISignal; - readonly onCancel: ISignal; } const DEFAULT_INVALID_INPUT_MESSAGE = "Invalid input!"; +export +interface ICellEditOptions { + editor?: ICellEditor; + validator?: ICellInputValidator; + onCommit?: (response: ICellEditResponse) => void; + onCancel?: () => void; +} + +export +interface ICellEditorController { + edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): boolean; + cancel(): void; +} export -class CellEditorController { +class CellEditorController implements ICellEditorController { + edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): boolean { + const grid = cell.grid; + + if (!grid.editable) { + console.error('Grid cannot be edited!'); + return false; + } + + this.cancel(); + + if (options && options.editor) { + options.editor.edit(cell, options); + return true; + } + + const editor = this._createEditor(cell); + if (editor) { + editor.edit(cell, options); + return true; + } + + return false; + } + + cancel(): void { + if (this._editor) { + this._editor.cancel(); + this._editor = null; + } + } + private _getKey(cell: CellEditor.CellConfig): string { const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : null; @@ -71,7 +111,7 @@ class CellEditorController { return key; } - createEditor(cell: CellEditor.CellConfig): ICellEditor | null { + private _createEditor(cell: CellEditor.CellConfig): ICellEditor | null { const key = this._getKey(cell); switch (key) { @@ -97,7 +137,7 @@ class CellEditorController { return new DynamicOptionCellEditor(); } - const data = cell.grid.dataModel ? cell.grid.dataModel.data('body', cell.row, cell.column) : undefined; + const data = cell.grid.dataModel!.data('body', cell.row, cell.column); if (!data || typeof data !== 'object') { return new TextCellEditor(); } @@ -105,15 +145,7 @@ class CellEditorController { return null; } - edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator): boolean { - const editor = this.createEditor(cell); - if (editor) { - editor.edit(cell, validator); - return true; - } - - return false; - } + private _editor: ICellEditor | null = null; } export @@ -244,11 +276,12 @@ abstract class CellEditor implements ICellEditor, IDisposable { this._cell.grid.node.removeChild(this._viewportOccluder); } - edit(cell: CellEditor.CellConfig, validator?: ICellInputValidator) { + edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): void { this._cell = cell; + this._onCommit = options && options.onCommit; - if (validator) { - this._validator = validator; + if (options && options.validator) { + this._validator = options.validator; } else { const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : null; @@ -331,7 +364,9 @@ abstract class CellEditor implements ICellEditor, IDisposable { cancel() { this.dispose(); - this._onCancel.emit(void 0); + if (this._onCancel) { + this._onCancel(); + } } protected getCellInfo(cell: CellEditor.CellConfig) { @@ -410,25 +445,20 @@ abstract class CellEditor implements ICellEditor, IDisposable { } this.dispose(); - this._onCommit.emit({ - cell: this._cell, - value: value, - cursorMovement: cursorMovement - }); - - return true; - } - get onCommit(): ISignal { - return this._onCommit; - } + if (this._onCommit) { + this._onCommit({ + cell: this._cell, + value: value, + cursorMovement: cursorMovement + }); + } - get onCancel(): ISignal { - return this._onCancel; + return true; } - protected _onCommit = new Signal(this); - protected _onCancel = new Signal(this); + protected _onCommit?: (response: ICellEditResponse) => void; + protected _onCancel: () => void; protected _cell: CellEditor.CellConfig; protected _validator: ICellInputValidator | undefined; protected _viewportOccluder: HTMLDivElement; diff --git a/packages/datagrid/src/datagrid.ts b/packages/datagrid/src/datagrid.ts index eafb88caf..5f3f792e3 100644 --- a/packages/datagrid/src/datagrid.ts +++ b/packages/datagrid/src/datagrid.ts @@ -30,7 +30,7 @@ import { } from './cellrenderer'; import { - DataModel + DataModel, MutableDataModel } from './datamodel'; import { @@ -48,7 +48,7 @@ import { import { SelectionModel } from './selectionmodel'; -import { CellEditorController } from './celleditor'; +import { ICellEditorController, CellEditorController } from './celleditor'; /** @@ -128,7 +128,7 @@ class DataGrid extends Widget { this._hScrollBar = new ScrollBar({ orientation: 'horizontal' }); this._scrollCorner = new Widget(); - this._cellEditorController = new CellEditorController(); + this._editorController = new CellEditorController(); // Add the extra class names to the child widgets. this._viewport.addClass('p-DataGrid-viewport'); @@ -5060,8 +5060,27 @@ class DataGrid extends Widget { gc.restore(); } - get cellEditorController(): CellEditorController { - return this._cellEditorController; + get editorController(): ICellEditorController | null { + return this._editorController; + } + + set editorController(controller: ICellEditorController | null) { + this._editorController = controller; + } + + get editingEnabled(): boolean { + return this._editingEnabled; + } + + set editingEnabled(enabled: boolean) { + this._editingEnabled = enabled; + } + + get editable(): boolean { + return this._editingEnabled && + this._selectionModel !== null && + this._editorController !== null && + this.dataModel instanceof MutableDataModel; } private _viewport: Widget; @@ -5104,7 +5123,8 @@ class DataGrid extends Widget { private _cellRenderers: RendererMap; private _copyConfig: DataGrid.CopyConfig; private _headerVisibility: DataGrid.HeaderVisibility; - private _cellEditorController: CellEditorController; + private _editorController: ICellEditorController | null; + private _editingEnabled: boolean = false; } From 46eb12de3605867c201a79291ed929dad710fb81 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sun, 20 Oct 2019 21:39:38 -0400 Subject: [PATCH 23/36] allow editor overrides by data type or metadata --- packages/datagrid/src/celleditor.ts | 121 +++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 18 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 845643622..c041ce6e5 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -10,9 +10,17 @@ import { DataGrid } from './datagrid'; -import { SelectionModel } from './selectionmodel'; +import { + SelectionModel +} from './selectionmodel'; -import { getKeyboardLayout } from '@phosphor/keyboard'; +import { + getKeyboardLayout +} from '@phosphor/keyboard'; + +import { + DataModel +} from './datamodel'; export interface ICellInputValidatorResponse { @@ -40,6 +48,11 @@ interface ICellEditor { const DEFAULT_INVALID_INPUT_MESSAGE = "Invalid input!"; +export +type CellDataType = 'string' | 'number' | 'integer' | 'boolean' | 'date' | + 'string:option' | 'number:option' | 'integer:option'| 'date:option'| + 'string:dynamic-option' | 'number:dynamic-option' | 'integer:dynamic-option' | 'date:dynamic-option'; + export interface ICellEditOptions { editor?: ICellEditor; @@ -71,7 +84,7 @@ class CellEditorController implements ICellEditorController { return true; } - const editor = this._createEditor(cell); + const editor = this._getEditor(cell); if (editor) { editor.edit(cell, options); return true; @@ -87,7 +100,7 @@ class CellEditorController implements ICellEditorController { } } - private _getKey(cell: CellEditor.CellConfig): string { + private _getDataTypeKey(cell: CellEditor.CellConfig): string { const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : null; if (!metadata) { @@ -102,19 +115,80 @@ class CellEditorController implements ICellEditorController { if (metadata.constraint && metadata.constraint.enum) { if (metadata.constraint.enum === 'dynamic') { - key += ':dynamic-enum'; + key += ':dynamic-option'; } else { - key += ':enum'; + key += ':option'; } } return key; } - private _createEditor(cell: CellEditor.CellConfig): ICellEditor | null { - const key = this._getKey(cell); + private _objectToKey(object: any): string { + let str = ''; + for (let key in object) { + const value = object[key]; + if (typeof value === 'object') { + str += `${key}:${this._objectToKey(value)}`; + } else { + str += `[${key}:${value}]`; + } + } + + return str; + } + + private _metadataIdentifierToKey(metadata: DataModel.Metadata): string { + return this._objectToKey(metadata); + } + + private _metadataMatchesIdentifier(metadata: DataModel.Metadata, identifier: DataModel.Metadata): boolean { + for (let key in identifier) { + if (!metadata.hasOwnProperty(key)) { + return false; + } + + const identifierValue = identifier[key]; + const metadataValue = metadata[key]; + if (typeof identifierValue === 'object') { + if (!this._metadataMatchesIdentifier(metadataValue, identifierValue)) { + return false; + } + } else if (metadataValue !== identifierValue) { + return false; + } + } - switch (key) { + return true; + } + + private _getMetadataBasedEditor(metadata: DataModel.Metadata): ICellEditor | undefined { + for (let key of Array.from(this._metadataBasedOverrides.keys())) { + const [identifier, editor] = this._metadataBasedOverrides.get(key)!; + if (this._metadataMatchesIdentifier(metadata, identifier)) { + return editor; + } + } + + return undefined; + } + + private _getEditor(cell: CellEditor.CellConfig): ICellEditor | undefined { + const dtKey = this._getDataTypeKey(cell); + + if (this._typeBasedOverrides.has(dtKey)) { + return this._typeBasedOverrides.get(dtKey); + } else { + const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); + if (metadata) { + const editor = this._getMetadataBasedEditor(metadata); + if (editor) { + return editor; + } + } + } + + switch (dtKey) { case 'string': return new TextCellEditor(); case 'number': @@ -125,15 +199,15 @@ class CellEditorController implements ICellEditorController { return new BooleanCellEditor(); case 'date': return new DateCellEditor(); - case 'string:enum': - case 'number:enum': - case 'integer:enum': - case 'date:enum': + case 'string:option': + case 'number:option': + case 'integer:option': + case 'date:option': return new OptionCellEditor(); - case 'string:dynamic-enum': - case 'number:dynamic-enum': - case 'integer:dynamic-enum': - case 'date:dynamic-enum': + case 'string:dynamic-option': + case 'number:dynamic-option': + case 'integer:dynamic-option': + case 'date:dynamic-option': return new DynamicOptionCellEditor(); } @@ -142,10 +216,21 @@ class CellEditorController implements ICellEditorController { return new TextCellEditor(); } - return null; + return undefined; + } + + setEditor(identifier: CellDataType | DataModel.Metadata, editor: ICellEditor) { + if (typeof identifier === 'string') { + this._typeBasedOverrides.set(identifier, editor); + } else { + const key = this._metadataIdentifierToKey(identifier); + this._metadataBasedOverrides.set(key, [identifier, editor]); + } } private _editor: ICellEditor | null = null; + private _typeBasedOverrides: Map = new Map(); + private _metadataBasedOverrides: Map = new Map(); } export From 4edc5f4a7835b59ae88964d1e9711fd827614845 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sun, 20 Oct 2019 22:14:36 -0400 Subject: [PATCH 24/36] fix cursor move within selections --- packages/datagrid/src/basicselectionmodel.ts | 49 +++++++++++--------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/datagrid/src/basicselectionmodel.ts b/packages/datagrid/src/basicselectionmodel.ts index d16e083c7..96fbe29a2 100644 --- a/packages/datagrid/src/basicselectionmodel.ts +++ b/packages/datagrid/src/basicselectionmodel.ts @@ -81,37 +81,44 @@ class BasicSelectionModel extends SelectionModel { const c1 = Math.min(cursorRect.c1, cursorRect.c2); const c2 = Math.max(cursorRect.c1, cursorRect.c2); + const moveToNextRect = () => { + this._cursorRectIndex = (this._cursorRectIndex + 1) % this._selections.length; + cursorRect = this._selections[this._cursorRectIndex]; + newRow = Math.min(cursorRect.r1, cursorRect.r2); + newColumn = Math.min(cursorRect.c1, cursorRect.c2); + }; + + const moveToPreviousRect = () => { + this._cursorRectIndex = this._cursorRectIndex === 0 ? this._selections.length - 1 : this._cursorRectIndex - 1; + cursorRect = this._selections[this._cursorRectIndex]; + newRow = Math.max(cursorRect.r1, cursorRect.r2); + newColumn = Math.max(cursorRect.c1, cursorRect.c2); + }; + if (newRow > r2) { newRow = r1; newColumn += 1; + if (newColumn > c2) { + moveToNextRect(); + } } else if (newRow < r1) { newRow = r2; newColumn -= 1; - } - - if (newColumn > c2 && newRow != r2) { + if (newColumn < c1) { + moveToPreviousRect(); + } + } else if (newColumn > c2) { newColumn = c1; newRow += 1; - } else if (newColumn < c1 && newRow != r1) { + if (newRow > r2) { + moveToNextRect(); + } + } else if (newColumn < c1) { newColumn = c2; newRow -= 1; - } - - // if going downward/right and the last cell in the selection rectangle visited, - // move to next rectangle - if (newColumn > c2) { - this._cursorRectIndex = (this._cursorRectIndex + 1) % this._selections.length; - cursorRect = this._selections[this._cursorRectIndex]; - newRow = Math.min(cursorRect.r1, cursorRect.r2); - newColumn = Math.min(cursorRect.c1, cursorRect.c2); - } - // if going upward/left and the first cell in the selection rectangle visited, - // move to previous rectangle - else if (newColumn < c1) { - this._cursorRectIndex = this._cursorRectIndex === 0 ? this._selections.length - 1 : this._cursorRectIndex - 1; - cursorRect = this._selections[this._cursorRectIndex]; - newRow = Math.max(cursorRect.r1, cursorRect.r2); - newColumn = Math.max(cursorRect.c1, cursorRect.c2); + if (newRow < r1) { + moveToPreviousRect(); + } } this._cursorRow = newRow; From 50897dc6290db8244a66aea7ade830daa7756954 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sun, 20 Oct 2019 22:54:13 -0400 Subject: [PATCH 25/36] editable grid example --- examples/example-datagrid/src/index.ts | 312 ++++++++++++++++++++++++- packages/datagrid/src/celleditor.ts | 1 + 2 files changed, 312 insertions(+), 1 deletion(-) diff --git a/examples/example-datagrid/src/index.ts b/examples/example-datagrid/src/index.ts index dbd5b8311..f9601c180 100644 --- a/examples/example-datagrid/src/index.ts +++ b/examples/example-datagrid/src/index.ts @@ -9,7 +9,7 @@ import 'es6-promise/auto'; // polyfill Promise on IE import { BasicKeyHandler, BasicMouseHandler, BasicSelectionModel, CellRenderer, - DataGrid, DataModel, JSONModel, TextRenderer + DataGrid, DataModel, JSONModel, TextRenderer, MutableDataModel, CellEditor } from '@phosphor/datagrid'; import { @@ -158,6 +158,139 @@ class RandomDataModel extends DataModel { private _data: number[] = []; } +class JSONCellEditor extends CellEditor { + startEditing() { + this._createWidgets(); + } + + _createWidgets() { + const cell = this._cell; + const grid = this._cell.grid; + if (!grid.dataModel) { + this.cancel(); + return; + } + + let data = grid.dataModel.data('body', cell.row, cell.column); + + const button = document.createElement('button'); + button.type = 'button'; + button.classList.add('cell-editor'); + button.style.whiteSpace = 'nowrap'; + button.style.overflow = 'hidden'; + button.style.textOverflow = 'ellipsis'; + + button.textContent = this.deserialize(data); + this._form.appendChild(button); + + this._button = button; + + const width = 200; + const height = 50; + const textarea = document.createElement('textarea'); + textarea.style.pointerEvents = 'auto'; + textarea.style.position = 'absolute'; + const buttonRect = this._button.getBoundingClientRect(); + const top = buttonRect.bottom + 2; + const left = buttonRect.left; + + textarea.style.top = top + 'px'; + textarea.style.left = left + 'px'; + textarea.style.width = width + 'px'; + textarea.style.height = height + 'px'; + + textarea.value = JSON.stringify(data); + + textarea.addEventListener('keydown', (event: KeyboardEvent) => { + if (event.keyCode === 13) { + if (!this.commit(event.shiftKey ? "up" : "down")) { + this.validInput = false; + event.preventDefault(); + event.stopPropagation(); + } + } else if (event.keyCode === 27) { + this.cancel(); + } + }); + + this._textarea = textarea; + + document.body.appendChild(this._textarea); + this._textarea.focus(); + } + + serialize(): any { + return JSON.parse(this._textarea.value); + } + + deserialize(value: any): any { + return JSON.stringify(value); + } + + dispose(): void { + this._textarea.remove(); + + super.dispose(); + } + + private _button: HTMLButtonElement; + private _textarea: HTMLTextAreaElement; +} + +class MutableJSONModel extends MutableDataModel { + constructor(options: JSONModel.IOptions) { + super(); + + this._jsonModel = new JSONModel(options); + } + + rowCount(region: DataModel.RowRegion): number { + return this._jsonModel.rowCount(region); + } + + columnCount(region: DataModel.ColumnRegion): number { + return this._jsonModel.columnCount(region); + } + + metadata(region: DataModel.CellRegion, row: number, column: number): DataModel.Metadata { + return this._jsonModel.metadata(region, row, column); + } + + data(region: DataModel.CellRegion, row: number, column: number): any { + return this._jsonModel.data(region, row, column); + } + + setData(region: DataModel.CellRegion, row: number, column: number, value: any): boolean { + const model = this._jsonModel as any; + + // Set up the field and value variables. + let field: JSONModel.Field; + + // Look up the field and value for the region. + switch (region) { + case 'body': + field = model._bodyFields[column]; + model._data[row][field.name] = value; + break; + default: + throw 'cannot change header data'; + } + + this.emitChanged({ + type: 'cells-changed', + region: 'body', + row: row, + column: column, + rowSpan: 1, + columnSpan: 1 + }); + + return true; + } + + private _jsonModel: JSONModel; +} + const redGreenBlack: CellRenderer.ConfigFunc = ({ value }) => { if (value <= 1 / 3) { @@ -202,6 +335,7 @@ function main(): void { let model3 = new RandomDataModel(15, 10); let model4 = new RandomDataModel(80, 80); let model5 = new JSONModel(Data.cars); + let model6 = new MutableJSONModel(Data.editable_test_data); let blueStripeStyle: DataGrid.Style = { ...DataGrid.defaultStyle, @@ -287,11 +421,36 @@ function main(): void { selectionMode: 'row' }); + let grid6 = new DataGrid({ + defaultSizes: { + rowHeight: 32, + columnWidth: 90, + rowHeaderWidth: 64, + columnHeaderHeight: 32 + } + }); + grid6.dataModel = model6; + grid6.keyHandler = new BasicKeyHandler(); + grid6.mouseHandler = new BasicMouseHandler(); + grid6.selectionModel = new BasicSelectionModel({ + dataModel: model6, + selectionMode: 'cell' + }); + grid6.editingEnabled = true; + const jsonCellEditor = new JSONCellEditor(); + const columnIdentifier = {'name': 'Corp. Data'}; + grid6.editorController!.setEditor(columnIdentifier, jsonCellEditor); + + let grid7 = new DataGrid(); + grid7.dataModel = model6; + let wrapper1 = createWrapper(grid1, 'Trillion Rows/Cols'); let wrapper2 = createWrapper(grid2, 'Streaming Rows'); let wrapper3 = createWrapper(grid3, 'Random Ticks 1'); let wrapper4 = createWrapper(grid4, 'Random Ticks 2'); let wrapper5 = createWrapper(grid5, 'JSON Data'); + let wrapper6 = createWrapper(grid6, 'Editable Grid'); + let wrapper7 = createWrapper(grid7, 'Copy'); let dock = new DockPanel(); dock.id = 'dock'; @@ -301,6 +460,9 @@ function main(): void { dock.addWidget(wrapper3, { mode: 'split-bottom', ref: wrapper1 }); dock.addWidget(wrapper4, { mode: 'split-bottom', ref: wrapper2 }); dock.addWidget(wrapper5, { mode: 'split-bottom', ref: wrapper2 }); + dock.addWidget(wrapper6, { mode: 'tab-before', ref: wrapper1 }); + dock.addWidget(wrapper7, { mode: 'split-bottom', ref: wrapper6 }); + dock.activateWidget(wrapper6); window.onresize = () => { dock.update(); }; @@ -1011,4 +1173,152 @@ namespace Data { } } + export + const editable_test_data = { + "data": [ + { + "index": 0, + "Name": "Chevrolet", + "Contact": "info@chevrolet.com", + "Origin": "USA", + "Cylinders": 8, + "Horsepower": 130.0, + "Models": 2, + "Automatic": true, + "Date in Service": "1980-01-02", + "Corp. Data": {"headquarter": "USA", "num_employees": 100, "locations": 80} + }, + { + "index": 1, + "Name": "BMW", + "Contact": "info@bmw.com", + "Origin": "Germany", + "Cylinders": 8, + "Horsepower": 120.0, + "Models": 3, + "Automatic": true, + "Date in Service": "1990-11-22", + "Corp. Data": {"headquarter": "Germany", "num_employees": 200, "locations": 20} + }, + { + "index": 2, + "Name": "Mercedes", + "Contact": "info@mbusa.com", + "Origin": "Germany", + "Cylinders": 4, + "Horsepower": 100.0, + "Models": 5, + "Automatic": false, + "Date in Service": "1970-06-13", + "Corp. Data": {"headquarter": "Germany", "num_employees": 250, "locations": 45} + }, + { + "index": 3, + "Name": "Honda", + "Contact": "info@honda.com", + "Origin": "Japan", + "Cylinders": 4, + "Horsepower": 90.0, + "Models": 5, + "Automatic": true, + "Date in Service": "1985-05-09", + "Corp. Data": {"headquarter": "Germany", "num_employees": 200, "locations": 40} + }, + { + "index": 4, + "Name": "Toyota", + "Contact": "info@toyota.com", + "Origin": "Japan", + "Cylinders": 8, + "Horsepower": 95.0, + "Models": 7, + "Automatic": true, + "Date in Service": "1975-05-19", + "Corp. Data": {"headquarter": "Japan", "num_employees": 500, "locations": 70} + }, + { + "index": 5, + "Name": "Renault", + "Contact": "info@renault.com", + "Origin": "France", + "Cylinders": 4, + "Horsepower": 75.0, + "Models": 4, + "Automatic": false, + "Date in Service": "1962-07-28", + "Corp. Data": {"headquarter": "France", "num_employees": 400, "locations": 80} + }, + ], + "schema": { + "primaryKey": [ + "index" + ], + "fields": [ + { + "name": "index", + "type": "integer" + }, + { + "name": "Name", + "type": "string", + "constraint": { + "minLength": 2, + "maxLength": 100, + "pattern": "[a-zA-Z]" + } + }, + { + "name": "Origin", + "type": "string", + "constraint": { + "enum": "dynamic" + } + }, + { + "name": "Cylinders", + "type": "integer", + "constraint": { + "enum": [ + 2, 3, 4, 6, 8, 16 + ] + } + }, + { + "name": "Horsepower", + "type": "number", + "constraint": { + "minimum": 50, + "maximum": 900 + } + }, + { + "name": "Models", + "type": "integer", + "constraint": { + "minimum": 1, + "maximum": 30 + } + }, + { + "name": "Automatic", + "type": "boolean" + }, + { + "name": "Date in Service", + "type": "date" + }, + { + "name": "Contact", + "type": "string", + "format": "email" + }, + { + "name": "Corp. Data", + "type": "object" + } + ], + "pandas_version": "0.20.0" + } + } + } diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index c041ce6e5..07d27d799 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -63,6 +63,7 @@ interface ICellEditOptions { export interface ICellEditorController { + setEditor(identifier: CellDataType | DataModel.Metadata, editor: ICellEditor): void; edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): boolean; cancel(): void; } From 63bf6d3b38289beb49c5fbd876ff901b69813c9c Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Mon, 21 Oct 2019 12:46:30 -0400 Subject: [PATCH 26/36] move controller to separate file --- packages/datagrid/src/celleditor.ts | 184 +---------------- packages/datagrid/src/celleditorcontroller.ts | 195 ++++++++++++++++++ packages/datagrid/src/datagrid.ts | 2 +- 3 files changed, 203 insertions(+), 178 deletions(-) create mode 100644 packages/datagrid/src/celleditorcontroller.ts diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 07d27d799..e3fc7477a 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -1,3 +1,10 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2014-2019, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ import { Widget } from '@phosphor/widgets'; @@ -18,10 +25,6 @@ import { getKeyboardLayout } from '@phosphor/keyboard'; -import { - DataModel -} from './datamodel'; - export interface ICellInputValidatorResponse { valid: boolean; @@ -61,179 +64,6 @@ interface ICellEditOptions { onCancel?: () => void; } -export -interface ICellEditorController { - setEditor(identifier: CellDataType | DataModel.Metadata, editor: ICellEditor): void; - edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): boolean; - cancel(): void; -} - -export -class CellEditorController implements ICellEditorController { - edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): boolean { - const grid = cell.grid; - - if (!grid.editable) { - console.error('Grid cannot be edited!'); - return false; - } - - this.cancel(); - - if (options && options.editor) { - options.editor.edit(cell, options); - return true; - } - - const editor = this._getEditor(cell); - if (editor) { - editor.edit(cell, options); - return true; - } - - return false; - } - - cancel(): void { - if (this._editor) { - this._editor.cancel(); - this._editor = null; - } - } - - private _getDataTypeKey(cell: CellEditor.CellConfig): string { - const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : null; - - if (!metadata) { - return 'undefined'; - } - - let key = ''; - - if (metadata) { - key = metadata.type; - } - - if (metadata.constraint && metadata.constraint.enum) { - if (metadata.constraint.enum === 'dynamic') { - key += ':dynamic-option'; - } else { - key += ':option'; - } - } - - return key; - } - - private _objectToKey(object: any): string { - let str = ''; - for (let key in object) { - const value = object[key]; - if (typeof value === 'object') { - str += `${key}:${this._objectToKey(value)}`; - } else { - str += `[${key}:${value}]`; - } - } - - return str; - } - - private _metadataIdentifierToKey(metadata: DataModel.Metadata): string { - return this._objectToKey(metadata); - } - - private _metadataMatchesIdentifier(metadata: DataModel.Metadata, identifier: DataModel.Metadata): boolean { - for (let key in identifier) { - if (!metadata.hasOwnProperty(key)) { - return false; - } - - const identifierValue = identifier[key]; - const metadataValue = metadata[key]; - if (typeof identifierValue === 'object') { - if (!this._metadataMatchesIdentifier(metadataValue, identifierValue)) { - return false; - } - } else if (metadataValue !== identifierValue) { - return false; - } - } - - return true; - } - - private _getMetadataBasedEditor(metadata: DataModel.Metadata): ICellEditor | undefined { - for (let key of Array.from(this._metadataBasedOverrides.keys())) { - const [identifier, editor] = this._metadataBasedOverrides.get(key)!; - if (this._metadataMatchesIdentifier(metadata, identifier)) { - return editor; - } - } - - return undefined; - } - - private _getEditor(cell: CellEditor.CellConfig): ICellEditor | undefined { - const dtKey = this._getDataTypeKey(cell); - - if (this._typeBasedOverrides.has(dtKey)) { - return this._typeBasedOverrides.get(dtKey); - } else { - const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); - if (metadata) { - const editor = this._getMetadataBasedEditor(metadata); - if (editor) { - return editor; - } - } - } - - switch (dtKey) { - case 'string': - return new TextCellEditor(); - case 'number': - return new NumberCellEditor(); - case 'integer': - return new IntegerCellEditor(); - case 'boolean': - return new BooleanCellEditor(); - case 'date': - return new DateCellEditor(); - case 'string:option': - case 'number:option': - case 'integer:option': - case 'date:option': - return new OptionCellEditor(); - case 'string:dynamic-option': - case 'number:dynamic-option': - case 'integer:dynamic-option': - case 'date:dynamic-option': - return new DynamicOptionCellEditor(); - } - - const data = cell.grid.dataModel!.data('body', cell.row, cell.column); - if (!data || typeof data !== 'object') { - return new TextCellEditor(); - } - - return undefined; - } - - setEditor(identifier: CellDataType | DataModel.Metadata, editor: ICellEditor) { - if (typeof identifier === 'string') { - this._typeBasedOverrides.set(identifier, editor); - } else { - const key = this._metadataIdentifierToKey(identifier); - this._metadataBasedOverrides.set(key, [identifier, editor]); - } - } - - private _editor: ICellEditor | null = null; - private _typeBasedOverrides: Map = new Map(); - private _metadataBasedOverrides: Map = new Map(); -} - export class PassInputValidator implements ICellInputValidator { validate(cell: CellEditor.CellConfig, value: any): ICellInputValidatorResponse { diff --git a/packages/datagrid/src/celleditorcontroller.ts b/packages/datagrid/src/celleditorcontroller.ts new file mode 100644 index 000000000..144344067 --- /dev/null +++ b/packages/datagrid/src/celleditorcontroller.ts @@ -0,0 +1,195 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2014-2019, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +import { + ICellEditor, + CellEditor, + CellDataType, + ICellEditOptions, + TextCellEditor, + NumberCellEditor, + IntegerCellEditor, + BooleanCellEditor, + DateCellEditor, + OptionCellEditor, + DynamicOptionCellEditor +} from './celleditor'; + +import { DataModel } from './datamodel'; + +export +interface ICellEditorController { + setEditor(identifier: CellDataType | DataModel.Metadata, editor: ICellEditor): void; + edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): boolean; + cancel(): void; +} + +export +class CellEditorController implements ICellEditorController { + edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): boolean { + const grid = cell.grid; + + if (!grid.editable) { + console.error('Grid cannot be edited!'); + return false; + } + + this.cancel(); + + if (options && options.editor) { + options.editor.edit(cell, options); + return true; + } + + const editor = this._getEditor(cell); + if (editor) { + editor.edit(cell, options); + return true; + } + + return false; + } + + cancel(): void { + if (this._editor) { + this._editor.cancel(); + this._editor = null; + } + } + + private _getDataTypeKey(cell: CellEditor.CellConfig): string { + const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : null; + + if (!metadata) { + return 'undefined'; + } + + let key = ''; + + if (metadata) { + key = metadata.type; + } + + if (metadata.constraint && metadata.constraint.enum) { + if (metadata.constraint.enum === 'dynamic') { + key += ':dynamic-option'; + } else { + key += ':option'; + } + } + + return key; + } + + private _objectToKey(object: any): string { + let str = ''; + for (let key in object) { + const value = object[key]; + if (typeof value === 'object') { + str += `${key}:${this._objectToKey(value)}`; + } else { + str += `[${key}:${value}]`; + } + } + + return str; + } + + private _metadataIdentifierToKey(metadata: DataModel.Metadata): string { + return this._objectToKey(metadata); + } + + private _metadataMatchesIdentifier(metadata: DataModel.Metadata, identifier: DataModel.Metadata): boolean { + for (let key in identifier) { + if (!metadata.hasOwnProperty(key)) { + return false; + } + + const identifierValue = identifier[key]; + const metadataValue = metadata[key]; + if (typeof identifierValue === 'object') { + if (!this._metadataMatchesIdentifier(metadataValue, identifierValue)) { + return false; + } + } else if (metadataValue !== identifierValue) { + return false; + } + } + + return true; + } + + private _getMetadataBasedEditor(metadata: DataModel.Metadata): ICellEditor | undefined { + for (let key of Array.from(this._metadataBasedOverrides.keys())) { + const [identifier, editor] = this._metadataBasedOverrides.get(key)!; + if (this._metadataMatchesIdentifier(metadata, identifier)) { + return editor; + } + } + + return undefined; + } + + private _getEditor(cell: CellEditor.CellConfig): ICellEditor | undefined { + const dtKey = this._getDataTypeKey(cell); + + if (this._typeBasedOverrides.has(dtKey)) { + return this._typeBasedOverrides.get(dtKey); + } else { + const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); + if (metadata) { + const editor = this._getMetadataBasedEditor(metadata); + if (editor) { + return editor; + } + } + } + + switch (dtKey) { + case 'string': + return new TextCellEditor(); + case 'number': + return new NumberCellEditor(); + case 'integer': + return new IntegerCellEditor(); + case 'boolean': + return new BooleanCellEditor(); + case 'date': + return new DateCellEditor(); + case 'string:option': + case 'number:option': + case 'integer:option': + case 'date:option': + return new OptionCellEditor(); + case 'string:dynamic-option': + case 'number:dynamic-option': + case 'integer:dynamic-option': + case 'date:dynamic-option': + return new DynamicOptionCellEditor(); + } + + const data = cell.grid.dataModel!.data('body', cell.row, cell.column); + if (!data || typeof data !== 'object') { + return new TextCellEditor(); + } + + return undefined; + } + + setEditor(identifier: CellDataType | DataModel.Metadata, editor: ICellEditor) { + if (typeof identifier === 'string') { + this._typeBasedOverrides.set(identifier, editor); + } else { + const key = this._metadataIdentifierToKey(identifier); + this._metadataBasedOverrides.set(key, [identifier, editor]); + } + } + + private _editor: ICellEditor | null = null; + private _typeBasedOverrides: Map = new Map(); + private _metadataBasedOverrides: Map = new Map(); +} diff --git a/packages/datagrid/src/datagrid.ts b/packages/datagrid/src/datagrid.ts index 5f3f792e3..3f091ae24 100644 --- a/packages/datagrid/src/datagrid.ts +++ b/packages/datagrid/src/datagrid.ts @@ -48,7 +48,7 @@ import { import { SelectionModel } from './selectionmodel'; -import { ICellEditorController, CellEditorController } from './celleditor'; +import { ICellEditorController, CellEditorController } from './celleditorcontroller'; /** From b4ec7b47fcdd7c789fb6d0d1a852fbcbcbb1fa5d Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Mon, 21 Oct 2019 12:54:15 -0400 Subject: [PATCH 27/36] refactoring --- packages/datagrid/src/celleditor.ts | 146 ++++++++++++++-------------- 1 file changed, 74 insertions(+), 72 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index e3fc7477a..db8519139 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -175,7 +175,6 @@ export abstract class CellEditor implements ICellEditor, IDisposable { protected abstract startEditing(): void; protected abstract serialize(): any; - protected abstract deserialize(value: any): any; /** * Whether the cell editor is disposed. @@ -196,77 +195,7 @@ abstract class CellEditor implements ICellEditor, IDisposable { this._cell = cell; this._onCommit = options && options.onCommit; - if (options && options.validator) { - this._validator = options.validator; - } else { - const metadata = cell.grid.dataModel ? cell.grid.dataModel.metadata('body', cell.row, cell.column) : null; - - switch (metadata && metadata.type) { - case 'string': - { - const validator = new TextInputValidator(); - if (typeof(metadata!.format) === 'string') { - const format = metadata!.format; - switch (format) { - case 'email': - validator.pattern = new RegExp("^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$"); - break; - case 'uuid': - validator.pattern = new RegExp("[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}"); - break; - case 'uri': - // TODO - break; - case 'binary': - // TODO - break; - } - } - - if (metadata!.constraint) { - if (metadata!.constraint.minLength !== undefined) { - validator.minLength = metadata!.constraint.minLength; - } - if (metadata!.constraint.maxLength !== undefined) { - validator.maxLength = metadata!.constraint.maxLength; - } - if (typeof(metadata!.constraint.pattern) === 'string') { - validator.pattern = new RegExp(metadata!.constraint.pattern); - } - } - this._validator = validator; - } - break; - case 'number': - { - const validator = new NumberInputValidator(); - if (metadata!.constraint) { - if (metadata!.constraint.minimum !== undefined) { - validator.min = metadata!.constraint.minimum; - } - if (metadata!.constraint.maximum !== undefined) { - validator.max = metadata!.constraint.maximum; - } - } - this._validator = validator; - } - break; - case 'integer': - { - const validator = new IntegerInputValidator(); - if (metadata!.constraint) { - if (metadata!.constraint.minimum !== undefined) { - validator.min = metadata!.constraint.minimum; - } - if (metadata!.constraint.maximum !== undefined) { - validator.max = metadata!.constraint.maximum; - } - } - this._validator = validator; - } - break; - } - } + this._validator = (options && options.validator) ? options.validator : this.createValidatorBasedOnType(); cell.grid.node.addEventListener('wheel', () => { this.updatePosition(); @@ -285,6 +214,79 @@ abstract class CellEditor implements ICellEditor, IDisposable { } } + protected createValidatorBasedOnType(): ICellInputValidator | undefined { + const cell = this._cell; + const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); + + switch (metadata && metadata.type) { + case 'string': + { + const validator = new TextInputValidator(); + if (typeof(metadata!.format) === 'string') { + const format = metadata!.format; + switch (format) { + case 'email': + validator.pattern = new RegExp("^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$"); + break; + case 'uuid': + validator.pattern = new RegExp("[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}"); + break; + case 'uri': + // TODO + break; + case 'binary': + // TODO + break; + } + } + + if (metadata!.constraint) { + if (metadata!.constraint.minLength !== undefined) { + validator.minLength = metadata!.constraint.minLength; + } + if (metadata!.constraint.maxLength !== undefined) { + validator.maxLength = metadata!.constraint.maxLength; + } + if (typeof(metadata!.constraint.pattern) === 'string') { + validator.pattern = new RegExp(metadata!.constraint.pattern); + } + } + return validator; + } + break; + case 'number': + { + const validator = new NumberInputValidator(); + if (metadata!.constraint) { + if (metadata!.constraint.minimum !== undefined) { + validator.min = metadata!.constraint.minimum; + } + if (metadata!.constraint.maximum !== undefined) { + validator.max = metadata!.constraint.maximum; + } + } + return validator; + } + break; + case 'integer': + { + const validator = new IntegerInputValidator(); + if (metadata!.constraint) { + if (metadata!.constraint.minimum !== undefined) { + validator.min = metadata!.constraint.minimum; + } + if (metadata!.constraint.maximum !== undefined) { + validator.max = metadata!.constraint.maximum; + } + } + return validator; + } + break; + } + + return undefined; + } + protected getCellInfo(cell: CellEditor.CellConfig) { const { grid, row, column } = cell; const data = grid.dataModel!.data('body', row, column); From 4a082e6050bf4d87d35c519e5bf88b77468f184e Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Mon, 21 Oct 2019 14:08:54 -0400 Subject: [PATCH 28/36] remove unused interfaces --- packages/datagrid/src/celleditor.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index db8519139..376977b4f 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -5,10 +5,6 @@ | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ -import { - Widget -} from '@phosphor/widgets'; - import { IDisposable } from '@phosphor/disposable'; @@ -642,8 +638,8 @@ class DateCellEditor extends CellEditor { this._onKeyDown(event as KeyboardEvent); break; case 'blur': - this._onBlur(event as FocusEvent); - break; + this._onBlur(event as FocusEvent); + break; } } @@ -1043,17 +1039,6 @@ class DynamicOptionCellEditor extends CellEditor { export namespace CellEditor { - export - interface IOptions extends Widget.IOptions { - grid: DataGrid; - row: number; - column: number; - } - - export - interface IInputOptions extends IOptions { - } - /** * An object which holds the configuration data for a cell. */ From d4a8b612085f36fd9dea138758a0b5daf4facd93 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Mon, 21 Oct 2019 14:57:29 -0400 Subject: [PATCH 29/36] fix onCancel bind, refactoring --- examples/example-datagrid/src/index.ts | 18 ++++++++++++----- packages/datagrid/src/basickeyhandler.ts | 21 +++++++++++--------- packages/datagrid/src/basicmousehandler.ts | 6 +++++- packages/datagrid/src/basicselectionmodel.ts | 2 +- packages/datagrid/src/celleditor.ts | 3 ++- packages/datagrid/src/datagrid.ts | 8 ++++++-- 6 files changed, 39 insertions(+), 19 deletions(-) diff --git a/examples/example-datagrid/src/index.ts b/examples/example-datagrid/src/index.ts index f9601c180..8ad20bb87 100644 --- a/examples/example-datagrid/src/index.ts +++ b/examples/example-datagrid/src/index.ts @@ -16,6 +16,10 @@ import { DockPanel, StackedPanel, Widget } from '@phosphor/widgets'; +import { + getKeyboardLayout +} from '@phosphor/keyboard'; + import '../style/index.css'; @@ -202,13 +206,17 @@ class JSONCellEditor extends CellEditor { textarea.value = JSON.stringify(data); textarea.addEventListener('keydown', (event: KeyboardEvent) => { - if (event.keyCode === 13) { - if (!this.commit(event.shiftKey ? "up" : "down")) { + const key = getKeyboardLayout().keyForKeydownEvent(event); + if (key === 'Enter' || key === 'Tab' ) { + const next = key === 'Enter' ? + (event.shiftKey ? "up" : "down") : + (event.shiftKey ? "left" : "right"); + if (!this.commit(next)) { this.validInput = false; - event.preventDefault(); - event.stopPropagation(); } - } else if (event.keyCode === 27) { + event.preventDefault(); + event.stopPropagation(); + } else if (key === 'Escape') { this.cancel(); } }); diff --git a/packages/datagrid/src/basickeyhandler.ts b/packages/datagrid/src/basickeyhandler.ts index a40e9a2ee..d77a9b29c 100644 --- a/packages/datagrid/src/basickeyhandler.ts +++ b/packages/datagrid/src/basickeyhandler.ts @@ -21,9 +21,14 @@ import { SelectionModel } from './selectionmodel'; -import { ICellEditResponse, CellEditor } from './celleditor'; +import { + ICellEditResponse, + CellEditor +} from './celleditor'; -import { MutableDataModel } from './datamodel'; +import { + MutableDataModel +} from './datamodel'; /** @@ -62,8 +67,8 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { if (grid.editable && grid.selectionModel!.cursorRow !== -1 && grid.selectionModel!.cursorColumn !== -1) { - const inp = String.fromCharCode(event.keyCode); - if (/[a-zA-Z0-9-_ ]/.test(inp)) { + const input = String.fromCharCode(event.keyCode); + if (/[a-zA-Z0-9-_ ]/.test(input)) { const row = grid.selectionModel!.cursorRow; const column = grid.selectionModel!.cursorColumn; const cell: CellEditor.CellConfig = { @@ -769,16 +774,14 @@ class BasicKeyHandler implements DataGrid.IKeyHandler { * @param event - The keyboard event of interest. */ protected onDelete(grid: DataGrid, event: KeyboardEvent): void { - if (grid.selectionModel && - !grid.selectionModel.isEmpty && - grid.dataModel instanceof MutableDataModel) { - + if (grid.editable && + !grid.selectionModel!.isEmpty) { const dataModel = grid.dataModel as MutableDataModel; // Fetch the max row and column. let maxRow = dataModel.rowCount('body') - 1; let maxColumn = dataModel.columnCount('body') - 1; - const it = grid.selectionModel.selections(); + const it = grid.selectionModel!.selections(); let s: SelectionModel.Selection | undefined; while ((s = it.next()) !== undefined) { // Clamp the cell to the model bounds. diff --git a/packages/datagrid/src/basicmousehandler.ts b/packages/datagrid/src/basicmousehandler.ts index 33ed9cf50..0d2257f1e 100644 --- a/packages/datagrid/src/basicmousehandler.ts +++ b/packages/datagrid/src/basicmousehandler.ts @@ -28,7 +28,11 @@ import { import { SelectionModel } from './selectionmodel'; -import { ICellEditResponse, CellEditor } from './celleditor'; + +import { + ICellEditResponse, + CellEditor +} from './celleditor'; /** * A basic implementation of a data grid mouse handler. diff --git a/packages/datagrid/src/basicselectionmodel.ts b/packages/datagrid/src/basicselectionmodel.ts index 96fbe29a2..bc8b50898 100644 --- a/packages/datagrid/src/basicselectionmodel.ts +++ b/packages/datagrid/src/basicselectionmodel.ts @@ -49,7 +49,7 @@ class BasicSelectionModel extends SelectionModel { } /** - * Move cursor down/up while making sure it remains + * Move cursor down/up/left/right while making sure it remains * within the bounds of selected rectangles */ moveCursorWithinSelections(direction: SelectionModel.CursorMoveDirection): void { diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 376977b4f..cdaf9862b 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -190,6 +190,7 @@ abstract class CellEditor implements ICellEditor, IDisposable { edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): void { this._cell = cell; this._onCommit = options && options.onCommit; + this._onCancel = options && options.onCancel; this._validator = (options && options.validator) ? options.validator : this.createValidatorBasedOnType(); @@ -372,7 +373,7 @@ abstract class CellEditor implements ICellEditor, IDisposable { } protected _onCommit?: (response: ICellEditResponse) => void; - protected _onCancel: () => void; + protected _onCancel?: () => void; protected _cell: CellEditor.CellConfig; protected _validator: ICellInputValidator | undefined; protected _viewportOccluder: HTMLDivElement; diff --git a/packages/datagrid/src/datagrid.ts b/packages/datagrid/src/datagrid.ts index 3f091ae24..82f85e768 100644 --- a/packages/datagrid/src/datagrid.ts +++ b/packages/datagrid/src/datagrid.ts @@ -48,7 +48,11 @@ import { import { SelectionModel } from './selectionmodel'; -import { ICellEditorController, CellEditorController } from './celleditorcontroller'; + +import { + ICellEditorController, + CellEditorController +} from './celleditorcontroller'; /** @@ -826,7 +830,7 @@ class DataGrid extends Widget { } /** - * Move cursor down/up while making sure it remains + * Move cursor down/up/left/right while making sure it remains * within the bounds of selected rectangles */ moveCursor(direction: SelectionModel.CursorMoveDirection): void { From 9345e9cd81ec2aed6ab87d9f100a06364a882120 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 22 Oct 2019 11:38:57 -0400 Subject: [PATCH 30/36] set active editor in controller, cancel editing on grid resize --- packages/datagrid/src/celleditorcontroller.ts | 2 ++ packages/datagrid/src/datagrid.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/datagrid/src/celleditorcontroller.ts b/packages/datagrid/src/celleditorcontroller.ts index 144344067..0170a4bc6 100644 --- a/packages/datagrid/src/celleditorcontroller.ts +++ b/packages/datagrid/src/celleditorcontroller.ts @@ -41,12 +41,14 @@ class CellEditorController implements ICellEditorController { this.cancel(); if (options && options.editor) { + this._editor = options.editor; options.editor.edit(cell, options); return true; } const editor = this._getEditor(cell); if (editor) { + this._editor = editor; editor.edit(cell, options); return true; } diff --git a/packages/datagrid/src/datagrid.ts b/packages/datagrid/src/datagrid.ts index 82f85e768..265b26ac1 100644 --- a/packages/datagrid/src/datagrid.ts +++ b/packages/datagrid/src/datagrid.ts @@ -1891,6 +1891,10 @@ class DataGrid extends Widget { * A message handler invoked on a `'resize'` message. */ protected onResize(msg: Widget.ResizeMessage): void { + if (this._editorController) { + this._editorController.cancel(); + } + this._syncScrollState(); } From f4a92a72bcedee350a4e4ce2b5e398ee635f1007 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 22 Oct 2019 13:58:25 -0400 Subject: [PATCH 31/36] check if disposed before canceling --- packages/datagrid/src/celleditor.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index cdaf9862b..2babc458e 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -183,6 +183,10 @@ abstract class CellEditor implements ICellEditor, IDisposable { * Dispose of the resources held by cell editor handler. */ dispose(): void { + if (this._disposed) { + return; + } + this._disposed = true; this._cell.grid.node.removeChild(this._viewportOccluder); } @@ -205,6 +209,10 @@ abstract class CellEditor implements ICellEditor, IDisposable { } cancel() { + if (this._disposed) { + return; + } + this.dispose(); if (this._onCancel) { this._onCancel(); From 8f3e90cad57131e1fad770fa259f6c2731d441c3 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 22 Oct 2019 19:29:56 -0400 Subject: [PATCH 32/36] editor resolver as an override option --- examples/example-datagrid/src/index.ts | 7 +- packages/datagrid/src/celleditorcontroller.ts | 71 ++++++++++++++----- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/examples/example-datagrid/src/index.ts b/examples/example-datagrid/src/index.ts index 8ad20bb87..e45e304ba 100644 --- a/examples/example-datagrid/src/index.ts +++ b/examples/example-datagrid/src/index.ts @@ -9,7 +9,7 @@ import 'es6-promise/auto'; // polyfill Promise on IE import { BasicKeyHandler, BasicMouseHandler, BasicSelectionModel, CellRenderer, - DataGrid, DataModel, JSONModel, TextRenderer, MutableDataModel, CellEditor + DataGrid, DataModel, JSONModel, TextRenderer, MutableDataModel, CellEditor, ICellEditor } from '@phosphor/datagrid'; import { @@ -445,9 +445,10 @@ function main(): void { selectionMode: 'cell' }); grid6.editingEnabled = true; - const jsonCellEditor = new JSONCellEditor(); const columnIdentifier = {'name': 'Corp. Data'}; - grid6.editorController!.setEditor(columnIdentifier, jsonCellEditor); + grid6.editorController!.setEditor(columnIdentifier, (config: CellEditor.CellConfig): ICellEditor => { + return new JSONCellEditor(); + }); let grid7 = new DataGrid(); grid7.dataModel = model6; diff --git a/packages/datagrid/src/celleditorcontroller.ts b/packages/datagrid/src/celleditorcontroller.ts index 0170a4bc6..569dc51f3 100644 --- a/packages/datagrid/src/celleditorcontroller.ts +++ b/packages/datagrid/src/celleditorcontroller.ts @@ -23,11 +23,47 @@ import { DataModel } from './datamodel'; export interface ICellEditorController { - setEditor(identifier: CellDataType | DataModel.Metadata, editor: ICellEditor): void; + setEditor(identifier: CellDataType | DataModel.Metadata, editor: ICellEditor | Resolver): void; edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): boolean; cancel(): void; } +/** + * A type alias for a cell editor config function. + * + * This type is used to compute a value from a cell config object. + */ +export +type ConfigFunc = (config: CellEditor.CellConfig) => T; + +/** + * A type alias for a cell editor config option. + * + * A config option can be a static value or a config function. + */ +export +type ConfigOption = T | ConfigFunc; + +/** + * A type alias for a cell editor resolver function. + */ +export +type Resolver = ConfigFunc; + +/** + * Resolve a config option for a cell editor. + * + * @param option - The config option to resolve. + * + * @param config - The cell config object. + * + * @returns The resolved value for the option. + */ +export +function resolveOption(option: ConfigOption, config: CellEditor.CellConfig): T { + return typeof option === 'function' ? option(config) : option; +} + export class CellEditorController implements ICellEditorController { edit(cell: CellEditor.CellConfig, options?: ICellEditOptions): boolean { @@ -125,11 +161,14 @@ class CellEditorController implements ICellEditorController { return true; } - private _getMetadataBasedEditor(metadata: DataModel.Metadata): ICellEditor | undefined { - for (let key of Array.from(this._metadataBasedOverrides.keys())) { - const [identifier, editor] = this._metadataBasedOverrides.get(key)!; - if (this._metadataMatchesIdentifier(metadata, identifier)) { - return editor; + private _getMetadataBasedEditor(cell: CellEditor.CellConfig): ICellEditor | undefined { + const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); + if (metadata) { + for (let key of Array.from(this._metadataBasedOverrides.keys())) { + let [identifier, editor] = this._metadataBasedOverrides.get(key)!; + if (this._metadataMatchesIdentifier(metadata, identifier)) { + return resolveOption(editor, cell); + } } } @@ -140,14 +179,12 @@ class CellEditorController implements ICellEditorController { const dtKey = this._getDataTypeKey(cell); if (this._typeBasedOverrides.has(dtKey)) { - return this._typeBasedOverrides.get(dtKey); - } else { - const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); - if (metadata) { - const editor = this._getMetadataBasedEditor(metadata); - if (editor) { - return editor; - } + let editor = this._typeBasedOverrides.get(dtKey); + return resolveOption(editor!, cell); + } else if (this._metadataBasedOverrides.size > 0) { + const editor = this._getMetadataBasedEditor(cell); + if (editor) { + return editor; } } @@ -182,7 +219,7 @@ class CellEditorController implements ICellEditorController { return undefined; } - setEditor(identifier: CellDataType | DataModel.Metadata, editor: ICellEditor) { + setEditor(identifier: CellDataType | DataModel.Metadata, editor: ICellEditor | Resolver) { if (typeof identifier === 'string') { this._typeBasedOverrides.set(identifier, editor); } else { @@ -192,6 +229,6 @@ class CellEditorController implements ICellEditorController { } private _editor: ICellEditor | null = null; - private _typeBasedOverrides: Map = new Map(); - private _metadataBasedOverrides: Map = new Map(); + private _typeBasedOverrides: Map = new Map(); + private _metadataBasedOverrides: Map = new Map(); } From dcc4ae2549af339e29ceebb8d019d7407a86e8f7 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 24 Oct 2019 17:52:54 -0400 Subject: [PATCH 33/36] fix checkbox focus loss problem in some browsers --- packages/datagrid/src/celleditor.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 2babc458e..b11409bb4 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -735,11 +735,13 @@ class BooleanCellEditor extends CellEditor { _bindEvents() { this._input.addEventListener('keydown', this); + this._input.addEventListener('mousedown', this); this._input.addEventListener('blur', this); } _unbindEvents() { this._input.removeEventListener('keydown', this); + this._input.removeEventListener('mousedown', this); this._input.removeEventListener('blur', this); } @@ -748,6 +750,12 @@ class BooleanCellEditor extends CellEditor { case 'keydown': this._onKeyDown(event as KeyboardEvent); break; + case 'mousedown': + // fix focus loss problem in Safari and Firefox + this._input.focus(); + event.stopPropagation(); + event.preventDefault(); + break; case 'blur': this._onBlur(event as FocusEvent); break; From 67141e87726a00499e929449772d531a8acd12b9 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 25 Oct 2019 12:33:50 -0400 Subject: [PATCH 34/36] use ES2015.Collection --- packages/datagrid/src/celleditor.ts | 24 +++++++++---------- packages/datagrid/src/celleditorcontroller.ts | 15 +++++++----- packages/datagrid/tsconfig.json | 2 +- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index b11409bb4..51b1b9c38 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -77,14 +77,14 @@ class TextInputValidator implements ICellInputValidator { }; } - if (!Number.isNaN(this.minLength) && value.length < this.minLength) { + if (!isNaN(this.minLength) && value.length < this.minLength) { return { valid: false, message: `Text length must be greater than ${this.minLength}` }; } - if (!Number.isNaN(this.maxLength) && value.length > this.maxLength) { + if (!isNaN(this.maxLength) && value.length > this.maxLength) { return { valid: false, message: `Text length must be less than ${this.maxLength}` @@ -109,20 +109,20 @@ class TextInputValidator implements ICellInputValidator { export class IntegerInputValidator implements ICellInputValidator { validate(cell: CellEditor.CellConfig, value: number): ICellInputValidatorResponse { - if (Number.isNaN(value) || (value % 1 !== 0)) { + if (isNaN(value) || (value % 1 !== 0)) { return { valid: false, message: 'Input must be valid integer' }; } - if (!Number.isNaN(this.min) && value < this.min) { + if (!isNaN(this.min) && value < this.min) { return { valid: false, message: `Input must be greater than ${this.min}` }; } - if (!Number.isNaN(this.max) && value > this.max) { + if (!isNaN(this.max) && value > this.max) { return { valid: false, message: `Input must be less than ${this.max}` @@ -139,20 +139,20 @@ class IntegerInputValidator implements ICellInputValidator { export class NumberInputValidator implements ICellInputValidator { validate(cell: CellEditor.CellConfig, value: number): ICellInputValidatorResponse { - if (Number.isNaN(value)) { + if (isNaN(value)) { return { valid: false, message: 'Input must be valid number' }; } - if (!Number.isNaN(this.min) && value < this.min) { + if (!isNaN(this.min) && value < this.min) { return { valid: false, message: `Input must be greater than ${this.min}` }; } - if (!Number.isNaN(this.max) && value > this.max) { + if (!isNaN(this.max) && value > this.max) { return { valid: false, message: `Input must be less than ${this.max}` @@ -525,8 +525,8 @@ class NumberCellEditor extends TextCellEditor { return null; } - let floatValue = Number.parseFloat(value); - if (Number.isNaN(floatValue)) { + let floatValue = parseFloat(value); + if (isNaN(floatValue)) { this.validInput = false; this._input.setCustomValidity('Input must be valid number'); this._form.reportValidity(); @@ -583,8 +583,8 @@ class IntegerCellEditor extends NumberCellEditor { return null; } - let intValue = Number.parseInt(value); - if (Number.isNaN(intValue)) { + let intValue = parseInt(value); + if (isNaN(intValue)) { this.validInput = false; this._input.setCustomValidity('Input must be valid number'); this._form.reportValidity(); diff --git a/packages/datagrid/src/celleditorcontroller.ts b/packages/datagrid/src/celleditorcontroller.ts index 569dc51f3..f9481f2d0 100644 --- a/packages/datagrid/src/celleditorcontroller.ts +++ b/packages/datagrid/src/celleditorcontroller.ts @@ -162,17 +162,20 @@ class CellEditorController implements ICellEditorController { } private _getMetadataBasedEditor(cell: CellEditor.CellConfig): ICellEditor | undefined { + let editorMatched: ICellEditor | undefined; const metadata = cell.grid.dataModel!.metadata('body', cell.row, cell.column); if (metadata) { - for (let key of Array.from(this._metadataBasedOverrides.keys())) { - let [identifier, editor] = this._metadataBasedOverrides.get(key)!; - if (this._metadataMatchesIdentifier(metadata, identifier)) { - return resolveOption(editor, cell); + this._metadataBasedOverrides.forEach((value) => { + if (!editorMatched) { + let [identifier, editor] = value; + if (this._metadataMatchesIdentifier(metadata, identifier)) { + editorMatched = resolveOption(editor, cell); + } } - } + }); } - return undefined; + return editorMatched; } private _getEditor(cell: CellEditor.CellConfig): ICellEditor | undefined { diff --git a/packages/datagrid/tsconfig.json b/packages/datagrid/tsconfig.json index 51d3c4738..9e6b07cfb 100644 --- a/packages/datagrid/tsconfig.json +++ b/packages/datagrid/tsconfig.json @@ -12,7 +12,7 @@ "outDir": "lib", "lib": [ "ES5", - "es2015", + "ES2015.Collection", "DOM" ], "types": [], From 2390569aaa8697b558d1ebeb67c49d3f11b9e09e Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 25 Oct 2019 13:45:35 -0400 Subject: [PATCH 35/36] improvements to editable grid example --- examples/example-datagrid/src/index.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/examples/example-datagrid/src/index.ts b/examples/example-datagrid/src/index.ts index e45e304ba..4e0670507 100644 --- a/examples/example-datagrid/src/index.ts +++ b/examples/example-datagrid/src/index.ts @@ -194,6 +194,7 @@ class JSONCellEditor extends CellEditor { const textarea = document.createElement('textarea'); textarea.style.pointerEvents = 'auto'; textarea.style.position = 'absolute'; + textarea.style.outline = 'none'; const buttonRect = this._button.getBoundingClientRect(); const top = buttonRect.bottom + 2; const left = buttonRect.left; @@ -221,6 +222,20 @@ class JSONCellEditor extends CellEditor { } }); + textarea.addEventListener('blur', (event: FocusEvent) => { + if (this.isDisposed) { + return; + } + + if (!this.commit()) { + this.validInput = false; + } + }); + + textarea.addEventListener('input', (event: FocusEvent) => { + this.validInput = true; + }); + this._textarea = textarea; document.body.appendChild(this._textarea); @@ -236,9 +251,13 @@ class JSONCellEditor extends CellEditor { } dispose(): void { - this._textarea.remove(); + if (this.isDisposed) { + return; + } super.dispose(); + + document.body.removeChild(this._textarea); } private _button: HTMLButtonElement; From dce4dfe6ec8f45b85ff40c85c27060200e3437cb Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 6 Dec 2019 09:28:18 -0800 Subject: [PATCH 36/36] fix for blur problem in invalid state, validation refactoring --- examples/example-datagrid/src/index.ts | 2 +- packages/datagrid/src/celleditor.ts | 211 ++++++++++++------------- 2 files changed, 100 insertions(+), 113 deletions(-) diff --git a/examples/example-datagrid/src/index.ts b/examples/example-datagrid/src/index.ts index 4e0670507..2f8dd4e03 100644 --- a/examples/example-datagrid/src/index.ts +++ b/examples/example-datagrid/src/index.ts @@ -242,7 +242,7 @@ class JSONCellEditor extends CellEditor { this._textarea.focus(); } - serialize(): any { + protected getInput(): any { return JSON.parse(this._textarea.value); } diff --git a/packages/datagrid/src/celleditor.ts b/packages/datagrid/src/celleditor.ts index 51b1b9c38..a5536dd30 100644 --- a/packages/datagrid/src/celleditor.ts +++ b/packages/datagrid/src/celleditor.ts @@ -21,6 +21,10 @@ import { getKeyboardLayout } from '@phosphor/keyboard'; +import { + Signal +} from '@phosphor/signaling'; + export interface ICellInputValidatorResponse { valid: boolean; @@ -70,6 +74,10 @@ class PassInputValidator implements ICellInputValidator { export class TextInputValidator implements ICellInputValidator { validate(cell: CellEditor.CellConfig, value: string): ICellInputValidatorResponse { + if (value === null) { + return { valid: true }; + } + if (typeof value !== 'string') { return { valid: false, @@ -109,12 +117,17 @@ class TextInputValidator implements ICellInputValidator { export class IntegerInputValidator implements ICellInputValidator { validate(cell: CellEditor.CellConfig, value: number): ICellInputValidatorResponse { + if (value === null) { + return { valid: true }; + } + if (isNaN(value) || (value % 1 !== 0)) { return { valid: false, message: 'Input must be valid integer' }; } + if (!isNaN(this.min) && value < this.min) { return { valid: false, @@ -139,12 +152,17 @@ class IntegerInputValidator implements ICellInputValidator { export class NumberInputValidator implements ICellInputValidator { validate(cell: CellEditor.CellConfig, value: number): ICellInputValidatorResponse { + if (value === null) { + return { valid: true }; + } + if (isNaN(value)) { return { valid: false, message: 'Input must be valid number' }; } + if (!isNaN(this.min) && value < this.min) { return { valid: false, @@ -170,7 +188,45 @@ class NumberInputValidator implements ICellInputValidator { export abstract class CellEditor implements ICellEditor, IDisposable { protected abstract startEditing(): void; - protected abstract serialize(): any; + protected abstract getInput(): any; + protected inputChanged = new Signal(this); + + constructor() { + this.inputChanged.connect(() => { + this.validate(); + }); + } + + protected validate() { + let value; + try { + value = this.getInput(); + } catch (error) { + console.log(`Input error: ${error.message}`); + this.setValidity(false, error.message || DEFAULT_INVALID_INPUT_MESSAGE); + return; + } + + if (this._validator) { + const result = this._validator.validate(this._cell, value); + if (result.valid) { + this.setValidity(true); + } else { + this.setValidity(false, result.message || DEFAULT_INVALID_INPUT_MESSAGE); + } + } else { + this.setValidity(true); + } + } + + protected setValidity(valid: boolean, message: string = "") { + this.validInput = valid; + + this._validityReportInput.setCustomValidity(message); + if (message !== "") { + this._form.reportValidity(); + } + } /** * Whether the cell editor is disposed. @@ -325,6 +381,25 @@ abstract class CellEditor implements ICellEditor, IDisposable { this._form = document.createElement('form'); this._form.className = 'cell-editor-form'; this._cellContainer.appendChild(this._form); + + this._validityReportInput = document.createElement('input'); + this._validityReportInput.style.opacity = '0'; + this._validityReportInput.style.zIndex = '-1'; + this._validityReportInput.style.position = 'absolute'; + this._validityReportInput.style.left = '0'; + this._validityReportInput.style.top = '0'; + this._validityReportInput.style.width = '100%'; + this._validityReportInput.style.height = '100%'; + this._validityReportInput.style.visibility = 'visible'; + this._form.appendChild(this._validityReportInput); + + // update mouse event pass-through state based on input validity + this._cellContainer.addEventListener('mouseleave', (event: MouseEvent) => { + this._viewportOccluder.style.pointerEvents = this.validInput ? 'none' : 'auto'; + }); + this._cellContainer.addEventListener('mouseenter', (event: MouseEvent) => { + this._viewportOccluder.style.pointerEvents = 'none'; + }); } protected updatePosition(): void { @@ -359,11 +434,17 @@ abstract class CellEditor implements ICellEditor, IDisposable { } protected commit(cursorMovement: SelectionModel.CursorMoveDirection = 'none'): boolean { + this.validate(); + + if (!this.validInput) { + return false; + } + let value; try { - value = this.serialize(); + value = this.getInput(); } catch (error) { - console.error(error); + console.log(`Input error: ${error.message}`); return false; } @@ -387,6 +468,7 @@ abstract class CellEditor implements ICellEditor, IDisposable { protected _viewportOccluder: HTMLDivElement; protected _cellContainer: HTMLDivElement; protected _form: HTMLFormElement; + protected _validityReportInput: HTMLInputElement; private _validInput: boolean = true; private _disposed = false; } @@ -472,28 +554,11 @@ class TextCellEditor extends CellEditor { } _onInput(event: Event) { - this._input.setCustomValidity(""); - this.validInput = true; + this.inputChanged.emit(void 0); } - protected serialize(): any { - const value = this._input.value; - - if (value.trim() === '') { - return null; - } - - if (this._validator) { - const result = this._validator.validate(this._cell, value); - if (!result.valid) { - this.validInput = false; - this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); - this._form.reportValidity(); - throw new Error('Invalid input'); - } - } - - return value; + protected getInput(): any { + return this._input.value; } protected deserialize(value: any): any { @@ -519,32 +584,17 @@ class TextCellEditor extends CellEditor { export class NumberCellEditor extends TextCellEditor { - protected serialize(): any { + protected getInput(): any { let value = this._input.value; if (value.trim() === '') { return null; } - let floatValue = parseFloat(value); + const floatValue = parseFloat(value); if (isNaN(floatValue)) { - this.validInput = false; - this._input.setCustomValidity('Input must be valid number'); - this._form.reportValidity(); throw new Error('Invalid input'); } - this._input.value = floatValue.toString(); - - if (this._validator) { - const result = this._validator.validate(this._cell, floatValue); - if (!result.valid) { - this.validInput = false; - this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); - this._form.reportValidity(); - throw new Error('Invalid input'); - } - } - return floatValue; } } @@ -577,7 +627,7 @@ class IntegerCellEditor extends NumberCellEditor { this._bindEvents(); } - protected serialize(): any { + protected getInput(): any { let value = this._input.value; if (value.trim() === '') { return null; @@ -585,24 +635,9 @@ class IntegerCellEditor extends NumberCellEditor { let intValue = parseInt(value); if (isNaN(intValue)) { - this.validInput = false; - this._input.setCustomValidity('Input must be valid number'); - this._form.reportValidity(); throw new Error('Invalid input'); } - this._input.value = intValue.toString(); - - if (this._validator) { - const result = this._validator.validate(this._cell, intValue); - if (!result.valid) { - this.validInput = false; - this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); - this._form.reportValidity(); - throw new Error('Invalid input'); - } - } - return intValue; } } @@ -682,20 +717,8 @@ class DateCellEditor extends CellEditor { } } - protected serialize(): any { - const value = this._input.value; - - if (this._validator) { - const result = this._validator.validate(this._cell, value); - if (!result.valid) { - this.validInput = false; - this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); - this._form.reportValidity(); - throw new Error('Invalid input'); - } - } - - return value; + protected getInput(): any { + return this._input.value; } protected deserialize(value: any): any { @@ -792,20 +815,8 @@ class BooleanCellEditor extends CellEditor { } } - protected serialize(): any { - const value = this._input.checked; - - if (this._validator) { - const result = this._validator.validate(this._cell, value); - if (!result.valid) { - this.validInput = false; - this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); - this._form.reportValidity(); - throw new Error('Invalid input'); - } - } - - return value; + protected getInput(): any { + return this._input.checked; } protected deserialize(value: any): any { @@ -902,20 +913,8 @@ class OptionCellEditor extends CellEditor { } } - protected serialize(): any { - const value = this._select.value; - - if (this._validator) { - const result = this._validator.validate(this._cell, value); - if (!result.valid) { - this.validInput = false; - this._select.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); - this._form.reportValidity(); - throw new Error('Invalid input'); - } - } - - return value; + protected getInput(): any { + return this._select.value; } protected deserialize(value: any): any { @@ -1026,20 +1025,8 @@ class DynamicOptionCellEditor extends CellEditor { } } - protected serialize(): any { - const value = this._input.value; - - if (this._validator) { - const result = this._validator.validate(this._cell, value); - if (!result.valid) { - this.validInput = false; - this._input.setCustomValidity(result.message || DEFAULT_INVALID_INPUT_MESSAGE); - this._form.reportValidity(); - throw new Error('Invalid input'); - } - } - - return value; + protected getInput(): any { + return this._input.value; } protected deserialize(value: any): any {