diff --git a/examples/pokemon.html b/examples/pokemon.html
new file mode 100644
index 00000000..36101bf0
--- /dev/null
+++ b/examples/pokemon.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+ Grid.js Pokemon Example
+
+
+
+
+
+ Top 20 Pokemon
+ A local Grid.js example using the built files from this repository.
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 931b1a1c..516b98bf 100644
--- a/package.json
+++ b/package.json
@@ -108,6 +108,7 @@
"build:grid": "microbundle build --raw --external none --tsconfig tsconfig.release.json",
"build:i18n": "microbundle build --raw --cwd l10n --tsconfig l10n/tsconfig.release.json",
"build:watch": "npm run build:grid -- -w",
+ "prebuild:plugins": "npm ci --prefix plugins/selection",
"build:plugins": "microbundle build --raw --cwd plugins/selection --tsconfig plugins/selection/tsconfig.release.json",
"postbuild": "node build/node-13-exports.js",
"prebuild:themes": "sass src/theme/mermaid/index.scss dist/theme/mermaid.css",
diff --git a/plugins/selection/package-lock.json b/plugins/selection/package-lock.json
index 26eb0ab8..9c4c75c0 100644
--- a/plugins/selection/package-lock.json
+++ b/plugins/selection/package-lock.json
@@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
+ "name": "selection",
"version": "4.0.0",
"license": "MIT",
"dependencies": {
@@ -12,54 +13,54 @@
}
},
"../..": {
- "version": "5.0.2",
+ "name": "gridjs",
+ "version": "6.2.0",
"license": "MIT",
"dependencies": {
- "preact": "^10.5.12",
- "tslib": "^2.0.1"
+ "preact": "^10.11.3"
},
"devDependencies": {
- "@types/enzyme": "^3.10.5",
- "@types/jest": "^26.0.0",
- "@types/jest-axe": "^3.2.2",
- "@types/node": "^15.3.0",
- "@typescript-eslint/eslint-plugin": "~4.28.0",
- "@typescript-eslint/parser": "~4.28.0",
- "autoprefixer": "^9.8.0",
- "axe-core": "^4.0.0",
+ "@types/enzyme": "^3.10.12",
+ "@types/jest": "^29.2.4",
+ "@types/jest-axe": "^3.5.5",
+ "@types/node": "^18.11.17",
+ "@typescript-eslint/eslint-plugin": "^5.47.0",
+ "@typescript-eslint/parser": "^5.47.0",
+ "autoprefixer": "^10.4.8",
+ "axe-core": "^4.4.2",
"check-export-map": "^1.1.1",
"cssnano": "^5.0.5",
- "cypress": "^7.4.0",
+ "cypress": "^8.1.0",
"cypress-visual-regression": "^1.5.7",
"enzyme": "^3.11.0",
- "enzyme-adapter-preact-pure": "^3.0.0",
- "eslint": "~7.29.0",
- "eslint-config-prettier": "^6.11.0",
- "eslint-plugin-jest": "~24.3.2",
+ "enzyme-adapter-preact-pure": "^4.0.1",
+ "eslint": "^8.18.0",
+ "eslint-config-prettier": "^8.5.0",
+ "eslint-plugin-jest": "~26.8.7",
"identity-obj-proxy": "^3.0.0",
- "jest": "~27.0.6",
- "jest-axe": "^5.0.1",
- "jest-extended": "^0.11.5",
- "jsdom": "^16.2.2",
+ "jest": "^29.3.1",
+ "jest-axe": "^6.0.0",
+ "jest-extended": "^3.2.0",
+ "jsdom": "^19.0.0",
"jsdom-global": "^3.0.2",
- "lerna-changelog": "^1.0.1",
- "microbundle": "^0.13.0",
- "node-sass": "^6.0.1",
- "node-sass-chokidar": "^1.5.0",
+ "lerna-changelog": "^2.1.0",
+ "microbundle": "^0.15.1",
"npm-run-all": "^4.1.5",
- "postcss": "^8.3.0",
- "postcss-cli": "^8.3.1",
- "postcss-nested": "^5.0.5",
- "postcss-scss": "^4.0.0",
- "postcss-sort-media-queries": "^3.10.11",
- "prettier": "~2.3.1",
+ "postcss": "^8.4.16",
+ "postcss-cli": "^9.1.0",
+ "postcss-nested": "^5.0.6",
+ "postcss-scss": "^4.0.4",
+ "postcss-sort-media-queries": "^4.1.0",
+ "prettier": "~2.7.1",
"rimraf": "~3.0.2",
- "source-map-loader": "^2.0.1",
+ "sass": "^1.54.5",
+ "source-map-loader": "^4.0.0",
"start-server-and-test": "^1.12.3",
- "ts-jest": "^27.0.3",
- "ts-loader": "^9.1.1",
- "tsutils": "~3.21.0",
- "typescript": "^4.2.4"
+ "ts-jest": "^29.0.3",
+ "ts-loader": "^9.4.2",
+ "tslib": "^2.4.1",
+ "tsutils": "^3.21.0",
+ "typescript": "^4.9.4"
}
},
"node_modules/gridjs": {
@@ -71,49 +72,48 @@
"gridjs": {
"version": "file:../..",
"requires": {
- "@types/enzyme": "^3.10.5",
- "@types/jest": "^26.0.0",
- "@types/jest-axe": "^3.2.2",
- "@types/node": "^15.3.0",
- "@typescript-eslint/eslint-plugin": "~4.28.0",
- "@typescript-eslint/parser": "~4.28.0",
- "autoprefixer": "^9.8.0",
- "axe-core": "^4.0.0",
+ "@types/enzyme": "^3.10.12",
+ "@types/jest": "^29.2.4",
+ "@types/jest-axe": "^3.5.5",
+ "@types/node": "^18.11.17",
+ "@typescript-eslint/eslint-plugin": "^5.47.0",
+ "@typescript-eslint/parser": "^5.47.0",
+ "autoprefixer": "^10.4.8",
+ "axe-core": "^4.4.2",
"check-export-map": "^1.1.1",
"cssnano": "^5.0.5",
- "cypress": "^7.4.0",
+ "cypress": "^8.1.0",
"cypress-visual-regression": "^1.5.7",
"enzyme": "^3.11.0",
- "enzyme-adapter-preact-pure": "^3.0.0",
- "eslint": "~7.29.0",
- "eslint-config-prettier": "^6.11.0",
- "eslint-plugin-jest": "~24.3.2",
+ "enzyme-adapter-preact-pure": "^4.0.1",
+ "eslint": "^8.18.0",
+ "eslint-config-prettier": "^8.5.0",
+ "eslint-plugin-jest": "~26.8.7",
"identity-obj-proxy": "^3.0.0",
- "jest": "~27.0.6",
- "jest-axe": "^5.0.1",
- "jest-extended": "^0.11.5",
- "jsdom": "^16.2.2",
+ "jest": "^29.3.1",
+ "jest-axe": "^6.0.0",
+ "jest-extended": "^3.2.0",
+ "jsdom": "^19.0.0",
"jsdom-global": "^3.0.2",
- "lerna-changelog": "^1.0.1",
- "microbundle": "^0.13.0",
- "node-sass": "^6.0.1",
- "node-sass-chokidar": "^1.5.0",
+ "lerna-changelog": "^2.1.0",
+ "microbundle": "^0.15.1",
"npm-run-all": "^4.1.5",
- "postcss": "^8.3.0",
- "postcss-cli": "^8.3.1",
- "postcss-nested": "^5.0.5",
- "postcss-scss": "^4.0.0",
- "postcss-sort-media-queries": "^3.10.11",
- "preact": "^10.5.12",
- "prettier": "~2.3.1",
+ "postcss": "^8.4.16",
+ "postcss-cli": "^9.1.0",
+ "postcss-nested": "^5.0.6",
+ "postcss-scss": "^4.0.4",
+ "postcss-sort-media-queries": "^4.1.0",
+ "preact": "^10.11.3",
+ "prettier": "~2.7.1",
"rimraf": "~3.0.2",
- "source-map-loader": "^2.0.1",
+ "sass": "^1.54.5",
+ "source-map-loader": "^4.0.0",
"start-server-and-test": "^1.12.3",
- "ts-jest": "^27.0.3",
- "ts-loader": "^9.1.1",
- "tslib": "^2.0.1",
- "tsutils": "~3.21.0",
- "typescript": "^4.2.4"
+ "ts-jest": "^29.0.3",
+ "ts-loader": "^9.4.2",
+ "tslib": "^2.4.1",
+ "tsutils": "^3.21.0",
+ "typescript": "^4.9.4"
}
}
}
diff --git a/src/config.ts b/src/config.ts
index 86bdbc6f..28f512dc 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -52,6 +52,8 @@ export interface Config {
fixedHeader: boolean;
/** Resizable columns? */
resizable: boolean;
+ /** Editable cells? */
+ editable: boolean;
columns: OneDArray;
search: SearchConfig | boolean;
language: Language;
@@ -132,6 +134,7 @@ export class Config {
height: 'auto',
processingThrottleMs: 100,
autoWidth: true,
+ editable: false,
style: {},
className: {},
};
diff --git a/src/theme/mermaid/td.scss b/src/theme/mermaid/td.scss
index 506f3c9d..b779090c 100644
--- a/src/theme/mermaid/td.scss
+++ b/src/theme/mermaid/td.scss
@@ -8,6 +8,11 @@ td.gridjs {
box-sizing: content-box;
}
+ &-td-editing {
+ border-color: $blue1;
+ box-shadow: inset 0 0 0 1px $blue1;
+ }
+
&-td:first-child {
border-left: none;
}
@@ -20,3 +25,22 @@ td.gridjs {
text-align: center;
}
}
+
+input.gridjs-td-editor-input {
+ appearance: none;
+ -webkit-appearance: none;
+ background: transparent;
+ border: 0;
+ box-shadow: none;
+ box-sizing: border-box;
+ color: inherit;
+ display: block;
+ font: inherit;
+ letter-spacing: inherit;
+ line-height: inherit;
+ margin: 0;
+ outline: none;
+ padding: 0;
+ text-align: inherit;
+ width: 100%;
+}
diff --git a/src/view/table/td.tsx b/src/view/table/td.tsx
index 7464c47c..502bb4e9 100644
--- a/src/view/table/td.tsx
+++ b/src/view/table/td.tsx
@@ -1,4 +1,5 @@
import { h, ComponentChild, JSX } from 'preact';
+import { useEffect, useRef, useState } from 'preact/hooks';
import Cell from '../../cell';
import { classJoin, className } from '../../util/className';
@@ -18,10 +19,40 @@ export function TD(
} & Omit, 'style'>,
) {
const config = useConfig();
+ const inputRef = useRef(null);
+ const skipBlurSaveRef = useRef(false);
+ const [editing, setEditing] = useState(false);
+ const [editValue, setEditValue] = useState('');
+ const [renderValue, setRenderValue] = useState(props.cell.data);
+
+ useEffect(() => {
+ setRenderValue(props.cell.data);
+ }, [props.cell.data]);
+
+ useEffect(() => {
+ if (editing && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, [editing]);
const content = (): ComponentChild => {
+ if (editing) {
+ return (
+ e.stopPropagation()}
+ onBlur={handleEditorBlur}
+ onInput={(e) => setEditValue(e.currentTarget.value)}
+ onKeyDown={handleEditorKeyDown}
+ />
+ );
+ }
+
if (props.column && typeof props.column.formatter === 'function') {
- return props.column.formatter(props.cell.data, props.row, props.column);
+ return props.column.formatter(renderValue, props.row, props.column);
}
if (props.column && props.column.plugin) {
@@ -37,7 +68,77 @@ export function TD(
);
}
- return props.cell.data;
+ return renderValue;
+ };
+
+ const isEditable = (): boolean => {
+ return (
+ config.editable &&
+ !props.messageCell &&
+ !(props.column && props.column.plugin)
+ );
+ };
+
+ const stringifyCellValue = (value: typeof props.cell.data): string => {
+ if (typeof value === 'string') return value;
+ if (typeof value === 'number' || typeof value === 'boolean') {
+ return String(value);
+ }
+
+ return '';
+ };
+
+ const castEditedValue = (value: string): typeof props.cell.data => {
+ if (typeof props.cell.data === 'number') {
+ const numberValue = Number(value);
+ return Number.isNaN(numberValue) ? value : numberValue;
+ }
+
+ if (typeof props.cell.data === 'boolean') {
+ return value.toLowerCase() === 'true';
+ }
+
+ return value;
+ };
+
+ const startEditing = (
+ e: JSX.TargetedMouseEvent,
+ ): void => {
+ if (!isEditable()) return;
+
+ e.stopPropagation();
+ setEditValue(stringifyCellValue(props.cell.data));
+ setEditing(true);
+ };
+
+ const saveEdit = (): void => {
+ const nextValue = castEditedValue(editValue);
+
+ props.cell.update(nextValue);
+ setRenderValue(nextValue);
+ setEditing(false);
+ };
+
+ const handleEditorBlur = (): void => {
+ if (skipBlurSaveRef.current) {
+ skipBlurSaveRef.current = false;
+ return;
+ }
+
+ saveEdit();
+ };
+
+ const handleEditorKeyDown = (
+ e: JSX.TargetedKeyboardEvent,
+ ): void => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ saveEdit();
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ skipBlurSaveRef.current = true;
+ setEditing(false);
+ }
};
const handleClick = (
@@ -73,6 +174,7 @@ export function TD(
data-column-id={props.column && props.column.id}
className={classJoin(
className('td'),
+ editing ? className('td', 'editing') : undefined,
props.className,
config.className.td,
)}
@@ -81,6 +183,7 @@ export function TD(
...config.style.td,
}}
onClick={handleClick}
+ onDblClick={startEditing}
{...getCustomAttributes(props.column)}
>
{content()}
diff --git a/tests/jest/config.test.ts b/tests/jest/config.test.ts
index 7c60e845..5255d454 100644
--- a/tests/jest/config.test.ts
+++ b/tests/jest/config.test.ts
@@ -15,6 +15,7 @@ describe('Config', () => {
const config = new Config();
config.assign({});
expect(config.width).toEqual('100%');
+ expect(config.editable).toBe(false);
});
it('assign should set the correct default', () => {
diff --git a/tests/jest/grid.test.ts b/tests/jest/grid.test.ts
index bc3d542d..f76ff441 100644
--- a/tests/jest/grid.test.ts
+++ b/tests/jest/grid.test.ts
@@ -56,11 +56,13 @@ describe('Grid class', () => {
it('should set the config correctly', () => {
const config = {
data: [[1, 2, 3]],
+ editable: true,
};
const grid = new Grid(config).render(document.createElement('div'));
expect(grid.config.data).toStrictEqual(config.data);
+ expect(grid.config.editable).toBe(true);
});
it('should update the config correctly', () => {
diff --git a/tests/jest/view/table/td.test.tsx b/tests/jest/view/table/td.test.tsx
index db8aa0c4..64c4c2bb 100644
--- a/tests/jest/view/table/td.test.tsx
+++ b/tests/jest/view/table/td.test.tsx
@@ -38,4 +38,31 @@ describe('TD component', () => {
expect(cells.length).toEqual(1);
expect(onClick).toHaveBeenCalledTimes(1);
});
+
+ it('should not edit on double click by default', () => {
+ const cell = new Cell('boo');
+ const td = mount(
+
+ |
+ ,
+ );
+
+ td.find('td').simulate('dblclick');
+
+ expect(td.find('input.gridjs-td-editor-input')).toHaveLength(0);
+ });
+
+ it('should edit on double click when editable is enabled', () => {
+ config.editable = true;
+ const cell = new Cell('boo');
+ const td = mount(
+
+ |
+ ,
+ );
+
+ td.find('td').simulate('dblclick');
+
+ expect(td.find('input.gridjs-td-editor-input')).toHaveLength(1);
+ });
});