From 3d8d08c235a716af90ca1bdfef7903d97fd7d9eb Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Fri, 6 Sep 2024 12:21:23 +0900 Subject: [PATCH] Add TreeColumn --- .../.vscode/settings.json | 2 +- ..._content_svg_production_ic_add_48px_svg.js | 13 + ...duction_ic_keyboard_arrow_down_48px_svg.js | 13 + ...duction_ic_keyboard_arrow_left_48px_svg.js | 13 + ...uction_ic_keyboard_arrow_right_48px_svg.js | 13 + ...roduction_ic_keyboard_arrow_up_48px_svg.js | 13 + ...__image_svg_production_ic_edit_48px_svg.js | 13 + ...g_production_ic_arrow_downward_48px_svg.js | 13 + ...svg_production_ic_arrow_upward_48px_svg.js | 13 + ...svg_production_ic_chevron_left_48px_svg.js | 13 + ...vg_production_ic_chevron_right_48px_svg.js | 13 + ..._svg_production_ic_expand_less_48px_svg.js | 13 + ..._svg_production_ic_expand_more_48px_svg.js | 13 + .../test-fixtures/expect/test-icon.html | 2 +- .../test-fixtures/expect/test-icons.html | 2 +- .../test-fixtures/inputs/package-lock.json | 12 +- .../tests/lib/font-svg-to-icons-load.js | 6 +- .../tests/lib/svg-to-icon-load.js | 20 +- .../tests/test-utils.js | 4 +- .../cheetah-grid/src/js/GridCanvasHelper.ts | 102 ++-- .../src/js/columns/action/Action.ts | 61 ++- .../src/js/columns/action/ButtonAction.ts | 18 +- .../src/js/columns/action/actionBind.ts | 94 +++- packages/cheetah-grid/src/js/columns/style.ts | 4 + .../src/js/columns/style/TreeStyle.ts | 65 +++ packages/cheetah-grid/src/js/columns/type.ts | 4 + .../src/js/columns/type/BranchGraphColumn.ts | 47 +- .../src/js/columns/type/IconColumn.ts | 17 +- .../js/columns/type/MultilineTextColumn.ts | 4 +- .../src/js/columns/type/NumberColumn.ts | 20 +- .../columns/type/PercentCompleteBarColumn.ts | 9 + .../src/js/columns/type/TreeColumn.ts | 515 ++++++++++++++++++ packages/cheetah-grid/src/js/core/DrawGrid.ts | 76 ++- .../src/js/header/type/SortHeader.ts | 4 +- packages/cheetah-grid/src/js/icons.ts | 64 +++ .../src/js/internal/symbolManager.ts | 3 + packages/cheetah-grid/src/js/themes/theme.ts | 58 ++ .../src/js/ts-types-internal/grid-engine.ts | 23 + .../src/js/ts-types/column/action.ts | 23 +- .../src/js/ts-types/column/style.ts | 9 + .../src/js/ts-types/column/type.ts | 80 ++- .../cheetah-grid/src/js/ts-types/define.ts | 5 + .../cheetah-grid/src/js/ts-types/events.ts | 5 +- .../src/js/ts-types/grid-engine.ts | 2 +- .../cheetah-grid/src/js/ts-types/plugin.ts | 15 +- .../src/test/ListGrid_sample_for_tree.html | 28 + .../src/test/ListGrid_sample_for_tree.js | 174 ++++++ packages/docs/api/js/column_actions/Action.md | 30 + .../api/js/column_actions/ButtonAction.md | 18 + .../docs/api/js/column_actions/CheckEditor.md | 20 +- .../docs/api/js/column_actions/Classes.md | 4 + .../js/column_actions/InlineInputEditor.md | 23 +- .../api/js/column_actions/InlineMenuEditor.md | 22 + .../docs/api/js/column_actions/RadioEditor.md | 22 +- .../column_actions/SmallDialogInputEditor.md | 18 +- .../api/js/column_types/BranchGraphColumn.md | 65 ++- .../docs/api/js/column_types/TreeColumn.md | 218 ++++++++ 57 files changed, 2003 insertions(+), 170 deletions(-) create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__content_svg_production_ic_add_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_down_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_left_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_right_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_up_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__image_svg_production_ic_edit_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_arrow_downward_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_arrow_upward_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_chevron_left_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_chevron_right_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_expand_less_48px_svg.js create mode 100644 packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_expand_more_48px_svg.js create mode 100644 packages/cheetah-grid/src/js/columns/style/TreeStyle.ts create mode 100644 packages/cheetah-grid/src/js/columns/type/TreeColumn.ts create mode 100644 packages/cheetah-grid/src/test/ListGrid_sample_for_tree.html create mode 100644 packages/cheetah-grid/src/test/ListGrid_sample_for_tree.js create mode 100644 packages/docs/api/js/column_actions/Action.md create mode 100644 packages/docs/api/js/column_types/TreeColumn.md diff --git a/packages/cheetah-grid-icon-svg-loader/.vscode/settings.json b/packages/cheetah-grid-icon-svg-loader/.vscode/settings.json index e8c5bbd63..4c9e2db05 100644 --- a/packages/cheetah-grid-icon-svg-loader/.vscode/settings.json +++ b/packages/cheetah-grid-icon-svg-loader/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.codeActionsOnSaveTimeout": 10000, } \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__content_svg_production_ic_add_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__content_svg_production_ic_add_48px_svg.js new file mode 100644 index 000000000..7c6415d5d --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__content_svg_production_ic_add_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_add_48px": { + "d": "M38 26H26v12h-4V26H10v-4h12V10h4v12h12v4z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__content_svg_production_ic_add_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_down_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_down_48px_svg.js new file mode 100644 index 000000000..528d1d73a --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_down_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_keyboard_arrow_down_48px": { + "d": "M14.83 16.42L24 25.59l9.17-9.17L36 19.25l-12 12-12-12z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__hardware_svg_production_ic_keyboard_arrow_down_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_left_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_left_48px_svg.js new file mode 100644 index 000000000..6207a643b --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_left_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_keyboard_arrow_left_48px": { + "d": "M30.83 32.67l-9.17-9.17 9.17-9.17L28 11.5l-12 12 12 12z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__hardware_svg_production_ic_keyboard_arrow_left_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_right_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_right_48px_svg.js new file mode 100644 index 000000000..806e51d75 --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_right_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_keyboard_arrow_right_48px": { + "d": "M17.17 32.92l9.17-9.17-9.17-9.17L20 11.75l12 12-12 12z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__hardware_svg_production_ic_keyboard_arrow_right_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_up_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_up_48px_svg.js new file mode 100644 index 000000000..030ccfdad --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__hardware_svg_production_ic_keyboard_arrow_up_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_keyboard_arrow_up_48px": { + "d": "M14.83 30.83L24 21.66l9.17 9.17L36 28 24 16 12 28z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__hardware_svg_production_ic_keyboard_arrow_up_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__image_svg_production_ic_edit_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__image_svg_production_ic_edit_48px_svg.js new file mode 100644 index 000000000..6361c1cad --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__image_svg_production_ic_edit_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_edit_48px": { + "d": "M6 34.5V42h7.5l22.13-22.13-7.5-7.5L6 34.5zm35.41-20.41c.78-.78.78-2.05 0-2.83l-4.67-4.67c-.78-.78-2.05-.78-2.83 0l-3.66 3.66 7.5 7.5 3.66-3.66z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__image_svg_production_ic_edit_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_arrow_downward_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_arrow_downward_48px_svg.js new file mode 100644 index 000000000..f4bef285f --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_arrow_downward_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_arrow_downward_48px": { + "d": "M40 24l-2.82-2.82L26 32.34V8h-4v24.34L10.84 21.16 8 24l16 16 16-16z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__navigation_svg_production_ic_arrow_downward_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_arrow_upward_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_arrow_upward_48px_svg.js new file mode 100644 index 000000000..66821ef57 --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_arrow_upward_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_arrow_upward_48px": { + "d": "M8 24l2.83 2.83L22 15.66V40h4V15.66l11.17 11.17L40 24 24 8 8 24z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__navigation_svg_production_ic_arrow_upward_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_chevron_left_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_chevron_left_48px_svg.js new file mode 100644 index 000000000..6fedd08c8 --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_chevron_left_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_chevron_left_48px": { + "d": "M30.83 14.83L28 12 16 24l12 12 2.83-2.83L21.66 24z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__navigation_svg_production_ic_chevron_left_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_chevron_right_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_chevron_right_48px_svg.js new file mode 100644 index 000000000..db7194ec1 --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_chevron_right_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_chevron_right_48px": { + "d": "M20 12l-2.83 2.83L26.34 24l-9.17 9.17L20 36l12-12z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__navigation_svg_production_ic_chevron_right_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_expand_less_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_expand_less_48px_svg.js new file mode 100644 index 000000000..44bae1f51 --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_expand_less_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_expand_less_48px": { + "d": "M24 16L12 28l2.83 2.83L24 21.66l9.17 9.17L36 28z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__navigation_svg_production_ic_expand_less_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_expand_more_48px_svg.js b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_expand_more_48px_svg.js new file mode 100644 index 000000000..5eb642cce --- /dev/null +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/material-design-icons__navigation_svg_production_ic_expand_more_48px_svg.js @@ -0,0 +1,13 @@ +var obj={ + "ic_expand_more_48px": { + "d": "M33.17 17.17L24 26.34l-9.17-9.17L12 20l12 12 12-12z", + "width": 48, + "height": 48 + } +} + if (typeof window !== 'undefined') { + window['material-design-icons__navigation_svg_production_ic_expand_more_48px_svg']=obj + } else { + module.exports=obj + } + \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/test-icon.html b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/test-icon.html index 1ff228e4e..76e1e3fe4 100644 --- a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/test-icon.html +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/test-icon.html @@ -81,7 +81,7 @@

ICONS

headerRowHeight: 24, }); const records = []; - const {icons} = cheetahGrid; + const icons = cheetahGrid.getIcons(); Object.keys(icons).forEach((k, i) => { records.push({ no: i + 1, diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/test-icons.html b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/test-icons.html index d2ff8a0a9..e3d9256bf 100644 --- a/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/test-icons.html +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/expect/test-icons.html @@ -80,7 +80,7 @@

ICONS

headerRowHeight: 24, }); const records = []; - const {icons} = cheetahGrid; + const icons = cheetahGrid.getIcons(); Object.keys(icons).forEach((k, i) => { records.push({ no: i + 1, diff --git a/packages/cheetah-grid-icon-svg-loader/test-fixtures/inputs/package-lock.json b/packages/cheetah-grid-icon-svg-loader/test-fixtures/inputs/package-lock.json index 80ff234da..88a6bfbf3 100644 --- a/packages/cheetah-grid-icon-svg-loader/test-fixtures/inputs/package-lock.json +++ b/packages/cheetah-grid-icon-svg-loader/test-fixtures/inputs/package-lock.json @@ -1,9 +1,15 @@ { "name": "cheetah-grid-icon-svg-loader-test-inputs", + "lockfileVersion": 3, "requires": true, - "lockfileVersion": 1, - "dependencies": { - "material-design-icons": { + "packages": { + "": { + "name": "cheetah-grid-icon-svg-loader-test-inputs", + "devDependencies": { + "material-design-icons": "^3.0.1" + } + }, + "node_modules/material-design-icons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz", "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=", diff --git a/packages/cheetah-grid-icon-svg-loader/tests/lib/font-svg-to-icons-load.js b/packages/cheetah-grid-icon-svg-loader/tests/lib/font-svg-to-icons-load.js index 915b53e1c..ff02b068c 100644 --- a/packages/cheetah-grid-icon-svg-loader/tests/lib/font-svg-to-icons-load.js +++ b/packages/cheetah-grid-icon-svg-loader/tests/lib/font-svg-to-icons-load.js @@ -23,9 +23,8 @@ describe('font svg load', () => { const result = eval(resultModule);//eslint-disable-line no-eval const expect = loadExpect(name); - assert.deepStrictEqual(result, expect); - saveExpect(name, result); + assert.deepStrictEqual(result, expect); }); it('should succeed loading module for fontawesome-webfont.svg', () => { @@ -34,8 +33,7 @@ describe('font svg load', () => { const result = eval(resultModule);//eslint-disable-line no-eval const expect = loadExpect(name); - assert.deepStrictEqual(result, expect); - saveExpect(name, result); + assert.deepStrictEqual(result, expect); }); }); \ No newline at end of file diff --git a/packages/cheetah-grid-icon-svg-loader/tests/lib/svg-to-icon-load.js b/packages/cheetah-grid-icon-svg-loader/tests/lib/svg-to-icon-load.js index 1d42f5d85..15b3d5633 100644 --- a/packages/cheetah-grid-icon-svg-loader/tests/lib/svg-to-icon-load.js +++ b/packages/cheetah-grid-icon-svg-loader/tests/lib/svg-to-icon-load.js @@ -12,6 +12,21 @@ const MDI_ROOT = path.join(__dirname, '../../test-fixtures/inputs/node_modules/m const TEST_TARGETS = [ './action/svg/production', './av/svg/design/ic_playlist_play_48px.svg', + './toggle/svg/production/ic_star_24px.svg', + './toggle/svg/production/ic_star_border_24px.svg', + './toggle/svg/production/ic_star_half_24px.svg', + './content/svg/production/ic_add_48px.svg', + './image/svg/production/ic_edit_48px.svg', + './navigation/svg/production/ic_arrow_downward_48px.svg', + './navigation/svg/production/ic_arrow_upward_48px.svg', + './navigation/svg/production/ic_chevron_left_48px.svg', + './navigation/svg/production/ic_chevron_right_48px.svg', + './navigation/svg/production/ic_expand_less_48px.svg', + './navigation/svg/production/ic_expand_more_48px.svg', + './hardware/svg/production/ic_keyboard_arrow_up_48px.svg', + './hardware/svg/production/ic_keyboard_arrow_down_48px.svg', + './hardware/svg/production/ic_keyboard_arrow_left_48px.svg', + './hardware/svg/production/ic_keyboard_arrow_right_48px.svg', ]; const walkTree = (rootDir, callback) => new Promise((resolve) => { @@ -81,11 +96,10 @@ describe('svg load', () => { return getAllSvgPaths(dirRoot).then((svgs) => { const result = getAllModule(svgs); const name = `material-design-icons${target.replace(/\\|\/|\./g, '_')}`; - // saveExpect(name, result); - const expect = loadExpect(name); - assert.deepStrictEqual(result, expect); + const expect = loadExpect(name); saveExpect(name, result); + assert.deepStrictEqual(result, expect); }); }); } diff --git a/packages/cheetah-grid-icon-svg-loader/tests/test-utils.js b/packages/cheetah-grid-icon-svg-loader/tests/test-utils.js index 4691f97df..772cdc3be 100644 --- a/packages/cheetah-grid-icon-svg-loader/tests/test-utils.js +++ b/packages/cheetah-grid-icon-svg-loader/tests/test-utils.js @@ -11,7 +11,9 @@ function resolve(...names) { module.exports = { loadExpect(name) { - return require(path.join(FIXTURES_ROOT, `./expect/${name}`)); + const file = path.join(FIXTURES_ROOT, `./expect/${name}`); + if (!fs.existsSync(file)) { return {}; } + return require(file); }, saveExpect(name, obj) { const text = `var obj=${JSON.stringify(sortProps(obj), null, ' ')} diff --git a/packages/cheetah-grid/src/js/GridCanvasHelper.ts b/packages/cheetah-grid/src/js/GridCanvasHelper.ts index f0739d8ac..18a950e70 100644 --- a/packages/cheetah-grid/src/js/GridCanvasHelper.ts +++ b/packages/cheetah-grid/src/js/GridCanvasHelper.ts @@ -16,6 +16,8 @@ import type { RequiredThemeDefine, StylePropertyFunctionArg, TextOverflow, + TreeBranchIconStyle, + TreeLineStyle, } from "./ts-types"; import type { Inline, InlineDrawOption } from "./element/Inline"; import { calcStartPosition, getFontSize } from "./internal/canvases"; @@ -98,30 +100,30 @@ function getFont( context, }); } -function getThemeColor< +function getThemeValue< R, T extends ColorPropertyDefine | ColorsPropertyDefine | string | number >(grid: ListGridAPI, ...names: string[]): T { - const gridThemeColor = getChainSafe(grid.theme, ...names); - if (gridThemeColor == null) { + const gridThemeValue = getChainSafe(grid.theme, ...names); + if (gridThemeValue == null) { // use default theme return getChainSafe(themes.getDefault(), ...names); } - if (typeof gridThemeColor !== "function") { - return gridThemeColor; + if (typeof gridThemeValue !== "function") { + return gridThemeValue; } - let defaultThemeColor: ColorDef; + let defaultThemeValuer: unknown; // eslint-disable-next-line @typescript-eslint/no-explicit-any return ((args: StylePropertyFunctionArg): any => { - const color = gridThemeColor(args); - if (color != null) { + const value = gridThemeValue(args); + if (value != null) { // use grid theme - return color; + return value; } // use default theme - defaultThemeColor = - defaultThemeColor || getChainSafe(themes.getDefault(), ...names); - return getOrApply(defaultThemeColor, args); + defaultThemeValuer = + defaultThemeValuer || getChainSafe(themes.getDefault(), ...names); + return getOrApply(defaultThemeValuer, args); // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as any; } @@ -864,52 +866,53 @@ class ThemeResolver implements RequiredThemeDefine { private _checkbox: RequiredThemeDefine["checkbox"] | null = null; private _radioButton: RequiredThemeDefine["radioButton"] | null = null; private _button: RequiredThemeDefine["button"] | null = null; + private _tree: RequiredThemeDefine["tree"] | null = null; private _header: RequiredThemeDefine["header"] | null = null; private _messages: RequiredThemeDefine["messages"] | null = null; private _indicators: RequiredThemeDefine["indicators"] | null = null; constructor(grid: ListGridAPI) { this._grid = grid; } - getThemeColor< + getThemeValue< T extends ColorPropertyDefine | ColorsPropertyDefine | FontPropertyDefine >(...name: string[]): T { - return getThemeColor(this._grid, ...name); + return getThemeValue(this._grid, ...name); } get font(): string { - return getThemeColor(this._grid, "font"); + return getThemeValue(this._grid, "font"); } get underlayBackgroundColor(): string { - return getThemeColor(this._grid, "underlayBackgroundColor"); + return getThemeValue(this._grid, "underlayBackgroundColor"); } // color get color(): ColorPropertyDefine { - return getThemeColor(this._grid, "color"); + return getThemeValue(this._grid, "color"); } get frozenRowsColor(): ColorPropertyDefine { - return getThemeColor(this._grid, "frozenRowsColor"); + return getThemeValue(this._grid, "frozenRowsColor"); } // background get defaultBgColor(): ColorPropertyDefine { - return getThemeColor(this._grid, "defaultBgColor"); + return getThemeValue(this._grid, "defaultBgColor"); } get frozenRowsBgColor(): ColorPropertyDefine { - return getThemeColor(this._grid, "frozenRowsBgColor"); + return getThemeValue(this._grid, "frozenRowsBgColor"); } get selectionBgColor(): ColorPropertyDefine { - return getThemeColor(this._grid, "selectionBgColor"); + return getThemeValue(this._grid, "selectionBgColor"); } get highlightBgColor(): ColorPropertyDefine { - return getThemeColor(this._grid, "highlightBgColor"); + return getThemeValue(this._grid, "highlightBgColor"); } // border get borderColor(): ColorsPropertyDefine { - return getThemeColor(this._grid, "borderColor"); + return getThemeValue(this._grid, "borderColor"); } get frozenRowsBorderColor(): ColorsPropertyDefine { - return getThemeColor(this._grid, "frozenRowsBorderColor"); + return getThemeValue(this._grid, "frozenRowsBorderColor"); } get highlightBorderColor(): ColorsPropertyDefine { - return getThemeColor(this._grid, "highlightBorderColor"); + return getThemeValue(this._grid, "highlightBorderColor"); } get checkbox(): RequiredThemeDefine["checkbox"] { const grid = this._grid; @@ -929,7 +932,7 @@ class ThemeResolver implements RequiredThemeDefine { ); function getCheckboxProp(prop: string): ColorPropertyDefine { - return getThemeColor(grid, "checkbox", prop); + return getThemeValue(grid, "checkbox", prop); } } get radioButton(): RequiredThemeDefine["radioButton"] { @@ -956,7 +959,7 @@ class ThemeResolver implements RequiredThemeDefine { ); function getRadioButtonProp(prop: string): ColorPropertyDefine { - return getThemeColor(grid, "radioButton", prop); + return getThemeValue(grid, "radioButton", prop); } } get button(): RequiredThemeDefine["button"] { @@ -974,7 +977,40 @@ class ThemeResolver implements RequiredThemeDefine { ); function getButtonProp(prop: string): ColorPropertyDefine { - return getThemeColor(grid, "button", prop); + return getThemeValue(grid, "button", prop); + } + } + get tree(): RequiredThemeDefine["tree"] { + const grid = this._grid; + return ( + this._tree || + (this._tree = { + get lineStyle(): TreeLineStyle { + return getTreeProp("lineStyle"); + }, + get lineColor(): ColorPropertyDefine { + return getTreeProp("lineColor"); + }, + get lineWidth(): number { + return getTreeProp("lineWidth"); + }, + get branchIcon(): TreeBranchIconStyle { + return getTreeProp("branchIcon"); + }, + get openedBranchIcon(): TreeBranchIconStyle { + return getTreeProp("openedBranchIcon"); + }, + }) + ); + + function getTreeProp< + T extends + | ColorPropertyDefine + | number + | TreeLineStyle + | TreeBranchIconStyle + >(prop: string): T { + return getThemeValue(grid, "tree", prop); } } get header(): RequiredThemeDefine["header"] { @@ -983,7 +1019,7 @@ class ThemeResolver implements RequiredThemeDefine { this._header || (this._header = { get sortArrowColor(): ColorPropertyDefine { - return getThemeColor(grid, "header", "sortArrowColor"); + return getThemeValue(grid, "header", "sortArrowColor"); }, }) ); @@ -1014,7 +1050,7 @@ class ThemeResolver implements RequiredThemeDefine { function getMessageProp( prop: string ): T { - return getThemeColor(grid, "messages", prop); + return getThemeValue(grid, "messages", prop); } } get indicators(): RequiredThemeDefine["indicators"] { @@ -1052,7 +1088,7 @@ class ThemeResolver implements RequiredThemeDefine { function getIndicatorsProp( prop: string ): T { - return getThemeColor(grid, "indicators", prop); + return getThemeValue(grid, "indicators", prop); } } } @@ -1277,7 +1313,7 @@ export class GridCanvasHelper implements GridCanvasHelperAPI { }); } multilineText( - multilines: string[], + lines: string[], context: CellContext, { padding, @@ -1332,7 +1368,7 @@ export class GridCanvasHelper implements GridCanvasHelperAPI { } const calculator = this.createCalculator(context, font); lineHeight = calculator.calcHeight(lineHeight); - _multiInlineRect(this._grid, ctx, multilines, rect, col, row, { + _multiInlineRect(this._grid, ctx, lines, rect, col, row, { offset, color, textAlign, diff --git a/packages/cheetah-grid/src/js/columns/action/Action.ts b/packages/cheetah-grid/src/js/columns/action/Action.ts index f457ad501..be8f18406 100644 --- a/packages/cheetah-grid/src/js/columns/action/Action.ts +++ b/packages/cheetah-grid/src/js/columns/action/Action.ts @@ -1,4 +1,6 @@ import type { + AbstractActionOption, + ActionAreaPredicate, ActionListener, ActionOption, CellAddress, @@ -12,9 +14,9 @@ import type { GridInternal } from "../../ts-types-internal"; import { extend } from "../../internal/utils"; import { isDisabledRecord } from "./action-utils"; -export class Action extends BaseAction { +export abstract class AbstractAction extends BaseAction { private _action: ActionListener; - constructor(option: ActionOption = {}) { + constructor(option: AbstractActionOption = {}) { super(option); this._action = option.action || ((): void => {}); } @@ -27,12 +29,10 @@ export class Action extends BaseAction { set action(action: ActionListener) { this._action = action; } - clone(): Action { - return new Action(this); - } - getState(_grid: GridInternal): { mouseActiveCell?: CellAddress } { - return {}; - } + abstract get area(): ActionAreaPredicate | undefined; + abstract set area(_area: ActionAreaPredicate | undefined); + abstract clone(): AbstractAction; + abstract getState(_grid: GridInternal): { mouseActiveCell?: CellAddress }; bindGridEvent( grid: ListGridAPI, cellId: LayoutObjectId @@ -66,6 +66,31 @@ export class Action extends BaseAction { const range = grid.getCellRange(e.col, e.row); grid.invalidateCellRange(range); }, + area: (e) => { + if (!this.area) { + return true; + } + const { event } = e; + const clientX = event.clientX || event.pageX + window.scrollX; + const clientY = event.clientY || event.pageY + window.scrollY; + const canvasRect = grid.canvas.getBoundingClientRect(); + const abstractX = clientX - canvasRect.left; + const abstractY = clientY - canvasRect.top; + const rect = grid.getCellRect(e.col, e.row); + return this.area({ + col: e.col, + row: e.row, + grid, + pointInCell: { + x: abstractX - rect.left, + y: abstractY - rect.top, + }, + pointInDrawingCanvas: { + x: abstractX - grid.scrollLeft, + y: abstractY - grid.scrollTop, + }, + }); + }, }), ...bindCellKeyAction(grid, cellId, { action, @@ -79,3 +104,23 @@ export class Action extends BaseAction { // noop } } + +export class Action extends AbstractAction { + private _area?: ActionAreaPredicate; + constructor(option: ActionOption = {}) { + super(option); + this._area = option.area; + } + get area(): ActionAreaPredicate | undefined { + return this._area; + } + set area(area: ActionAreaPredicate | undefined) { + this._area = area; + } + clone(): Action { + return new Action(this); + } + getState(_grid: GridInternal): { mouseActiveCell?: CellAddress } { + return {}; + } +} diff --git a/packages/cheetah-grid/src/js/columns/action/ButtonAction.ts b/packages/cheetah-grid/src/js/columns/action/ButtonAction.ts index f9c08f60a..cc8e1f522 100644 --- a/packages/cheetah-grid/src/js/columns/action/ButtonAction.ts +++ b/packages/cheetah-grid/src/js/columns/action/ButtonAction.ts @@ -1,9 +1,23 @@ +import type { ActionAreaPredicate, ButtonActionOption } from "../../ts-types"; import type { ButtonColumnState, GridInternal } from "../../ts-types-internal"; -import { Action } from "./Action"; +import { AbstractAction } from "./Action"; import { getButtonColumnStateId } from "../../internal/symbolManager"; import { obj } from "../../internal/utils"; + const BUTTON_COLUMN_STATE_ID = getButtonColumnStateId(); -export class ButtonAction extends Action { +export class ButtonAction extends AbstractAction { + constructor(option: ButtonActionOption = {}) { + super(option); + } + get area(): ActionAreaPredicate | undefined { + return undefined; + } + set area(_area: ActionAreaPredicate | undefined) { + // noop + } + clone(): ButtonAction { + return new ButtonAction(this); + } getState(grid: GridInternal): ButtonColumnState { let state = grid[BUTTON_COLUMN_STATE_ID]; if (!state) { diff --git a/packages/cheetah-grid/src/js/columns/action/actionBind.ts b/packages/cheetah-grid/src/js/columns/action/actionBind.ts index 1530530e4..fa0f1ea2c 100644 --- a/packages/cheetah-grid/src/js/columns/action/actionBind.ts +++ b/packages/cheetah-grid/src/js/columns/action/actionBind.ts @@ -3,6 +3,8 @@ import type { EventListenerId, LayoutObjectId, ListGridAPI, + MouseCellEvent, + MousePointerCellEvent, } from "../../ts-types"; import { event, isPromise } from "../../internal/utils"; import { DG_EVENT_TYPE } from "../../core/DG_EVENT_TYPE"; @@ -16,16 +18,46 @@ export function bindCellClickAction( action, mouseOver, mouseOut, + area, }: { action: (cell: CellAddress) => void; - mouseOver: (cell: CellAddress) => boolean; - mouseOut: (cell: CellAddress) => void; + mouseOver?: (cell: CellAddress) => boolean; + mouseOut?: (cell: CellAddress) => void; + area?: (event: MouseCellEvent | MousePointerCellEvent) => boolean; } ): EventListenerId[] { function isTarget(col: number, row: number): boolean { return grid.getLayoutCellId(col, row) === cellId; } - return [ + let mouseIsInCell: CellAddress | null = null; + let mouseOvered: CellAddress | null = null; + + function processMouseOver(e: MousePointerCellEvent) { + mouseOvered = e; + if (mouseOver) { + if ( + !mouseOver({ + col: e.col, + row: e.row, + }) + ) { + return; + } + } + grid.getElement().style.cursor = "pointer"; + } + function processMouseOut(e: MousePointerCellEvent) { + if (mouseOut) { + mouseOut({ + col: e.col, + row: e.row, + }); + } + mouseOvered = null; + grid.getElement().style.cursor = ""; + } + + const disposables = [ // click grid.listen(DG_EVENT_TYPE.CLICK_CELL, (e) => { if (!isTarget(e.col, e.row)) { @@ -34,6 +66,9 @@ export function bindCellClickAction( if (isPromise(grid.getRowRecord(e.row))) { return; } + if (area) { + if (!area(e)) return; + } action({ col: e.col, row: e.row, @@ -47,31 +82,50 @@ export function bindCellClickAction( if (isPromise(grid.getRowRecord(e.row))) { return; } - if (mouseOver) { - if ( - !mouseOver({ - col: e.col, - row: e.row, - }) - ) { - return; - } + mouseIsInCell = e; + if (area) { + if (!area(e)) return; } - grid.getElement().style.cursor = "pointer"; + processMouseOver(e); }), grid.listen(DG_EVENT_TYPE.MOUSEOUT_CELL, (e) => { - if (!isTarget(e.col, e.row)) { + if ( + !mouseIsInCell || + mouseIsInCell.col !== e.col || + mouseIsInCell.row !== e.row + ) { return; } - if (mouseOut) { - mouseOut({ - col: e.col, - row: e.row, - }); + if (!mouseOvered) { + processMouseOut(e); } - grid.getElement().style.cursor = ""; }), ]; + if (area) { + disposables.push( + grid.listen(DG_EVENT_TYPE.MOUSEMOVE_CELL, (e) => { + if ( + !mouseIsInCell || + mouseIsInCell.col !== e.col || + mouseIsInCell.row !== e.row + ) { + return; + } + const isInArea = area(e); + if (!mouseOvered) { + if (!isInArea) return; + // mouse over + processMouseOver(e); + } else { + if (isInArea) return; + // mouse out + processMouseOut(e); + } + }) + ); + } + + return disposables; } export function bindCellKeyAction( grid: ListGridAPI, diff --git a/packages/cheetah-grid/src/js/columns/style.ts b/packages/cheetah-grid/src/js/columns/style.ts index 67f30b45a..23be3a234 100644 --- a/packages/cheetah-grid/src/js/columns/style.ts +++ b/packages/cheetah-grid/src/js/columns/style.ts @@ -10,6 +10,7 @@ import type { NumberStyleOption, PercentCompleteBarStyleOption, StyleOption, + TreeStyleOption, } from "../ts-types"; import { BaseStyle } from "./style/BaseStyle"; import { ButtonStyle } from "./style/ButtonStyle"; @@ -22,6 +23,7 @@ import { NumberStyle } from "./style/NumberStyle"; import { PercentCompleteBarStyle } from "./style/PercentCompleteBarStyle"; import { RadioStyle } from "./style/RadioStyle"; import { Style } from "./style/Style"; +import { TreeStyle } from "./style/TreeStyle"; const { EVENT_TYPE } = BaseStyle; export { @@ -37,6 +39,7 @@ export { PercentCompleteBarStyle, MultilineTextStyle, MenuStyle, + TreeStyle, }; export type { BaseStyleOption, @@ -49,6 +52,7 @@ export type { NumberStyleOption, PercentCompleteBarStyleOption, StyleOption, + TreeStyleOption, }; export function of( columnStyle: ColumnStyleOption | null | undefined, diff --git a/packages/cheetah-grid/src/js/columns/style/TreeStyle.ts b/packages/cheetah-grid/src/js/columns/style/TreeStyle.ts new file mode 100644 index 000000000..100e7b064 --- /dev/null +++ b/packages/cheetah-grid/src/js/columns/style/TreeStyle.ts @@ -0,0 +1,65 @@ +import type { + ColorDef, + TreeBranchIconStyle, + TreeLineStyle, + TreeStyleOption, +} from "../../ts-types"; +import { Style } from "./Style"; + +let defaultStyle: TreeStyle; +export class TreeStyle extends Style { + private _lineStyle?: TreeLineStyle; + private _lineColor?: ColorDef; + private _lineWidth?: number; + private _branchIcon?: TreeBranchIconStyle; + private _openedBranchIcon?: TreeBranchIconStyle; + static get DEFAULT(): TreeStyle { + return defaultStyle ? defaultStyle : (defaultStyle = new TreeStyle()); + } + constructor(style: TreeStyleOption = {}) { + super(style); + this._lineStyle = style.lineStyle; + this._lineColor = style.lineColor; + this._lineWidth = style.lineWidth; + this._branchIcon = style.branchIcon; + this._openedBranchIcon = style.openedBranchIcon; + } + clone(): TreeStyle { + return new TreeStyle(this); + } + get lineStyle(): TreeLineStyle | undefined { + return this._lineStyle; + } + set lineStyle(lineStyle: TreeLineStyle | undefined) { + this._lineStyle = lineStyle; + this.doChangeStyle(); + } + get lineColor(): ColorDef | undefined { + return this._lineColor; + } + set lineColor(lineColor: ColorDef | undefined) { + this._lineColor = lineColor; + this.doChangeStyle(); + } + get lineWidth(): number | undefined { + return this._lineWidth; + } + set lineWidth(lineWidth: number | undefined) { + this._lineWidth = lineWidth; + this.doChangeStyle(); + } + get branchIcon(): TreeBranchIconStyle | undefined { + return this._branchIcon; + } + set branchIcon(branchIcon: TreeBranchIconStyle | undefined) { + this._branchIcon = branchIcon; + this.doChangeStyle(); + } + get openedBranchIcon(): TreeBranchIconStyle | undefined { + return this._openedBranchIcon; + } + set openedBranchIcon(openedBranchIcon: TreeBranchIconStyle | undefined) { + this._openedBranchIcon = openedBranchIcon; + this.doChangeStyle(); + } +} diff --git a/packages/cheetah-grid/src/js/columns/type.ts b/packages/cheetah-grid/src/js/columns/type.ts index 0f1a39237..fec51264d 100644 --- a/packages/cheetah-grid/src/js/columns/type.ts +++ b/packages/cheetah-grid/src/js/columns/type.ts @@ -7,6 +7,7 @@ import type { MenuColumnOption, NumberColumnOption, PercentCompleteBarColumnOption, + TreeColumnOption, } from "../ts-types"; import type { BaseColumn } from "./type/BaseColumn"; import { BranchGraphColumn } from "./type/BranchGraphColumn"; @@ -20,6 +21,7 @@ import { MultilineTextColumn } from "./type/MultilineTextColumn"; import { NumberColumn } from "./type/NumberColumn"; import { PercentCompleteBarColumn } from "./type/PercentCompleteBarColumn"; import { RadioColumn } from "./type/RadioColumn"; +import { TreeColumn } from "./type/TreeColumn"; const TYPES = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -55,6 +57,7 @@ export { BranchGraphColumn, MenuColumn, MultilineTextColumn, + TreeColumn, }; export type { BaseColumnOption, @@ -64,6 +67,7 @@ export type { MenuColumnOption, NumberColumnOption, PercentCompleteBarColumnOption, + TreeColumnOption, }; export function of( columnType: ColumnTypeOption | BaseColumn | null | undefined diff --git a/packages/cheetah-grid/src/js/columns/type/BranchGraphColumn.ts b/packages/cheetah-grid/src/js/columns/type/BranchGraphColumn.ts index f2bfe10c6..98719962d 100644 --- a/packages/cheetah-grid/src/js/columns/type/BranchGraphColumn.ts +++ b/packages/cheetah-grid/src/js/columns/type/BranchGraphColumn.ts @@ -1,6 +1,6 @@ import type { BranchGraphColumnOption, - BranchGraphCommand, + BranchGraphCommandValue, CellContext, ColorDef, FieldDef, @@ -22,16 +22,16 @@ type Timelines = { timeline: BranchPoint[][]; branches: string[] }; function getAllColumnData( grid: ListGridAPI, field: FieldDef, - callback: (allData: BranchGraphCommand[]) => void + callback: (allData: BranchGraphCommandValue[]) => void ): void { const { dataSource } = grid; - const allData: BranchGraphCommand[] = []; + const allData: BranchGraphCommandValue[] = []; let promise; for (let index = 0; index < dataSource.length; index++) { const data = dataSource.getField( index, field - ) as MaybePromiseOrUndef; + ) as MaybePromiseOrUndef; if (isPromise(data)) { const dataIndex = allData.length; allData.push(undefined); @@ -106,18 +106,22 @@ class BranchPoint { (l) => l.fromIndex != null && l.toIndex != null ); - const froms = lines.filter((l) => l.fromIndex != null && l.toIndex == null); - const tos = lines.filter((l) => l.fromIndex == null && l.toIndex != null); + const fromList = lines.filter( + (l) => l.fromIndex != null && l.toIndex == null + ); + const toList = lines.filter( + (l) => l.fromIndex == null && l.toIndex != null + ); - froms.forEach((f) => { - for (let i = 0; i < tos.length; i++) { - const t = tos[i]; + fromList.forEach((f) => { + for (let i = 0; i < toList.length; i++) { + const t = toList[i]; if (t.point) { continue; } if (f.colorIndex === t.colorIndex) { f.toIndex = t.toIndex; - tos.splice(i, 1); + toList.splice(i, 1); break; } } @@ -125,7 +129,7 @@ class BranchPoint { result.push(f); }); - return result.concat(tos); + return result.concat(toList); } static merge(a: BranchPoint, b: BranchPoint): BranchPoint { if (!a) { @@ -310,8 +314,8 @@ function commitMerge( }), ], }); - const froms = [...timeline]; - const fromTargetLine = froms.pop(); + const fromList = [...timeline]; + const fromTargetLine = fromList.pop(); if (fromTargetLine) { fromTargetLine[fromIndex] = BranchPoint.merge( fromTargetLine[fromIndex], @@ -327,7 +331,7 @@ function commitMerge( ); } - if (joinLine(froms, fromIndex) && fromTargetLine) { + if (joinLine(fromList, fromIndex) && fromTargetLine) { fromTargetLine[fromIndex].lines = BranchPoint.mergeLines( fromTargetLine[fromIndex].lines.concat([ new BranchLine({ @@ -342,7 +346,7 @@ function commitMerge( return result; } -function calcCommand(info: Timelines, command: BranchGraphCommand): void { +function calcCommand(info: Timelines, command: BranchGraphCommandValue): void { const { timeline } = info; const timelineData: BranchPoint[] = []; // const last = timeline.length > 0 ? timeline[timeline.length - 1] : null; @@ -511,7 +515,7 @@ function renderMerge( * BranchGraphColumn * * Data commands - * - mastar branch or orphan branch + * - master branch or orphan branch * * ```js * { @@ -576,8 +580,9 @@ export class BranchGraphColumn extends BaseColumn { get StyleClass(): typeof BranchGraphStyle { return BranchGraphStyle; } - clearCache(grid: GridInternal): void { - delete grid[_]; + clearCache(grid: ListGridAPI): void { + const internal = grid as GridInternal; + delete internal[_]; } onDrawCell( cellValue: MaybePromise, @@ -598,6 +603,12 @@ export class BranchGraphColumn extends BaseColumn { clone(): BranchGraphColumn { return new BranchGraphColumn(this); } + get start(): "top" | "bottom" { + return this._start; + } + get cache(): boolean { + return this._cache; + } drawInternal( _value: unknown, context: CellContext, diff --git a/packages/cheetah-grid/src/js/columns/type/IconColumn.ts b/packages/cheetah-grid/src/js/columns/type/IconColumn.ts index d5bc47b44..f1f71419b 100644 --- a/packages/cheetah-grid/src/js/columns/type/IconColumn.ts +++ b/packages/cheetah-grid/src/js/columns/type/IconColumn.ts @@ -25,7 +25,7 @@ function repeatArray( } export class IconColumn extends Column { - private _tagName?: string; + private _tagName: string; private _className?: string; private _content?: string; private _name?: string; @@ -44,6 +44,21 @@ export class IconColumn extends Column { clone(): IconColumn { return new IconColumn(this); } + get tagName(): string { + return this._tagName; + } + get className(): string | undefined { + return this._className; + } + get content(): string | undefined { + return this._content; + } + get name(): string | undefined { + return this._name; + } + get iconWidth(): number | undefined { + return this._iconWidth; + } drawInternal( value: unknown, context: CellContext, diff --git a/packages/cheetah-grid/src/js/columns/type/MultilineTextColumn.ts b/packages/cheetah-grid/src/js/columns/type/MultilineTextColumn.ts index 765d8e4c6..9964e0ce6 100644 --- a/packages/cheetah-grid/src/js/columns/type/MultilineTextColumn.ts +++ b/packages/cheetah-grid/src/js/columns/type/MultilineTextColumn.ts @@ -48,13 +48,13 @@ export class MultilineTextColumn extends BaseColumn { return; } const textValue = value != null ? String(value) : ""; - const multilines = textValue + const lines = textValue .replace(/\r?\n/g, "\n") .replace(/\r/g, "\n") .split("\n"); helper.testFontLoad(font, textValue, context); utils.loadIcons(getIcon(), context, helper, (icons, context) => { - helper.multilineText(multilines, context, { + helper.multilineText(lines, context, { textAlign, textBaseline, color, diff --git a/packages/cheetah-grid/src/js/columns/type/NumberColumn.ts b/packages/cheetah-grid/src/js/columns/type/NumberColumn.ts index 7e2792767..6bca061ac 100644 --- a/packages/cheetah-grid/src/js/columns/type/NumberColumn.ts +++ b/packages/cheetah-grid/src/js/columns/type/NumberColumn.ts @@ -1,14 +1,26 @@ import { Column } from "./Column"; import type { NumberColumnOption } from "../../ts-types"; import { NumberStyle } from "../style/NumberStyle"; -let defaultFotmat: Intl.NumberFormat; +let defaultFormat: Intl.NumberFormat; export class NumberColumn extends Column { private _format?: Intl.NumberFormat; + static get defaultFormat(): Intl.NumberFormat { + return defaultFormat || (defaultFormat = new Intl.NumberFormat()); + } + static set defaultFormat(fmt: Intl.NumberFormat) { + defaultFormat = fmt; + } + /** + * @deprecated Use defaultFormat instead + */ static get defaultFotmat(): Intl.NumberFormat { - return defaultFotmat || (defaultFotmat = new Intl.NumberFormat()); + return this.defaultFormat; } + /** + * @deprecated Use defaultFormat instead + */ static set defaultFotmat(fmt: Intl.NumberFormat) { - defaultFotmat = fmt; + this.defaultFormat = fmt; } constructor(option: NumberColumnOption = {}) { super(option); @@ -34,7 +46,7 @@ export class NumberColumn extends Column { const convertedValue = super.convertInternal(value); return convertedValue != null ? String(convertedValue) : ""; } - const format = this._format || NumberColumn.defaultFotmat; + const format = this._format || NumberColumn.defaultFormat; return format.format(num); } } diff --git a/packages/cheetah-grid/src/js/columns/type/PercentCompleteBarColumn.ts b/packages/cheetah-grid/src/js/columns/type/PercentCompleteBarColumn.ts index 5755ffa66..14366c9b5 100644 --- a/packages/cheetah-grid/src/js/columns/type/PercentCompleteBarColumn.ts +++ b/packages/cheetah-grid/src/js/columns/type/PercentCompleteBarColumn.ts @@ -27,6 +27,15 @@ export class PercentCompleteBarColumn extends Column { clone(): PercentCompleteBarColumn { return new PercentCompleteBarColumn(this); } + get min(): number { + return this.min; + } + get max(): number { + return this.max; + } + get formatter(): (value: unknown) => unknown { + return this.formatter; + } drawInternal( value: unknown, context: CellContext, diff --git a/packages/cheetah-grid/src/js/columns/type/TreeColumn.ts b/packages/cheetah-grid/src/js/columns/type/TreeColumn.ts new file mode 100644 index 000000000..1a84dcae0 --- /dev/null +++ b/packages/cheetah-grid/src/js/columns/type/TreeColumn.ts @@ -0,0 +1,515 @@ +import * as inlineUtils from "../../element/inlines"; +import * as utils from "../../columns/type/columnUtils"; +import type { + ActionAreaPredicate, + CellAddress, + CellContext, + ColumnIconOption, + FieldDef, + GridCanvasHelperAPI, + ListGridAPI, + MaybePromise, + RectProps, + TreeBranchIconStyle, + TreeColumnOption, + TreeDataValue, +} from "../../ts-types"; +import type { DrawCellInfo, GridInternal } from "../../ts-types-internal"; +import { Column } from "./Column"; +import { Rect } from "../../internal/Rect"; +import { TreeLineKind } from "../../ts-types-internal"; +import { TreeStyle } from "../style/TreeStyle"; +import { getFontSize } from "../../internal/canvases"; +import { getTreeColumnStateId } from "../../internal/symbolManager"; +import { isPromise } from "../../internal/utils"; + +type NormalizedTreeData = { + /** The caption of the record */ + caption: string; + /** An array of path indicating the hierarchy */ + path: unknown[]; + /** icon */ + icon?: ColumnIconOption; + nodeType?: "leaf" | "branch"; +}; + +type TreeInfo = { + getLines: () => TreeLineKind[]; + caption: string; + path: unknown[]; + getIcon: (data: { + branchIcon: TreeBranchIconStyle; + openedBranchIcon: TreeBranchIconStyle; + fontSize: number; + }) => ColumnIconOption | null; +}; + +const _ = getTreeColumnStateId(); + +export class TreeColumn extends Column { + private _cache: boolean; + constructor(option: TreeColumnOption = {}) { + super(option); + this._cache = option.cache != null ? option.cache : false; + } + get StyleClass(): typeof TreeStyle { + return TreeStyle; + } + clearCache(grid: ListGridAPI): void { + const internal = grid as GridInternal; + if (!internal[_]) return; + delete internal[_].cache; + } + get drawnIconActionArea(): ActionAreaPredicate { + return (param) => { + const internal = param.grid as GridInternal; + const state = internal[_]; + if (!state?.drawnIcons) return false; + const drawnIcons = state.drawnIcons as DrawnIcons; + return drawnIcons.area(param); + }; + } + onDrawCell( + cellValue: MaybePromise, + info: DrawCellInfo, + context: CellContext, + grid: GridInternal + ): void | Promise { + const state = grid[_] || (grid[_] = {}); + if (this._cache && !state.cache) { + const cache = state.cache || (state.cache = new Map()); + const { col, row } = context; + const field = grid.getField(col, row) as FieldDef; + if (!cache.has(field)) { + cache.set(field, new TreeColumnInfo(grid, field)); + } + } + return super.onDrawCell(cellValue, info, context, grid); + } + clone(): TreeColumn { + return new TreeColumn(this); + } + get cache(): boolean { + return this._cache; + } + getCopyCellValue(value: unknown): unknown { + const treeData = getTreeDataFromValue(value as TreeDataValue); + return treeData.caption; + } + drawInternal( + value: unknown, + context: CellContext, + style: TreeStyle, + helper: GridCanvasHelperAPI, + grid: GridInternal, + { drawCellBase, getIcon }: DrawCellInfo + ): void { + const { + textAlign, + textBaseline, + bgColor, + padding, + color, + font, + textOverflow, + } = style; + if (bgColor) { + drawCellBase({ + bgColor, + }); + } + + const state = grid[_] || (grid[_] = {}); + if (state.drawnIcons) { + const drawnIcons = state.drawnIcons as DrawnIcons; + drawnIcons.delete(context); + } + + const { col, row } = context; + const field = grid.getField(col, row) as FieldDef; + const tci: TreeColumnInfo = ((this._cache + ? state.cache?.get(field) + : null) ?? new TreeColumnInfo(grid, field)) as TreeColumnInfo; + const info = tci.getInfo(value as TreeDataValue, row); + + helper.testFontLoad(font, info.caption, context); + utils.loadIcons(getIcon(), context, helper, (icons, context) => { + const rect = context.getRect(); + + const basePadding = helper.toBoxPixelArray(padding || 0, context, font); + + const nestLevel = info.path.length; + + helper.drawWithClip(context, (ctx) => { + const fontSize = getFontSize(ctx, font); + const indentSize = fontSize.width; + const top = rect.top + basePadding[0]; + const left = rect.left + basePadding[3]; + const height = rect.height - basePadding[0] - basePadding[2]; + const lineBaseline = textBaseline ?? (ctx.textBaseline || "middle"); + + // Calculate horizontal line position + let hLineY: number = top + height / 2; + if ( + lineBaseline === "bottom" || + lineBaseline === "alphabetic" || + lineBaseline === "ideographic" + ) { + // bottom + hLineY = top + height - fontSize.height / 2; + } else if (textBaseline === "middle") { + hLineY = top + height / 2; + } else { + // top + hLineY = top + fontSize.height / 2; + } + + // Get icon + const treeIcon = info.getIcon({ + branchIcon: style.branchIcon || helper.theme.tree.branchIcon, + openedBranchIcon: + style.openedBranchIcon || helper.theme.tree.openedBranchIcon, + fontSize: fontSize.width, + }); + + // Calculate icon rect + let iconRect: Rect | null = null; + if (treeIcon) { + ctx.save(); + try { + const treeLineLeft = left + indentSize * (nestLevel - 1); + const vLineX = treeLineLeft + indentSize / 2; + const size = inlineUtils.iconOf(treeIcon).width({ ctx }); + iconRect = new Rect( + vLineX - size / 2, + hLineY - size / 2, + size, + size + ); + } finally { + ctx.restore(); + } + + // It preserves the position of the drawn icon + // because it is used for the `area` option of the `Action` class. + const drawnIcons = (state.drawnIcons || + (state.drawnIcons = new DrawnIcons())) as DrawnIcons; + drawnIcons.set(context, iconRect); + } + + // Get tree line color + const lineStyle = style.lineStyle || helper.theme.tree.lineStyle; + + if (lineStyle !== "none") { + const lineWidth = style.lineWidth || helper.theme.tree.lineWidth; + const lineColor = + style.lineColor || + helper.getColor(helper.theme.tree.lineColor, col, row, ctx); + + ctx.save(); + try { + ctx.strokeStyle = lineColor; + ctx.lineWidth = lineWidth; + ctx.lineCap = "round"; + + // Draw tree lines + let needRestoreClip = false; + try { + if (iconRect) { + ctx.save(); + needRestoreClip = true; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, ctx.canvas.height); + ctx.lineTo(ctx.canvas.width, ctx.canvas.height); + ctx.lineTo(ctx.canvas.width, 0); + ctx.lineTo(0, 0); + ctx.lineTo(iconRect.left, iconRect.top); + ctx.lineTo(iconRect.right, iconRect.top); + ctx.lineTo(iconRect.right, iconRect.bottom); + ctx.lineTo(iconRect.left, iconRect.bottom); + ctx.lineTo(iconRect.left, iconRect.top); + + ctx.closePath(); + + ctx.clip(); + } + info.getLines().forEach((line: TreeLineKind, index: number) => { + const treeLineLeft = left + indentSize * index; + const vLineX = treeLineLeft + indentSize / 2; + const treeLineRight = treeLineLeft + indentSize; + if (line !== TreeLineKind.none) { + ctx.beginPath(); + if (line === TreeLineKind.vertical) { + ctx.moveTo(vLineX, rect.top); + ctx.lineTo(vLineX, rect.bottom); + } else if (line === TreeLineKind.last) { + ctx.moveTo(vLineX, rect.top); + ctx.lineTo(vLineX, hLineY); + ctx.lineTo(treeLineRight, hLineY); + } else if (line === TreeLineKind.start) { + ctx.moveTo(treeLineRight, hLineY); + ctx.lineTo(vLineX, hLineY); + ctx.lineTo(vLineX, rect.bottom); + } else if (line === TreeLineKind.verticalBranch) { + ctx.moveTo(vLineX, rect.top); + ctx.lineTo(vLineX, rect.bottom); + ctx.moveTo(vLineX, hLineY); + ctx.lineTo(treeLineRight, hLineY); + } else if (line === TreeLineKind.horizontal) { + ctx.moveTo(treeLineLeft, hLineY); + ctx.lineTo(treeLineRight, hLineY); + } else if (line === TreeLineKind.horizontalBranch) { + ctx.moveTo(treeLineLeft, hLineY); + ctx.lineTo(treeLineRight, hLineY); + ctx.moveTo(vLineX, hLineY); + ctx.lineTo(vLineX, rect.bottom); + } else if (line === TreeLineKind.lone) { + ctx.moveTo(vLineX, hLineY); + ctx.lineTo(treeLineRight, hLineY); + } + ctx.stroke(); + } + }); + } finally { + if (needRestoreClip) { + ctx.restore(); + } + } + } finally { + ctx.restore(); + } + } + + if (treeIcon) { + // Draw tree icon + const iconLeftOffset = indentSize * (nestLevel - 1); + const iconWidth = indentSize; + const iconPadding = basePadding.slice(0); + iconPadding[3] += iconLeftOffset; + iconPadding[1] = rect.width - iconWidth - iconPadding[3]; // padding right + + helper.text("", context, { + textAlign: "center", + textBaseline, + color, + font, + icons: [treeIcon], + padding: iconPadding, + }); + } + + const textPadding = basePadding.slice(0); + textPadding[3] += nestLevel * indentSize; // Tree indent padding + + helper.text(info.caption, context, { + textAlign, + textBaseline, + color, + font, + padding: textPadding, + textOverflow, + icons, + }); + }); + }); + } +} + +class TreeColumnInfo { + private _cache: Record< + number, + { + hasNextSiblings: (boolean | undefined)[]; + } + > = {}; + private _grid: ListGridAPI; + private _field: FieldDef; + constructor(grid: ListGridAPI, field: FieldDef) { + this._grid = grid; + this._field = field; + } + getInfo(value: TreeDataValue, row: number): TreeInfo { + const { _field: field, _grid: grig, _cache: cache } = this; + const currIndex = grig.getRecordIndexByRow(row); + const { dataSource } = grig; + const treeData = getTreeDataFromValue(value); + + const hasChildren = hasNextSiblingWithCache(treeData.path); + + return { + caption: treeData.caption, + path: treeData.path, + getLines() { + const currPath: unknown[] = []; + const parentPath = treeData.path.slice(0, -1); + const parentLines: TreeLineKind[] = parentPath.map((p, index) => { + currPath.push(p); + const isLast = index === parentPath.length - 1; + if (hasNextSiblingWithCache(currPath)) { + return isLast ? TreeLineKind.verticalBranch : TreeLineKind.vertical; + } else { + return isLast ? TreeLineKind.last : TreeLineKind.none; + } + }); + + let selfLine: TreeLineKind; + if (hasChildren) { + selfLine = + parentPath.length > 0 + ? TreeLineKind.horizontalBranch + : TreeLineKind.start; + } else { + selfLine = + parentPath.length > 0 ? TreeLineKind.horizontal : TreeLineKind.lone; + } + return parentLines.concat(selfLine); + }, + getIcon(data) { + if (treeData.icon) return treeData.icon; + if (data.branchIcon === "none" && data.openedBranchIcon === "none") { + return null; + } + // const nodeType = hasChildren ? "branch" : treeData.nodeType ?? "leaf"; + const branchIcon = hasChildren + ? data.openedBranchIcon + : treeData.nodeType === "branch" + ? data.branchIcon + : "none"; + if (branchIcon === "chevron_right") { + return { name: "chevron_right", width: data.fontSize }; + } + if (branchIcon === "expand_more") { + return { name: "expand_more", width: data.fontSize }; + } + return null; + }, + }; + + function hasNextSiblingWithCache(targetPath: unknown[]): boolean { + const has = hasNextSiblingFromCache(currIndex, targetPath.length); + if (has != null) { + return has; + } + const result = hasNextSibling(targetPath); + for (let index = currIndex; index < result.end; index++) { + setNextSiblingToCache(index, targetPath.length, result.has); + } + return result.has; + } + + function hasNextSiblingFromCache( + index: number, + level: number + ): boolean | undefined { + const { hasNextSiblings } = + cache[index] || (cache[index] = { hasNextSiblings: [] }); + return hasNextSiblings[level]; + } + + function setNextSiblingToCache( + index: number, + level: number, + value: boolean + ): void { + const { hasNextSiblings } = + cache[index] || (cache[index] = { hasNextSiblings: [] }); + hasNextSiblings[level] = value; + } + + function hasNextSibling(targetPath: unknown[]): { + end: number; + has: boolean; + } { + const startIndex = currIndex + 1; + for (let index = startIndex; index < dataSource.length; index++) { + const data = dataSource.getField(index, field); + if (isPromise(data)) return { end: index, has: false }; + const nextPath = getParentPath(data); + if (!nextPath.length) return { end: index, has: false }; + if (targetPath.every((p, i) => p === nextPath[i])) { + // All matches! + if (targetPath.length < nextPath.length) { + // It's a child. + // e.g. + // ├ target + // │ ├ next + const has = hasNextSiblingFromCache(index, targetPath.length); + if (has != null) return { end: index, has }; + continue; + } + // There is next sibling. + // e.g. + // ├ target + // │ ├ x + // │ └ x + // └ next + return { end: index, has: true }; + } + // There is no next sibling. + // e.g. + // │ └ target + // │ ├ x + // │ └ x + // └ next + return { end: index, has: false }; + } + + // There is no next sibling. + return { end: dataSource.length, has: false }; + } + } +} + +function getTreeDataFromValue(value: TreeDataValue): NormalizedTreeData { + if (value != null) { + if (Array.isArray(value)) { + return getTreeDataFromValue({ path: value }); + } else { + if (Array.isArray(value.path)) + return { + caption: String( + value.caption ?? value.path[value.path.length - 1] ?? "" + ), + path: value.path, + icon: value.icon as never, + nodeType: value.nodeType as never, + }; + if (typeof value.path === "function") + return getTreeDataFromValue({ ...value, path: value.path() }); + } + } + + return { caption: String(value ?? ""), path: [value] }; +} + +function getParentPath(value: TreeDataValue): unknown[] { + return getTreeDataFromValue(value).path.slice(0, -1); +} + +class DrawnIcons { + private _drawnIcons = new Map(); + set(cell: CellAddress, clipRect: RectProps) { + this._drawnIcons.set(`${cell.col}:${cell.row}`, clipRect); + } + delete(cell: CellAddress) { + this._drawnIcons.delete(`${cell.col}:${cell.row}`); + } + area({ + col, + row, + pointInDrawingCanvas: point, + }: Parameters[0]): boolean { + const key = `${col}:${row}`; + const rect = this._drawnIcons.get(key); + if (!rect) { + return false; + } + return ( + rect.left <= point.x && + point.x <= rect.right && + rect.top <= point.y && + point.y <= rect.bottom + ); + } +} diff --git a/packages/cheetah-grid/src/js/core/DrawGrid.ts b/packages/cheetah-grid/src/js/core/DrawGrid.ts index dd5f18de8..5f283aac6 100644 --- a/packages/cheetah-grid/src/js/core/DrawGrid.ts +++ b/packages/cheetah-grid/src/js/core/DrawGrid.ts @@ -15,6 +15,7 @@ import type { EventListenerId, KeyboardEventListener, KeydownEvent, + MousePointerCellEvent, PasteCellEvent, PasteRangeBoxValues, } from "../ts-types"; @@ -1461,42 +1462,56 @@ function _bindEvents(this: DrawGrid): void { let isMouseover = false; let mouseEnterCell: CellAddress | null = null; let mouseOverCell: CellAddress | null = null; - function onMouseenterCell(cell: CellAddress, related?: CellAddress): void { + type MousePointerCellEventInfoProps = Pick< + MousePointerCellEvent, + "related" | "event" + >; + function onMouseenterCell( + cell: CellAddress, + props: MousePointerCellEventInfoProps + ): void { grid.fireListeners(DG_EVENT_TYPE.MOUSEENTER_CELL, { + ...props, col: cell.col, row: cell.row, - related, }); mouseEnterCell = cell; } - function onMouseleaveCell(related?: CellAddress): CellAddress | undefined { + function onMouseleaveCell( + props: MousePointerCellEventInfoProps + ): CellAddress | undefined { const beforeMouseCell = mouseEnterCell; mouseEnterCell = null; if (beforeMouseCell) { grid.fireListeners(DG_EVENT_TYPE.MOUSELEAVE_CELL, { + ...props, col: beforeMouseCell.col, row: beforeMouseCell.row, - related, }); } return beforeMouseCell || undefined; } - function onMouseoverCell(cell: CellAddress, related?: CellAddress): void { + function onMouseoverCell( + cell: CellAddress, + props: MousePointerCellEventInfoProps + ): void { grid.fireListeners(DG_EVENT_TYPE.MOUSEOVER_CELL, { + ...props, col: cell.col, row: cell.row, - related, }); mouseOverCell = cell; } - function onMouseoutCell(related?: CellAddress): CellAddress | undefined { + function onMouseoutCell( + props: MousePointerCellEventInfoProps + ): CellAddress | undefined { const beforeMouseCell = mouseOverCell; mouseOverCell = null; if (beforeMouseCell) { grid.fireListeners(DG_EVENT_TYPE.MOUSEOUT_CELL, { + ...props, col: beforeMouseCell.col, row: beforeMouseCell.row, - related, }); } return beforeMouseCell || undefined; @@ -1505,13 +1520,13 @@ function _bindEvents(this: DrawGrid): void { handler.on(scrollElement, "mouseover", (_e: MouseEvent): void => { isMouseover = true; }); - handler.on(scrollElement, "mouseout", (_e: MouseEvent): void => { + handler.on(scrollElement, "mouseout", (event: MouseEvent): void => { isMouseover = false; - onMouseoutCell(); + onMouseoutCell({ event }); }); - handler.on(element, "mouseleave", (_e: MouseEvent): void => { - onMouseleaveCell(); + handler.on(element, "mouseleave", (event: MouseEvent): void => { + onMouseleaveCell({ event }); }); handler.on(element, "mousemove", (e) => { @@ -1534,32 +1549,45 @@ function _bindEvents(this: DrawGrid): void { col: eventArgs.col, row: eventArgs.row, }; - const outCell = onMouseoutCell(enterCell); - const leaveCell = onMouseleaveCell(enterCell); - onMouseenterCell(enterCell, leaveCell); + const outCell = onMouseoutCell({ related: enterCell, event: e }); + const leaveCell = onMouseleaveCell({ related: enterCell, event: e }); + onMouseenterCell(enterCell, { related: leaveCell, event: e }); if (isMouseover) { - onMouseoverCell(enterCell, outCell); + onMouseoverCell(enterCell, { related: outCell, event: e }); } } else if (isMouseover && !mouseOverCell) { - onMouseoverCell({ - col: eventArgs.col, - row: eventArgs.row, - }); + onMouseoverCell( + { + col: eventArgs.col, + row: eventArgs.row, + }, + { + event: e, + } + ); } } else { const enterCell = { col: eventArgs.col, row: eventArgs.row, }; - onMouseenterCell(enterCell); + onMouseenterCell(enterCell, { + event: e, + }); if (isMouseover) { - onMouseoverCell(enterCell); + onMouseoverCell(enterCell, { + event: e, + }); } grid.fireListeners(DG_EVENT_TYPE.MOUSEMOVE_CELL, eventArgs); } } else { - onMouseoutCell(); - onMouseleaveCell(); + onMouseoutCell({ + event: e, + }); + onMouseleaveCell({ + event: e, + }); } if (grid[_].columnResizer.moving(e) || grid[_].cellSelector.moving(e)) { return; diff --git a/packages/cheetah-grid/src/js/header/type/SortHeader.ts b/packages/cheetah-grid/src/js/header/type/SortHeader.ts index 79fbd2e8f..02590843e 100644 --- a/packages/cheetah-grid/src/js/header/type/SortHeader.ts +++ b/packages/cheetah-grid/src/js/header/type/SortHeader.ts @@ -72,11 +72,11 @@ export class SortHeader extends BaseHeader { }; if (multiline) { - const multilines = textValue + const lines = textValue .replace(/\r?\n/g, "\n") .replace(/\r/g, "\n") .split("\n"); - helper.multilineText(multilines, context, { + helper.multilineText(lines, context, { textAlign, textBaseline, color, diff --git a/packages/cheetah-grid/src/js/icons.ts b/packages/cheetah-grid/src/js/icons.ts index fe205f468..8e471cc4e 100644 --- a/packages/cheetah-grid/src/js/icons.ts +++ b/packages/cheetah-grid/src/js/icons.ts @@ -60,6 +60,70 @@ const builtins = { height: 24, }; }, + get keyboard_arrow_down(): IconDefine { + // return require("cheetah-grid-icon-svg-loader!material-design-icons/hardware/svg/production/ic_keyboard_arrow_down_48px.svg"); + return { + d: "M14.83 16.42L24 25.59l9.17-9.17L36 19.25l-12 12-12-12z", + width: 48, + height: 48, + }; + }, + get keyboard_arrow_left(): IconDefine { + // return require("cheetah-grid-icon-svg-loader!material-design-icons/hardware/svg/production/ic_keyboard_arrow_left_48px.svg"); + return { + d: "M30.83 32.67l-9.17-9.17 9.17-9.17L28 11.5l-12 12 12 12z", + width: 48, + height: 48, + }; + }, + get keyboard_arrow_right(): IconDefine { + // return require("cheetah-grid-icon-svg-loader!material-design-icons/hardware/svg/production/ic_keyboard_arrow_right_48px.svg"); + return { + d: "M17.17 32.92l9.17-9.17-9.17-9.17L20 11.75l12 12-12 12z", + width: 48, + height: 48, + }; + }, + get keyboard_arrow_up(): IconDefine { + // return require("cheetah-grid-icon-svg-loader!material-design-icons/hardware/svg/production/ic_keyboard_arrow_up_48px.svg"); + return { + d: "M14.83 30.83L24 21.66l9.17 9.17L36 28 24 16 12 28z", + width: 48, + height: 48, + }; + }, + get chevron_left(): IconDefine { + // return require("cheetah-grid-icon-svg-loader!material-design-icons/navigation/svg/production/ic_chevron_left_48px.svg"); + return { + d: "M14.83 30.83L24 21.66l9.17 9.17L36 28 24 16 12 28z", + width: 48, + height: 48, + }; + }, + get chevron_right(): IconDefine { + // return require("cheetah-grid-icon-svg-loader!material-design-icons/navigation/svg/production/ic_chevron_right_48px.svg"); + return { + d: "M20 12l-2.83 2.83L26.34 24l-9.17 9.17L20 36l12-12z", + width: 48, + height: 48, + }; + }, + get expand_less(): IconDefine { + // return require("cheetah-grid-icon-svg-loader!material-design-icons/navigation/svg/production/ic_expand_less_48px.svg"); + return { + d: "M24 16L12 28l2.83 2.83L24 21.66l9.17 9.17L36 28z", + width: 48, + height: 48, + }; + }, + get expand_more(): IconDefine { + // return require("cheetah-grid-icon-svg-loader!material-design-icons/navigation/svg/production/ic_expand_more_48px.svg"); + return { + d: "M33.17 17.17L24 26.34l-9.17-9.17L12 20l12 12 12-12z", + width: 48, + height: 48, + }; + }, }; export function get(): { [key: string]: IconDefine } { diff --git a/packages/cheetah-grid/src/js/internal/symbolManager.ts b/packages/cheetah-grid/src/js/internal/symbolManager.ts index 2e1e2f37c..54f5a3e0f 100644 --- a/packages/cheetah-grid/src/js/internal/symbolManager.ts +++ b/packages/cheetah-grid/src/js/internal/symbolManager.ts @@ -54,6 +54,9 @@ export function getColumnFadeinStateId(): "$$$$col.fadein_stateID symbol$$$$" /* export function getBranchGraphColumnStateId(): "$$$$branch_graph_col.stateID symbol$$$$" /* It is treated as a string so that it can be handled easily with typescript. */ { return get("branch_graph_col.stateID") as any; } +export function getTreeColumnStateId(): "$$$$tree_col.stateID symbol$$$$" /* It is treated as a string so that it can be handled easily with typescript. */ { + return get("tree_col.stateID") as any; +} export function getSmallDialogInputEditorStateId(): "$$$$small_dialog_input_editor.stateID symbol$$$$" /* It is treated as a string so that it can be handled easily with typescript. */ { return get("small_dialog_input_editor.stateID") as any; } diff --git a/packages/cheetah-grid/src/js/themes/theme.ts b/packages/cheetah-grid/src/js/themes/theme.ts index a6594f1bf..dfac8bcea 100644 --- a/packages/cheetah-grid/src/js/themes/theme.ts +++ b/packages/cheetah-grid/src/js/themes/theme.ts @@ -5,6 +5,8 @@ import type { RequiredThemeDefine, StylePropertyFunctionArg, ThemeDefine, + TreeBranchIconStyle, + TreeLineStyle, } from "../ts-types"; import { getChainSafe } from "../internal/utils"; import { get as getSymbol } from "../internal/symbolManager"; @@ -52,6 +54,7 @@ export class Theme implements RequiredThemeDefine { private _checkbox: RequiredThemeDefine["checkbox"] | null = null; private _radioButton: RequiredThemeDefine["radioButton"] | null = null; private _button: RequiredThemeDefine["button"] | null = null; + private _tree: RequiredThemeDefine["tree"] | null = null; private _header: RequiredThemeDefine["header"] | null = null; private _messages: RequiredThemeDefine["messages"] | null = null; private _indicators: RequiredThemeDefine["indicators"] | null = null; @@ -233,6 +236,61 @@ export class Theme implements RequiredThemeDefine { return getProp(obj, superTheme, ["button", prop], defNames); } } + get tree(): RequiredThemeDefine["tree"] { + const { obj, superTheme } = this[_]; + return ( + this._tree || + (this._tree = { + get lineStyle(): TreeLineStyle { + return getTreeProp("lineStyle", undefined, undefined, "solid"); + }, + get lineColor(): ColorPropertyDefine { + return getTreeProp( + "lineColor", + ["borderColor"], + colorsToColor, + "#0000" + ); + }, + get lineWidth(): number { + return getTreeProp("lineWidth", undefined, undefined, 1); + }, + get branchIcon(): TreeBranchIconStyle { + return getTreeProp( + "branchIcon", + undefined, + undefined, + "chevron_right" + ); + }, + get openedBranchIcon(): TreeBranchIconStyle { + return getTreeProp( + "openedBranchIcon", + undefined, + undefined, + "expand_more" + ); + }, + }) + ); + function getTreeProp< + T extends ColorPropertyDefine | number | TreeLineStyle + >( + prop: string, + defNames: string[] | undefined, + convertForSuper?: (value: never) => T | undefined, + defaultValue?: T + ): T { + return getProp( + obj, + superTheme, + ["tree", prop], + defNames, + convertForSuper, + defaultValue + ); + } + } get header(): RequiredThemeDefine["header"] { const { obj, superTheme } = this[_]; return ( diff --git a/packages/cheetah-grid/src/js/ts-types-internal/grid-engine.ts b/packages/cheetah-grid/src/js/ts-types-internal/grid-engine.ts index 71544e77d..06906f778 100644 --- a/packages/cheetah-grid/src/js/ts-types-internal/grid-engine.ts +++ b/packages/cheetah-grid/src/js/ts-types-internal/grid-engine.ts @@ -97,12 +97,35 @@ export type CheckHeaderState = { mouseActiveCell?: CellAddress; }; +export const enum TreeLineKind { + none, + // │ + vertical, + // └ + last, + // ┌ + start, + // ├ + verticalBranch, + // ─ + horizontal, + // ┬ + horizontalBranch, + // half ─ + lone, +} +export type TreeColumnState = { + drawnIcons?: unknown; + cache?: Map, unknown>; +}; + export interface GridInternal extends ListGridAPI { "$$$$col.fadein_stateID symbol$$$$"?: ColumnFadeinState; "$$$$btncol.stateID symbol$$$$"?: ButtonColumnState; "$$$$chkcol.stateID symbol$$$$"?: CheckColumnState; "$$$$rdcol.stateID symbol$$$$"?: RadioColumnState; "$$$$branch_graph_col.stateID symbol$$$$"?: BranchGraphColumnState; + "$$$$tree_col.stateID symbol$$$$"?: TreeColumnState; "$$$$inline_menu_editor.stateID symbol$$$$"?: InputEditorState; "$$$$inline_input_editor.stateID symbol$$$$"?: InputEditorState; "$$$$small_dialog_input_editor.stateID symbol$$$$"?: InputEditorState; diff --git a/packages/cheetah-grid/src/js/ts-types/column/action.ts b/packages/cheetah-grid/src/js/ts-types/column/action.ts index 7aa22f5bc..a43a0e8c2 100644 --- a/packages/cheetah-grid/src/js/ts-types/column/action.ts +++ b/packages/cheetah-grid/src/js/ts-types/column/action.ts @@ -13,18 +13,35 @@ export interface BaseActionOption { export type ActionListener = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any record: any, - cell: CellAddress & { + meta: CellAddress & { // eslint-disable-next-line @typescript-eslint/no-explicit-any grid: ListGridAPI; } ) => void; -export interface ActionOption extends BaseActionOption { +export type ActionAreaPredicate = ( + meta: CellAddress & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + grid: ListGridAPI; + /** The mouse position relative to the cell position. */ + pointInCell: { x: number; y: number }; + /** The mouse position relative to the drawing canvas. */ + pointInDrawingCanvas: { x: number; y: number }; + } +) => boolean; +export interface AbstractActionOption extends BaseActionOption { action?: ActionListener; } +export interface ActionOption extends AbstractActionOption { + action?: ActionListener; + /** A function that checks whether the area can be operated with mouse actions. */ + area?: ActionAreaPredicate; +} export interface EditorOption extends BaseActionOption { readOnly?: RecordBoolean; } -export type ButtonActionOption = ActionOption; +export interface ButtonActionOption extends AbstractActionOption { + action?: ActionListener; +} export interface InlineMenuEditorOption extends EditorOption { classList?: string | string[]; diff --git a/packages/cheetah-grid/src/js/ts-types/column/style.ts b/packages/cheetah-grid/src/js/ts-types/column/style.ts index f5716d6c9..dc71c41bc 100644 --- a/packages/cheetah-grid/src/js/ts-types/column/style.ts +++ b/packages/cheetah-grid/src/js/ts-types/column/style.ts @@ -3,6 +3,8 @@ import type { IndicatorObject, LineClamp, TextOverflow, + TreeBranchIconStyle, + TreeLineStyle, Visibility, } from "../define"; import type { ColorDef } from "../base"; @@ -106,6 +108,13 @@ export interface PercentCompleteBarStyleOption extends StyleOption { barBgColor?: ColorDef; barHeight?: number; } +export interface TreeStyleOption extends StyleOption { + lineStyle?: TreeLineStyle; + lineColor?: ColorDef; + lineWidth?: number; + branchIcon?: TreeBranchIconStyle; + openedBranchIcon?: TreeBranchIconStyle; +} export interface SortHeaderStyleOption extends MultilineTextHeaderStyleOption { sortArrowColor?: ColorDef; diff --git a/packages/cheetah-grid/src/js/ts-types/column/type.ts b/packages/cheetah-grid/src/js/ts-types/column/type.ts index 101282690..b6414334c 100644 --- a/packages/cheetah-grid/src/js/ts-types/column/type.ts +++ b/packages/cheetah-grid/src/js/ts-types/column/type.ts @@ -1,4 +1,4 @@ -import type { ColumnMenuItemOptions } from "../define"; +import type { ColumnIconOption, ColumnMenuItemOptions } from "../define"; export interface BaseColumnOption { fadeinWhenCallbackInPromise?: boolean | null; @@ -31,37 +31,65 @@ export interface BranchGraphColumnOption extends BaseColumnOption { cache?: boolean; } -export type SimpleBranchGraphCommand = - | { - command: "branch"; - branch: - | string - | { - from: string; - to: string; - }; - } - | { - command: "commit"; - branch: string; - } - | { - command: "merge"; - branch: { +/** Branches from the branch specified by `branch.from` to the branch specified by `branch.to`. */ +export type BranchGraphCommandBranch = { + command: "branch"; + branch: + | string + | { from: string; to: string; }; - } - | { - command: "tag"; - branch: string; - tag: string; - }; +}; +/** Commit the branch specified by `branch`. */ +export type BranchGraphCommandCommit = { + command: "commit"; + branch: string; +}; +/** Merge the branch specified by `branch.from` into the branch specified by `branch.to`. */ +export type BranchGraphCommandMerge = { + command: "merge"; + branch: { + from: string; + to: string; + }; +}; +/** Creates a tag specified by `tag` from the branch specified by `branch`. */ +export type BranchGraphCommandTag = { + command: "tag"; + branch: string; + tag: string; +}; +/** The value to supply to the BranchGraphColumn. */ export type BranchGraphCommand = - | SimpleBranchGraphCommand + | BranchGraphCommandBranch + | BranchGraphCommandCommit + | BranchGraphCommandMerge + | BranchGraphCommandTag; + +export type BranchGraphCommandValue = + | BranchGraphCommand | undefined | null - | SimpleBranchGraphCommand[]; + | BranchGraphCommand[]; + +/** The value to supply to the TreeColumn. */ +export type TreeData = { + /** The caption of the record */ + caption?: string; + /** An array of path indicating the hierarchy */ + path: unknown[] | (() => unknown[]); + + nodeType?: "leaf" | "branch"; + /** The icon you want to display on the tree. */ + icon?: ColumnIconOption; +}; + +export type TreeDataValue = TreeData | unknown[] | undefined | null; + +export interface TreeColumnOption extends BaseColumnOption { + cache?: boolean; +} export type ColumnTypeOption = | "DEFAULT" diff --git a/packages/cheetah-grid/src/js/ts-types/define.ts b/packages/cheetah-grid/src/js/ts-types/define.ts index 138d16c15..d02ae08d5 100644 --- a/packages/cheetah-grid/src/js/ts-types/define.ts +++ b/packages/cheetah-grid/src/js/ts-types/define.ts @@ -119,3 +119,8 @@ export type IndicatorObject = { size?: number | string; }; export type IndicatorDefine = IndicatorObject | IndicatorStyle; + +// ****** TreeStyle Options ******* + +export type TreeLineStyle = "none" | "solid"; +export type TreeBranchIconStyle = "chevron_right" | "expand_more" | "none"; diff --git a/packages/cheetah-grid/src/js/ts-types/events.ts b/packages/cheetah-grid/src/js/ts-types/events.ts index e215d473a..5b9c71cf1 100644 --- a/packages/cheetah-grid/src/js/ts-types/events.ts +++ b/packages/cheetah-grid/src/js/ts-types/events.ts @@ -62,7 +62,10 @@ export type ModifyStatusEditableinputCellEvent = CellAddress & { input: HTMLInputElement; }; -export type MousePointerCellEvent = CellAddress & { related?: CellAddress }; +export type MousePointerCellEvent = CellAddress & { + related?: CellAddress; + event: Pick; +}; export interface DrawGridEventHandlersEventMap { selected_cell: [SelectedCellEvent, boolean]; diff --git a/packages/cheetah-grid/src/js/ts-types/grid-engine.ts b/packages/cheetah-grid/src/js/ts-types/grid-engine.ts index af0ddc5e2..b7626639b 100644 --- a/packages/cheetah-grid/src/js/ts-types/grid-engine.ts +++ b/packages/cheetah-grid/src/js/ts-types/grid-engine.ts @@ -312,7 +312,7 @@ export interface GridCanvasHelperAPI { } ): void; multilineText( - multilines: string[], + lines: string[], context: CellContext, option: { padding?: number | string | (number | string)[]; diff --git a/packages/cheetah-grid/src/js/ts-types/plugin.ts b/packages/cheetah-grid/src/js/ts-types/plugin.ts index bdd5c7c59..dbfb6c6a6 100644 --- a/packages/cheetah-grid/src/js/ts-types/plugin.ts +++ b/packages/cheetah-grid/src/js/ts-types/plugin.ts @@ -1,4 +1,9 @@ -import type { ColorPropertyDefine, ColorsPropertyDefine } from "./define"; +import type { + ColorPropertyDefine, + ColorsPropertyDefine, + TreeBranchIconStyle, + TreeLineStyle, +} from "./define"; // ****** Plugin Icons ******* export interface IconDefine { @@ -41,6 +46,13 @@ export interface ThemeDefine { color?: ColorPropertyDefine; bgColor?: ColorPropertyDefine; }; + tree: { + lineStyle?: TreeLineStyle; + lineColor?: ColorPropertyDefine; + lineWidth?: number; + branchIcon?: TreeBranchIconStyle; + openedBranchIcon?: TreeBranchIconStyle; + }; header: { sortArrowColor?: ColorPropertyDefine; }; @@ -67,6 +79,7 @@ export type RequiredThemeDefine = Required & { checkbox: Required; radioButton: Required; button: Required; + tree: Required; header: Required; messages: Required; indicators: Required; diff --git a/packages/cheetah-grid/src/test/ListGrid_sample_for_tree.html b/packages/cheetah-grid/src/test/ListGrid_sample_for_tree.html new file mode 100644 index 000000000..895d49023 --- /dev/null +++ b/packages/cheetah-grid/src/test/ListGrid_sample_for_tree.html @@ -0,0 +1,28 @@ + + + + + Grid with Tree + + + + + + +

ListGrid with Tree Example

+
+
+
+ + + + \ No newline at end of file diff --git a/packages/cheetah-grid/src/test/ListGrid_sample_for_tree.js b/packages/cheetah-grid/src/test/ListGrid_sample_for_tree.js new file mode 100644 index 000000000..9db15a5ad --- /dev/null +++ b/packages/cheetah-grid/src/test/ListGrid_sample_for_tree.js @@ -0,0 +1,174 @@ +/*global cheetahGrid*/ +/*eslint object-shorthand:0, prefer-arrow-callback:0, prefer-template: "off"*/ +'use strict'; + +(function() { + window.cheetahGrid = cheetahGrid; + const columnType = cheetahGrid.columns.type; + const columnAction = cheetahGrid.columns.action; + const columnStyle = cheetahGrid.columns.style; + + const expands = {'p1': true}; + + const tree = [ + { + code: 'p1', + children: [ + { + code: 'c1_1', + children: [ + { + code: 'd1_1_1' + }, + { + code: 'd1_1_2', + children: [ + { + code: 'e1_1_2_1' + }, + { + code: 'e1_1_2_2' + } + ] + } + ] + }, + { + code: 'c1_2' + } + ] + }, + { + code: 'p2', + children: [ + { + code: 'c2_1' + }, + { + code: 'c2_2' + } + ] + } + ]; + + // Set the parent property to the parent node so that we can keep track of the parent node. + const buffer = [...tree]; + while (buffer.length) { + const node = buffer.shift(); + for (const child of node.children || []) { + child.parent = node; + buffer.push(child); + } + } + + const treeColumn = new columnType.TreeColumn( + { + // cache: true + } + ); + const treeStyle = new columnStyle.TreeStyle( + { + // textBaseline: 'top', + padding: [5, 0, 0, 10], + // lineColor: '#aaa', + // lineWidth: 3, + // lineStyle: 'none' + } + ); + + const grid = new cheetahGrid.ListGrid({ + parentElement: document.querySelector('#parent'), + allowRangePaste: true, + header: [ + { + field: (node) => { + // Build tree data + const hasChildren = !!node.children?.length; + + return { + caption: node.code, + /** + * Returns an array of paths indicating the hierarchy to the record. + * The path must contain an element that identifies the node itself. + */ + path() { + return [...ancestors()]. + reverse(). + map((node) => node.code); + + function *ancestors() { + let n = node; + while (n) { + yield n; + n = n.parent; + } + } + }, + nodeType: hasChildren ? 'branch' : 'leaf', + }; + }, + caption: 'Tree', + width: 200, + columnType: treeColumn, + style: treeStyle, + action: new columnAction.Action({ + disabled: (node) => { + const hasChildren = !!node.children?.length; + return !hasChildren; + }, + action: (e) => { + expands[e.code] = !expands[e.code]; + grid.records = buildRecords(tree); + }, + area: treeColumn.drawnIconActionArea + }) + }, + { + field: 'code', + caption: 'Code', + width: 150, + }, + ], + frozenColCount: 1, + }); + + function buildRecords(nodes) { + const records = []; + for (const node of nodes) { + records.push(node); + if (expands[node.code]) { + records.push(...buildRecords(node.children)); + } + } + return records; + } + + + grid.records = buildRecords(tree); + window.grid = grid; + + const toolsRoot = document.querySelector('#tools'); + + const label = document.createElement('label'); + const showIconCheck = document.createElement('input'); + showIconCheck.type = 'checkbox'; + showIconCheck.checked = treeStyle.branchIcon !== 'none'; + label.appendChild(showIconCheck); + label.appendChild(document.createTextNode('Show Icons')); + toolsRoot.appendChild(label); + showIconCheck.addEventListener('change', () => { + treeStyle.branchIcon = showIconCheck.checked ? 'chevron_right' : 'none'; + treeStyle.openedBranchIcon = showIconCheck.checked ? 'expand_more' : 'none'; + grid.invalidate(); + }); + + const showLineCheck = document.createElement('input'); + showLineCheck.type = 'checkbox'; + showLineCheck.checked = treeStyle.lineStyle !== 'none'; + label.appendChild(showLineCheck); + label.appendChild(document.createTextNode('Show Lines')); + toolsRoot.appendChild(label); + showLineCheck.addEventListener('change', () => { + treeStyle.lineStyle = showLineCheck.checked ? 'solid' : 'none'; + }); +})(); \ No newline at end of file diff --git a/packages/docs/api/js/column_actions/Action.md b/packages/docs/api/js/column_actions/Action.md new file mode 100644 index 000000000..679e38852 --- /dev/null +++ b/packages/docs/api/js/column_actions/Action.md @@ -0,0 +1,30 @@ +--- +order: 700 +--- + +# Action + +Define the behavior when the cell is clicked. + +The specified method is executed after select the cell by clicking it and then push Enter. + +You can control the property of `disabled` and `action` by manipulating the instance of `ButtonAction` class. +You can also disable each record by specifying a function for the `disabled` property. + +## Constructor Properties + +| Property | Description | +| ------------------- | ------------------------------------------------------------------------------------ | +| `action` (Required) | Defines the action to be taken when clicking or pressing the Enter or Space key. | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `area` | Defines an actionable area within a cell. Set this property to a predicate. | + +[the standard properties]: ./standard-properties.md + +## Properties + +| Property | Description | +| ---------- | ------------------------------------------------------------------------------------ | +| `action` | Defines the action to be taken when clicking or pressing the Enter or Space key. | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `area` | Defines an actionable area within a cell. Set this property to a predicate. | diff --git a/packages/docs/api/js/column_actions/ButtonAction.md b/packages/docs/api/js/column_actions/ButtonAction.md index 14868d5c4..1febf1dee 100644 --- a/packages/docs/api/js/column_actions/ButtonAction.md +++ b/packages/docs/api/js/column_actions/ButtonAction.md @@ -11,6 +11,24 @@ The specified method is executed after select the cell by clicking it and then p You can control the property of `disabled` and `action` by manipulating the instance of `ButtonAction` class. You can also disable each record by specifying a function for the `disabled` property. +## Constructor Properties + +| Property | Description | +| ------------------- | ------------------------------------------------------------------------------------ | +| `action` (Required) | Defines the action to be taken when clicking or pressing the Enter or Space key. | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | + +[the standard properties]: ./standard-properties.md + +## Properties + +| Property | Description | +| ---------- | ------------------------------------------------------------------------------------ | +| `action` | Defines the action to be taken when clicking or pressing the Enter or Space key. | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | + +## Example + ```html diff --git a/packages/docs/api/js/column_actions/CheckEditor.md b/packages/docs/api/js/column_actions/CheckEditor.md index e0b86f94f..0b577c08b 100644 --- a/packages/docs/api/js/column_actions/CheckEditor.md +++ b/packages/docs/api/js/column_actions/CheckEditor.md @@ -12,6 +12,8 @@ You can control the property of `readOnly` and `disabled` by setting the instanc But if you define `'check'`, as string, to `action` of the column, you can't control these properties. You can also disable or read-only each record by specifying a function for the `disabled` and `readOnly` properties. +## Example + ```html @@ -207,7 +209,23 @@ grid.records = [ -## disabled +## Constructor Properties + +| Property | Description | +| ---------- | ------------------------------------------------------------------------------------- | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `readOnly` | Define a boolean or predicate to control readonly. See also [the standard properties] | + +[the standard properties]: ./standard-properties.md + +## Properties + +| Property | Description | +| ---------- | ------------------------------------------------------------------------------------- | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `readOnly` | Define a boolean or predicate to control readonly. See also [the standard properties] | + +### disabled You can control `disabled` depending on the state of the record by giving `disabled` a `function`. diff --git a/packages/docs/api/js/column_actions/Classes.md b/packages/docs/api/js/column_actions/Classes.md index f15a241e8..318f50313 100644 --- a/packages/docs/api/js/column_actions/Classes.md +++ b/packages/docs/api/js/column_actions/Classes.md @@ -14,6 +14,7 @@ order: 100 | [`InlineInputEditor`] | The behavior when input the cell | InlineInputEditor is an experiment stage | | [`InlineMenuEditor`] | The behavior when select menu the cell | --- | | [`RadioEditor`] | The behavior when clicking the radio button | same as `action: 'radio'` | +| [`Action`] | The behavior when clicking the cell | --- | [`checkeditor`]: ./CheckEditor.md [`buttonaction`]: ./ButtonAction.md @@ -21,5 +22,8 @@ order: 100 [`inlineinputeditor`]: ./InlineInputEditor.md [`inlinemenueditor`]: ./InlineMenuEditor.md [`radioeditor`]: ./RadioEditor.md +[`action`]: ./Action.md ## Standard Properties + +Please see [here](./standard-properties.md). diff --git a/packages/docs/api/js/column_actions/InlineInputEditor.md b/packages/docs/api/js/column_actions/InlineInputEditor.md index a1b28da31..d47c30d4c 100644 --- a/packages/docs/api/js/column_actions/InlineInputEditor.md +++ b/packages/docs/api/js/column_actions/InlineInputEditor.md @@ -6,6 +6,19 @@ order: 450 Enables data editing by input. +## Constructor Properties + +| Property | Description | +| ----------- | ------------------------------------------------------------------------------------- | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `readOnly` | Define a boolean or predicate to control readonly. See also [the standard properties] | +| `classList` | Defines the `class` to be set on the ``. | +| `type` | Defines the `type` to be set on the ``. | + +[the standard properties]: ./standard-properties.md + +## Example + ```html @@ -83,10 +96,12 @@ document.querySelector(".sample1mode").onchange = function () { ## Properties -| Property | Description | -| ----------- | ------------------------------------------------------ | -| `type` | Specify the `type` attribute of the `` element. | -| `classList` | Specify `class` of the `` element. | +| Property | Description | +| ----------- | ------------------------------------------------------------------------------------- | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `readOnly` | Define a boolean or predicate to control readonly. See also [the standard properties] | +| `type` | Defines the `type` attribute of the `` element. | +| `classList` | Defines `class` of the `` element. | diff --git a/packages/docs/api/js/column_actions/InlineMenuEditor.md b/packages/docs/api/js/column_actions/InlineMenuEditor.md index e83f4707a..1b3649639 100644 --- a/packages/docs/api/js/column_actions/InlineMenuEditor.md +++ b/packages/docs/api/js/column_actions/InlineMenuEditor.md @@ -6,6 +6,28 @@ order: 500 Enables data editing by menu selection. +## Constructor Properties + +| Property | Description | +| -------------------- | ------------------------------------------------------------------------------------- | +| `options` (Required) | Defines the options that can be selected. | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `readOnly` | Define a boolean or predicate to control readonly. See also [the standard properties] | +| `classList` | Defines the `class` to be set on the menu (`
    `). | + +[the standard properties]: ./standard-properties.md + +## Properties + +| Property | Description | +| -------------------- | ------------------------------------------------------------------------------------- | +| `options` (Required) | Defines the options that can be selected. | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `readOnly` | Define a boolean or predicate to control readonly. See also [the standard properties] | +| `classList` | Defines the `class` to be set on the menu (`
      `). | + +## Example + ```html diff --git a/packages/docs/api/js/column_actions/RadioEditor.md b/packages/docs/api/js/column_actions/RadioEditor.md index 7bb8434da..9f44c572a 100644 --- a/packages/docs/api/js/column_actions/RadioEditor.md +++ b/packages/docs/api/js/column_actions/RadioEditor.md @@ -12,6 +12,8 @@ You can control the property of `readOnly` and `disabled` by setting the instanc But if you define `'radio'`, as string, to `action` of the column, you can't control these properties. You can also disable or read-only each record by specifying a function for the `disabled` and `readOnly` properties. +## Example + ```html @@ -90,7 +92,25 @@ document.querySelector(".sample1mode").onchange = function () { -## disabled +## Constructor Properties + +| Property | Description | +| ------------- | ------------------------------------------------------------------------------------- | +| `checkAction` | Defines the action to be taken when clicking or pressing the Enter or Space key. | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `readOnly` | Define a boolean or predicate to control readonly. See also [the standard properties] | + +[the standard properties]: ./standard-properties.md + +## Properties + +| Property | Description | +| ------------- | ------------------------------------------------------------------------------------- | +| `checkAction` | Defines the action to be taken when clicking or pressing the Enter or Space key. | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `readOnly` | Define a boolean or predicate to control readonly. See also [the standard properties] | + +### disabled You can control `disabled` depending on the state of the record by giving `disabled` a `function`. diff --git a/packages/docs/api/js/column_actions/SmallDialogInputEditor.md b/packages/docs/api/js/column_actions/SmallDialogInputEditor.md index b60f9e4ff..d0a0626bd 100644 --- a/packages/docs/api/js/column_actions/SmallDialogInputEditor.md +++ b/packages/docs/api/js/column_actions/SmallDialogInputEditor.md @@ -93,12 +93,26 @@ document.querySelector(".sample1mode").onchange = function () { -## Properties +## Constructor Properties -The following properties can be set with the constructor argument of `SmallDialogInputEditor`. +| Property | Description | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `readOnly` | Define a boolean or predicate to control readonly. See also [the standard properties] | +| `type` | Specify the `type` attribute of the `` element. | +| `classList` | Specify `class` of the dialog element. | +| `helperText` | Specify helper text. You can also specify a function. | +| `validator` | Specify the validation function to be call before confirming the input value. If there is an error, please use the function to return the message. | +| `inputValidator` | Specify the validation function of the value of ``. If there is an error, please use the function to return the message. | + +[the standard properties]: ./standard-properties.md + +## Properties | Property | Description | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | Define a boolean or predicate to control disable. See also [the standard properties] | +| `readOnly` | Define a boolean or predicate to control readonly. See also [the standard properties] | | `type` | Specify the `type` attribute of the `` element. | | `classList` | Specify `class` of the dialog element. | | `helperText` | Specify helper text. You can also specify a function. | diff --git a/packages/docs/api/js/column_types/BranchGraphColumn.md b/packages/docs/api/js/column_types/BranchGraphColumn.md index 803fdbf10..dd8d56bca 100644 --- a/packages/docs/api/js/column_types/BranchGraphColumn.md +++ b/packages/docs/api/js/column_types/BranchGraphColumn.md @@ -10,8 +10,8 @@ Show branch graph. | Property | Description | Default | | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -| `start` | Set the moving direction by setting the beggining point. `'top'` or `'bottom'` | `'bottom'` | -| `cache` | Set `true` when caching the calculation result of the branch structure. Please call `clearCache(grid)` when deleting the cahced data. | `false` | +| `start` | Set the moving direction by setting the beginning point. `'top'` or `'bottom'` | `'bottom'` | +| `cache` | Set `true` when caching the calculation result of the branch structure. Please call `clearCache(grid)` when deleting the cached data. | `false` | ## Style Properties @@ -23,6 +23,67 @@ Show branch graph. | `branchLineWidth` | Set the width of branch lines. | `4` | | `mergeStyle` | Set the way to express the merge line. `'bezier'` or `'straight'` | `'bezier'` | +## Data Format + +The value provided from each record through the field must be an object or an array of it in the following format. + +### Branch Command + +This is the command to create a new branch.\ +It is an object with the following properties: + +| Property | Description | +| :--------------------- | :------------------------------------------------------------------------------------------ | +| `command` (Required) | Sets `"branch"`, which indicates what command the object is directing. | +| `branch` | Set the value of the branch name to operate on. | +| `branch.to` (Required) | Set the name of the new branch to be created. Set `branch` to a string has the same effect. | +| `branch.from` | Set the branch name to be branched from. | + +### Commit Command + +This is the command to commit a branch.\ +It is an object with the following properties: + +| Property | Description | +| :------------------- | :--------------------------------------------------------------------- | +| `command` (Required) | Sets `"commit"`, which indicates what command the object is directing. | +| `branch` (Required) | Set the value of the branch name to operate on. | + +### Merge Command + +This is the command to merge a branch into a branch.\ +It is an object with the following properties: + +| Property | Description | +| :----------------------- | :-------------------------------------------------------------------- | +| `command` (Required) | Sets `"merge"`, which indicates what command the object is directing. | +| `branch` (Required) | Set the value of the branch name to operate on. | +| `branch.to` (Required) | Sets the name of the branch that will be merged. | +| `branch.from` (Required) | Sets the name of the branch you want to merge from. | + +### Tag Command + +This is the command to create a tag.\ +It is an object with the following properties: + +| Property | Description | +| :------------------- | :------------------------------------------------------------------ | +| `command` (Required) | Sets `"tag"`, which indicates what command the object is directing. | +| `branch` (Required) | Set the value of the branch name to operate on. | +| `tag` (Required) | Set the name of the new tag to be created. | + +## Instance Methods + +### `clearCache(grid)` + +Clear the cache. + +| Parameter | Description | +| :-------- | :---------------------------------------- | +| `grid` | It should be given an instance of a grid. | + +## Example + ```html diff --git a/packages/docs/api/js/column_types/TreeColumn.md b/packages/docs/api/js/column_types/TreeColumn.md new file mode 100644 index 000000000..d8426963b --- /dev/null +++ b/packages/docs/api/js/column_types/TreeColumn.md @@ -0,0 +1,218 @@ +--- +order: 1010 +--- + +# TreeColumn + +Show a hierarchical tree. + +## Constructor Properties + +| Property | Description | Default | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `cache` | Set `true` when caching the calculation result of the branch structure. Please call `clearCache(grid)` when deleting the cached data. | `false` | + +## Style Properties + +| Property | Description | +| ------------------ | ------------------------------------------------------------------------------------------------------------------ | +| `lineColor` | Sets the tree lines color. | +| `lineStyle` | Sets the tree lines style. Allowed values ​​are `'none'` or `'solid'` | +| `lineWidth` | Sets the with of of the tree lines. | +| `branchIcon` | Sets the icon to display for the branch node. Allowed values ​​are `"chevron_right"`, `"expand_more"` or `"none"`. | +| `openedBranchIcon` | Sets the icon to display for the opened branch node. | + +## Data Format + +The value provided from each record through the field must be an object of the format described below. + +The object has the following properties: + +| Property | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `path` (Required) | An array of path indicating the hierarchy. See [the `path` description section](#path) for more information. | +| `caption` | The caption of the record. If not specified, the last value of the path will be used as the caption. | +| `nodeType` | Set to `"leaf"` or `"branch"`. Set whether the node is a leaf node or a branch node. See [the `nodeType` description section](#nodetype) for more information. | +| `icon` | Set this if you want a custom icon to be displayed in the tree. | + +### `path` + +This is a required property.\ +This is an array of path indicating the hierarchy. The path must contain an element that identifies the node itself. + +Example: + +| Node | `code` | `path` Value | +| :---------- | :----- | :----------------- | +| Grandparent | `'g'` | `['g']` | +| ├ Parent | `'p'` | `['g', 'p']` | +| │└ Child1 | `'c1'` | `['g', 'p', 'c1']` | +| └ Child2 | `'c2'` | `['g', 'c2']` | + +### `nodeType` + +Set to `"leaf"` or `"branch"`. +Set whether the node is a leaf node (`"leaf"`) or a branch node (`"branch"`). +Use this to display an icon if the node does not display any child nodes in the display. +If the child nodes are displayed, they are forced to be treated as branch nodes. + +## Instance Methods + +### `drawnIconActionArea(params)` + +A predicate that makes the icon an actionable area of ​​[the `Action` class]. Use it for the `area` property of ​​[the `Action` class]. + +| Parameter | Description | +| :-------- | :-------------------------------------------------------------------------- | +| `params` | It's a parameter passed from the `area` property of ​​[the `Action` class]. | + +[the `action` class]: ../column_actions/Action.md + +### `clearCache(grid)` + +Clear the cache. + +| Parameter | Description | +| :-------- | :---------------------------------------- | +| `grid` | It should be given an instance of a grid. | + +## Example + + + +```html +
      +``` + +```js +const expands = { p1: true }; + +const tree = [ + { + code: "p1", + children: [ + { + code: "c1_1", + children: [ + { + code: "d1_1_1", + }, + { + code: "d1_1_2", + children: [ + { + code: "e1_1_2_1", + }, + { + code: "e1_1_2_2", + }, + ], + }, + ], + }, + { + code: "c1_2", + }, + ], + }, + { + code: "p2", + children: [ + { + code: "c2_1", + }, + { + code: "c2_2", + }, + ], + }, +]; + +// Set the parent property to the parent node so that we can keep track of the parent node. +const buffer = [...tree]; +while (buffer.length) { + const node = buffer.shift(); + for (const child of node.children || []) { + child.parent = node; + buffer.push(child); + } +} + +/** Gets the specified node and its ancestor nodes. */ +function* ancestors(node) { + let n = node; + while (n) { + yield n; + n = n.parent; + } +} + +const treeColumn = new cheetahGrid.columns.type.TreeColumn({ + cache: false, // cache enable. default false +}); +const grid = new cheetahGrid.ListGrid({ + parentElement: document.querySelector(".sample1"), + header: [ + { + field: (node) => { + // Build tree data + const hasChildren = !!node.children?.length; + + const path = [...ancestors(node)].reverse().map((node) => node.code); + + return { + caption: node.code, + /** + * An array of paths indicating the hierarchy to the record. + * The path must contain an element that identifies the node itself. + */ + path, + nodeType: hasChildren ? "branch" : "leaf", + }; + }, + caption: "Tree", + width: 200, + columnType: treeColumn, + action: new cheetahGrid.columns.action.Action({ + disabled: (node) => { + const hasChildren = !!node.children?.length; + return !hasChildren; + }, + action: (node) => { + expands[node.code] = !expands[node.code]; + grid.records = buildRecords(tree); + }, + area: treeColumn.drawnIconActionArea, + }), + }, + { + field: "code", + caption: "Code", + width: 200, + }, + { + field: (node) => { + const path = [...ancestors(node)].reverse().map((node) => node.code); + return "[" + path.join(", ") + "]"; + }, + caption: "Path", + width: 300, + }, + ], + frozenColCount: 1, +}); +grid.records = buildRecords(tree); + +function buildRecords(nodes) { + const records = []; + for (const node of nodes) { + records.push(node); + if (expands[node.code]) { + records.push(...buildRecords(node.children)); + } + } + return records; +} +``` + +