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); + }); });