Skip to content
This repository has been archived by the owner on Nov 6, 2019. It is now read-only.

[WIP] Cell Editing #444

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9e012de
cell editing initial commit
mbektas Oct 10, 2019
3c9a6d6
validator support, refactor
mbektas Oct 10, 2019
9ffc533
viewport occluder, input validator, refactoring
mbektas Oct 11, 2019
aa1aa91
BooleanCellEditor, IntegerCellEditor, refactoring
mbektas Oct 12, 2019
45d7847
styling improvements
mbektas Oct 12, 2019
2d3a637
move cursor within selection rectangles and in reverse direction as well
mbektas Oct 14, 2019
50d807e
default validator implementations
mbektas Oct 14, 2019
f22d006
fix NaN checks
mbektas Oct 15, 2019
263cb41
use signals instead of promise
mbektas Oct 15, 2019
9e6ad27
refactoring
mbektas Oct 15, 2019
0559b55
support deleting cell values using keyboard
mbektas Oct 16, 2019
cc6ba2f
refactoring
mbektas Oct 16, 2019
ac1bd3a
fix styling issues with small cells
mbektas Oct 17, 2019
b348051
fix blur related isses
mbektas Oct 17, 2019
23db015
left & right cursor move implementation
mbektas Oct 17, 2019
026bd04
remove metadata from CellConfig
mbektas Oct 17, 2019
089ab89
ability to cancel an edit, emit oncancel signal
mbektas Oct 17, 2019
9bd6668
minLength, maxLength and pattern constraint support for text input
mbektas Oct 18, 2019
254b81f
update validity in exceptions
mbektas Oct 18, 2019
2225c81
metadata format property support
mbektas Oct 18, 2019
e17c76d
dynamic option cell editor
mbektas Oct 18, 2019
f2532d7
grid editable flag, refactoring
mbektas Oct 20, 2019
46eb12d
allow editor overrides by data type or metadata
mbektas Oct 21, 2019
4edc5f4
fix cursor move within selections
mbektas Oct 21, 2019
50897dc
editable grid example
mbektas Oct 21, 2019
63bf6d3
move controller to separate file
mbektas Oct 21, 2019
b4ec7b4
refactoring
mbektas Oct 21, 2019
4a082e6
remove unused interfaces
mbektas Oct 21, 2019
d4a8b61
fix onCancel bind, refactoring
mbektas Oct 21, 2019
9345e9c
set active editor in controller, cancel editing on grid resize
mbektas Oct 22, 2019
f4a92a7
check if disposed before canceling
mbektas Oct 22, 2019
8f3e90c
editor resolver as an override option
mbektas Oct 22, 2019
dcc4ae2
fix checkbox focus loss problem in some browsers
mbektas Oct 24, 2019
67141e8
use ES2015.Collection
mbektas Oct 25, 2019
2390569
improvements to editable grid example
mbektas Oct 25, 2019
dce4dfe
fix for blur problem in invalid state, validation refactoring
mbektas Dec 6, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
340 changes: 339 additions & 1 deletion examples/example-datagrid/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import 'es6-promise/auto'; // polyfill Promise on IE

import {
BasicKeyHandler, BasicMouseHandler, BasicSelectionModel, CellRenderer,
DataGrid, DataModel, JSONModel, TextRenderer
DataGrid, DataModel, JSONModel, TextRenderer, MutableDataModel, CellEditor, ICellEditor
} from '@phosphor/datagrid';

import {
DockPanel, StackedPanel, Widget
} from '@phosphor/widgets';

import {
getKeyboardLayout
} from '@phosphor/keyboard';

import '../style/index.css';


Expand Down Expand Up @@ -158,6 +162,162 @@ 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';
textarea.style.outline = 'none';
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) => {
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 (key === 'Escape') {
this.cancel();
}
});

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);
this._textarea.focus();
}

protected getInput(): any {
return JSON.parse(this._textarea.value);
}

deserialize(value: any): any {
return JSON.stringify(value);
}

dispose(): void {
if (this.isDisposed) {
return;
}

super.dispose();

document.body.removeChild(this._textarea);
}

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<string> = ({ value }) => {
if (value <= 1 / 3) {
Expand Down Expand Up @@ -202,6 +362,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,
Expand Down Expand Up @@ -287,11 +448,37 @@ 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 columnIdentifier = {'name': 'Corp. Data'};
grid6.editorController!.setEditor(columnIdentifier, (config: CellEditor.CellConfig): ICellEditor => {
return new 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';
Expand All @@ -301,6 +488,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(); };

Expand Down Expand Up @@ -1011,4 +1201,152 @@ namespace Data {
}
}

export
const editable_test_data = {
"data": [
{
"index": 0,
"Name": "Chevrolet",
"Contact": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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"
}
}

}
Loading