diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000000..c3b3d43a71
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,76 @@
+# GitHub Copilot Instructions for slickgrid-universal
+
+## Repository Overview
+This is a monorepo for SlickGrid Universal containing:
+- Core packages in `packages/` (common, plugins, exports, etc.)
+- Framework wrappers in `frameworks/` (Angular, React, Vue, Aurelia)
+- Demo applications in `demos/` for each framework
+ - except Angular demos which are located in `frameworks/angular-slickgrid/src/demos`
+- Shared test utilities in `test/`
+
+## Code Standards
+
+### Testing
+- Use **Vitest** for unit tests (`.spec.ts` files)
+- Use **Cypress** for E2E tests (`.cy.ts` files in `test/cypress/e2e/`)
+- Aim for 100% statement coverage on core functionality
+- Test files are usually in `__tests__/` subdirectory (or same folder if only 1-2 test files)
+- Unit tests exist in 2 implementations:
+ - Native/vanilla implementation: all under `packages/`
+ - Angular-specific tests: under `frameworks/angular-slickgrid/`
+
+### TypeScript Patterns
+- Use strict TypeScript with proper typing
+- Prefer `interface` over `type` for object shapes
+- Use `protected` for class methods that may be extended
+- Follow existing naming conventions: `_privateField`, `publicMethod`
+
+### Code Formatting & Linting
+- **OXLint** is used for linting TypeScript/JavaScript code
+- **Prettier** handles code formatting automatically
+- Code should be formatted before committing
+- Run `pnpm lint:fix` to auto-fix linting issues
+- Run `pnpm prettier:write` to format code
+- Ensure no linting errors before creating pull requests
+
+### Plugin Development
+- All plugins extend SlickGrid and use `SlickEventHandler`
+- Use `BindingEventService` for DOM event binding/cleanup
+- Always implement `init()`, `dispose()`, and `getOptions()`/`setOptions()`
+- Plugins should support both grid options and column definition options
+
+### Framework Support
+When making changes to demos or documentation:
+- Update **all 4 frameworks**: Angular, React, Vue, Aurelia
+- Maintain consistency across framework implementations
+- Check for framework-specific syntax (e.g., Vue's template vs React's JSX)
+
+### Documentation
+- Update corresponding docs in `docs/` AND framework-specific `frameworks/*/docs/`
+- Use markdown with proper linking: `[file.ts](path/to/file.ts)`
+- Include code examples that work across frameworks
+
+## Common Commands
+- `pnpm build` - Build all packages
+ - aka "Build Everything" task, which builds all packages and frameworks in the monorepo
+- `pnpm lint` - Run linter (OXLint) on all files
+- `pnpm lint:fix` - Auto-fix linting issues
+- `pnpm prettier:check` - Check code formatting
+- `pnpm prettier:write` - Format code with Prettier
+- `pnpm test` - Run Vitest unit tests
+- `pnpm test:coverage` - Run tests with coverage
+- `pnpm dev` - Start vanilla demo
+- `pnpm dev:[framework]` - Start framework-specific demos
+ - e.g.: `dev:angular`, `dev:react`, `dev:vue`, `dev:aurelia`
+
+## Monorepo Structure
+- Changes to `packages/*` affect all frameworks
+- Framework wrappers depend on core packages
+- Use relative imports within packages
+- Avoid circular dependencies
+
+## Code Review Focus
+- Maintain backward compatibility
+- Consider impact across all 4 framework implementations
+- Verify tests pass and coverage remains high
+- Check that examples work in all framework demos
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 6525d632b6..a656074633 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -25,20 +25,6 @@
enabled: false,
},
},
- {
- description: 'Prevent major updates for Vite 6 catalog',
- matchDepTypes: ['pnpm.catalog.vite6'],
- major: {
- enabled: false,
- },
- },
- {
- description: 'Ignore Analog packages',
- matchPackagePatterns: ['^@analogjs/'],
- groupName: 'Analog packages',
- allowedVersions: '2.2.2',
- enabled: false, // This will completely disable updates for Analog packages
- },
],
ignoreDeps: ['lee-dohm/no-response', 'node', 'pnpm', 'typescript'],
schedule: ['every 4 weeks on friday'],
diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml
index 50ce2852df..0c2a443cde 100644
--- a/.github/workflows/pkg-pr-new.yml
+++ b/.github/workflows/pkg-pr-new.yml
@@ -28,4 +28,4 @@ jobs:
run: pnpm build
- name: Publish
- run: pnpm dlx pkg-pr-new publish --no-template --pnpm './packages/*' './frameworks/*'
+ run: pnpm dlx pkg-pr-new publish --no-template --pnpm './packages/*' './frameworks/*' './frameworks-plugins/*'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index dc09f4b8e0..674f9acbd8 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,9 +6,6 @@ on:
dryrun:
type: boolean
description: Dry-Run
- graduate:
- type: boolean
- description: Force Conventional Graduate
tag:
type: choice
default: latest
@@ -73,20 +70,19 @@ jobs:
- name: Lerna Version (build query)
shell: bash
run: |
- if ${{inputs.dryrun == true && inputs.graduate != true}}
+ if ${{inputs.dryrun == true && (inputs.tag == 'alpha' || inputs.tag == 'beta' || inputs.tag == 'next')}}
+ then
+ echo "LERNA_VERSION_TYPE=๐งช Dry-Run w/Prerelease" >> $GITHUB_ENV
+ echo "LERNA_VERSION_QUERY=lerna version --yes --conventional-prerelease --preid ${{ inputs.tag }} --dry-run" >> $GITHUB_ENV
+ elif ${{inputs.dryrun == true}}
then
echo "LERNA_VERSION_TYPE=๐งช Dry-Run" >> $GITHUB_ENV
echo "LERNA_VERSION_QUERY=lerna version --yes --dry-run" >> $GITHUB_ENV
- elif ${{inputs.dryrun == true && inputs.graduate == true}}
- then
- echo "LERNA_VERSION_TYPE=๐งช Dry-Run w/Graduate" >> $GITHUB_ENV
- echo "LERNA_VERSION_QUERY=lerna version --yes --dry-run --conventional-graduate" >> $GITHUB_ENV
- elif ${{inputs.dryrun != true && inputs.graduate == true}}
- then
- echo "LERNA_VERSION_TYPE=๐ Prod Version w/Graduate" >> $GITHUB_ENV
- echo "LERNA_VERSION_QUERY=lerna version --yes --conventional-graduate" >> $GITHUB_ENV
- elif ${{inputs.dryrun != true && inputs.graduate != true}}
+ elif ${{inputs.tag == 'alpha' || inputs.tag == 'beta' || inputs.tag == 'next'}}
then
+ echo "LERNA_VERSION_TYPE=๐ Prod Version w/Prerelease" >> $GITHUB_ENV
+ echo "LERNA_VERSION_QUERY=lerna version --yes --conventional-prerelease --preid ${{ inputs.tag }}" >> $GITHUB_ENV
+ else
echo "LERNA_VERSION_TYPE=๐ Prod Version" >> $GITHUB_ENV
echo "LERNA_VERSION_QUERY=lerna version --yes" >> $GITHUB_ENV
fi
@@ -104,13 +100,36 @@ jobs:
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
pnpm exec ${{ env.LERNA_VERSION_QUERY }}
+ - name: Lerna Publish (build query)
+ shell: bash
+ run: |
+ if ${{inputs.dryrun == true && (inputs.tag == 'alpha' || inputs.tag == 'beta' || inputs.tag == 'next')}}
+ then
+ echo "LERNA_PUBLISH_TYPE=๐งช Dry-Run w/Prerelease" >> $GITHUB_ENV
+ echo "LERNA_PUBLISH_QUERY=lerna publish from-package --force-publish --yes --pre-dist-tag ${{ inputs.tag }} --dry-run" >> $GITHUB_ENV
+ elif ${{inputs.dryrun == true}}
+ then
+ echo "LERNA_PUBLISH_TYPE=๐งช Dry-Run" >> $GITHUB_ENV
+ echo "LERNA_PUBLISH_QUERY=lerna publish from-package --force-publish --yes --dist-tag latest --dry-run" >> $GITHUB_ENV
+ elif [[ "${{ inputs.tag }}" == "alpha" || "${{ inputs.tag }}" == "beta" || "${{ inputs.tag }}" == "next" ]]
+ then
+ echo "LERNA_PUBLISH_TYPE=๐ฆ Publish w/Prerelease" >> $GITHUB_ENV
+ echo "LERNA_PUBLISH_QUERY=lerna publish from-package --force-publish --yes --pre-dist-tag ${{ inputs.tag }}" >> $GITHUB_ENV
+ else
+ echo "LERNA_PUBLISH_TYPE=๐ฆ Publish Latest" >> $GITHUB_ENV
+ echo "LERNA_PUBLISH_QUERY=lerna publish from-package --force-publish --yes --dist-tag latest" >> $GITHUB_ENV
+ fi
+
+ - name: Final publish type โ ${{ env.LERNA_PUBLISH_TYPE }}
+ shell: bash
+ run: echo "${{ env.LERNA_PUBLISH_QUERY }}"
+
- name: Lerna Publish ๐ฆ
- if: ${{ inputs.dryrun != true }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
id: lerna-publish
- run: pnpm exec lerna publish from-package --force-publish --yes --dist-tag ${{ inputs.tag || 'latest' }}
+ run: pnpm exec ${{ env.LERNA_PUBLISH_QUERY }}
- name: Website Dev Build
run: pnpm build:dev
diff --git a/.github/workflows/test-angular.yml b/.github/workflows/test-angular.yml
index efee84125a..fdedfc9c1d 100644
--- a/.github/workflows/test-angular.yml
+++ b/.github/workflows/test-angular.yml
@@ -70,6 +70,9 @@ jobs:
- name: Angular-Slickgrid Framework Build
run: pnpm angular:build:framework
+ - name: Angular-Slickgrid Framework-Plugins Build
+ run: pnpm angular:build:framework-plugins
+
- name: Website Dev Build (served for Cypress)
run: pnpm angular:build:demo
@@ -80,7 +83,7 @@ jobs:
run: pnpm angular:serve &
- name: Run Cypress E2E tests
- uses: cypress-io/github-action@v6
+ uses: cypress-io/github-action@v7
with:
install: false
package-manager-cache: false
diff --git a/.github/workflows/test-aurelia.yml b/.github/workflows/test-aurelia.yml
index b7089615bd..f8d83ee847 100644
--- a/.github/workflows/test-aurelia.yml
+++ b/.github/workflows/test-aurelia.yml
@@ -55,6 +55,9 @@ jobs:
- name: Aurelia-Slickgrid Framework Build
run: pnpm aurelia:build:framework
+ - name: Aurelia-Slickgrid Framework-Plugins Build
+ run: pnpm aurelia:build:framework-plugins
+
- name: Website Dev Build (served for Cypress)
run: pnpm aurelia:build:demo
@@ -65,7 +68,7 @@ jobs:
run: pnpm aurelia:serve &
- name: Run Cypress E2E tests
- uses: cypress-io/github-action@v6
+ uses: cypress-io/github-action@v7
with:
install: false
package-manager-cache: false
diff --git a/.github/workflows/test-react.yml b/.github/workflows/test-react.yml
index e7e3565f4b..3838b22532 100644
--- a/.github/workflows/test-react.yml
+++ b/.github/workflows/test-react.yml
@@ -55,6 +55,9 @@ jobs:
- name: Slickgrid-React Framework Build
run: pnpm react:build:framework
+ - name: Slickgrid-React Framework-Plugins Build
+ run: pnpm react:build:framework-plugins
+
- name: Website Dev Build (served for Cypress)
run: pnpm react:build:demo
@@ -65,7 +68,7 @@ jobs:
run: pnpm react:serve &
- name: Run Cypress E2E tests
- uses: cypress-io/github-action@v6
+ uses: cypress-io/github-action@v7
with:
install: false
package-manager-cache: false
diff --git a/.github/workflows/test-vanilla.yml b/.github/workflows/test-vanilla.yml
index 12993c2b81..4cb1fa2d2f 100644
--- a/.github/workflows/test-vanilla.yml
+++ b/.github/workflows/test-vanilla.yml
@@ -62,7 +62,7 @@ jobs:
run: pnpm vanilla:serve:demo &
- name: Run Cypress E2E tests
- uses: cypress-io/github-action@v6
+ uses: cypress-io/github-action@v7
with:
install: false
package-manager-cache: false
diff --git a/.github/workflows/test-vue.yml b/.github/workflows/test-vue.yml
index 628a06e269..237b038b44 100644
--- a/.github/workflows/test-vue.yml
+++ b/.github/workflows/test-vue.yml
@@ -55,6 +55,9 @@ jobs:
- name: Slickgrid-Vue Framework Build
run: pnpm vue:build:framework
+ - name: Slickgrid-Vue Framework-Plugins Build
+ run: pnpm vue:build:framework-plugins
+
- name: Website Dev Build (served for Cypress)
run: pnpm vue:build:demo
@@ -65,7 +68,7 @@ jobs:
run: pnpm vue:serve &
- name: Run Cypress E2E tests
- uses: cypress-io/github-action@v6
+ uses: cypress-io/github-action@v7
with:
install: false
package-manager-cache: false
diff --git a/.prettierignore b/.prettierignore
index a61667e120..8f569d1b18 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,4 +2,6 @@
_variables.scss
_variables-theme-material.scss
_variables-theme-salesforce.scss
-**/test/vitest-coverage/*.*
\ No newline at end of file
+**/test/vitest-coverage/*.*
+demos/vue/src/router/index.ts
+demos/react/src/examples/slickgrid/App.tsx
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9a4f7944a7..57b4ac2042 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,5 +7,7 @@
],
"typescript.format.semicolons": "insert",
"vitest.rootConfig": "./test/vitest.config.mts",
- "typescript.tsdk": "node_modules\\typescript\\lib"
+ "typescript.tsdk": "node_modules\\typescript\\lib",
+ "prettier.prettierPath": "node_modules/prettier/index.cjs",
+ "prettier.configPath": ".prettierrc"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff21c45f1d..a9b3aac12f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,58 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [10.0.0-beta.0](https://github.com/ghiscoding/slickgrid-universal/compare/v9.12.0...v10.0.0-beta.0) (2026-02-14)
+
+### โ BREAKING CHANGES
+
+* **angular:** migrate Angular-Slickgrid to Zoneless (#2341)
+* **angular:** migrate Angular-Slickgrid to Standalone Component (#2339)
+* upgrade to Angular 21 (#2338)
+* migrate Row/Hybrid Selection flag into a single `enableSelection` flag (#2331)
+* drop Cell/Row Selection Models & keep only HybridSelectionModel (#2330)
+* remove all Deprecated code (#2302)
+* drop OperatorType enums and keep only type literal (#2301)
+* replacing multiple TypeScript `enum` with `type` to decrease build size (#2300)
+* drop FieldType enums and keep only field type literal (#2299)
+* rename `v-model:data` to `v-model:dataset` (#2298)
+* make Row Detail plugin as optional in all framework wrappers (#2291)
+* rewrite Grid Menu using CSS flexbox instead of width and calc() (#2282)
+* switch to column `hidden` property and always keep all columns (#2281)
+
+### Features
+
+* add custom menu slot renderers ([#2375](https://github.com/ghiscoding/slickgrid-universal/issues/2375)) ([7ebbda5](https://github.com/ghiscoding/slickgrid-universal/commit/7ebbda58233bb5ce94b81b5b2b578af0ea6a068d)) - by @ghiscoding
+* **angular:** migrate Angular-Slickgrid to Standalone Component ([#2339](https://github.com/ghiscoding/slickgrid-universal/issues/2339)) ([89e8e26](https://github.com/ghiscoding/slickgrid-universal/commit/89e8e261af4a747b9bfd274d3df0953b231edb97)) - by @ghiscoding
+* **angular:** migrate Angular-Slickgrid to Zoneless ([#2341](https://github.com/ghiscoding/slickgrid-universal/issues/2341)) ([f55cbe2](https://github.com/ghiscoding/slickgrid-universal/commit/f55cbe28ce6999fb3933bc55372c22a84df99a15)) - by @ghiscoding
+* auto-enabled external resources with their associated flags ([#2362](https://github.com/ghiscoding/slickgrid-universal/issues/2362)) ([16dd8a7](https://github.com/ghiscoding/slickgrid-universal/commit/16dd8a77dd5d136a5a99321f0fc4c50571fdb0c0)) - by @ghiscoding
+* bind css classes to slickgrid container ([#2369](https://github.com/ghiscoding/slickgrid-universal/issues/2369)) ([a7cd4aa](https://github.com/ghiscoding/slickgrid-universal/commit/a7cd4aa6af3688ebf1ec09140856ea451a6789da)) - by @zewa666
+* drop Cell/Row Selection Models & keep only HybridSelectionModel ([#2330](https://github.com/ghiscoding/slickgrid-universal/issues/2330)) ([4398cf4](https://github.com/ghiscoding/slickgrid-universal/commit/4398cf42e03f6971b81db8cea4ed11138c0aa452)) - by @ghiscoding
+* F2 activates cell editor, closes [#2351](https://github.com/ghiscoding/slickgrid-universal/issues/2351) ([#2370](https://github.com/ghiscoding/slickgrid-universal/issues/2370)) ([6e06f9d](https://github.com/ghiscoding/slickgrid-universal/commit/6e06f9dd3e0ff78650683b0641191b94f2a2aa96)) - by @zewa666
+* make Row Detail plugin as optional in all framework wrappers ([#2291](https://github.com/ghiscoding/slickgrid-universal/issues/2291)) ([fa1a14c](https://github.com/ghiscoding/slickgrid-universal/commit/fa1a14c16c987bfaf7725c46e8114b20ea5a505d)) - by @ghiscoding
+* migrate Row/Hybrid Selection flag into a single `enableSelection` flag ([#2331](https://github.com/ghiscoding/slickgrid-universal/issues/2331)) ([5be5e6a](https://github.com/ghiscoding/slickgrid-universal/commit/5be5e6a862ecd024cf43d404769d65c6c1dd335e)) - by @ghiscoding
+* rename `v-model:data` to `v-model:dataset` ([#2298](https://github.com/ghiscoding/slickgrid-universal/issues/2298)) ([34f42f2](https://github.com/ghiscoding/slickgrid-universal/commit/34f42f2d14c9a1b39a2695c8a885ff2bee53d0b5)) - by @ghiscoding
+* **SlickCompositeEditor:** migrate modal from div to dialog ([#2283](https://github.com/ghiscoding/slickgrid-universal/issues/2283)) ([371c2c6](https://github.com/ghiscoding/slickgrid-universal/commit/371c2c675728f51785485280237572ef5a584eeb)) - by @ghiscoding
+* switch to column `hidden` property and always keep all columns ([#2281](https://github.com/ghiscoding/slickgrid-universal/issues/2281)) ([075c649](https://github.com/ghiscoding/slickgrid-universal/commit/075c64961cb7400500df46b792866d39fba2d9e0)) - by @ghiscoding
+* **tooltip:** add global tooltip observation for non-grid elements ([#2371](https://github.com/ghiscoding/slickgrid-universal/issues/2371)) ([1bbc8de](https://github.com/ghiscoding/slickgrid-universal/commit/1bbc8de895e370843286eadd08574efc552ad8fd)) - by @ghiscoding
+* upgrade to Angular 21 ([#2338](https://github.com/ghiscoding/slickgrid-universal/issues/2338)) ([012def2](https://github.com/ghiscoding/slickgrid-universal/commit/012def265a4e5cb0738ea211d073df129a2eecd9)) - by @ghiscoding
+
+### Bug Fixes
+
+* **deps:** update all non-major dependencies ([#2292](https://github.com/ghiscoding/slickgrid-universal/issues/2292)) ([21da957](https://github.com/ghiscoding/slickgrid-universal/commit/21da95702f44739a9f1b226e305c18a1fa64000f)) - by @renovate-bot
+* **filters:** prevent tooltip triggers on Slider programmatic updates ([#2372](https://github.com/ghiscoding/slickgrid-universal/issues/2372)) ([5734d59](https://github.com/ghiscoding/slickgrid-universal/commit/5734d59a4acfd5c32571ee2f0839561966b1ee2c)) - by @ghiscoding
+* HybridSelectionModelOption should all be optional ([#2293](https://github.com/ghiscoding/slickgrid-universal/issues/2293)) ([1b8f760](https://github.com/ghiscoding/slickgrid-universal/commit/1b8f76060493c43a62e956fb9bf5e0e2feaf4a1f)) - by @ghiscoding
+* **plugin:** SlickCustomTooltip should work with parent+child tooltips ([#2374](https://github.com/ghiscoding/slickgrid-universal/issues/2374)) ([8af7f45](https://github.com/ghiscoding/slickgrid-universal/commit/8af7f45eb19f0a00da2f3de7c729504be7d043eb)) - by @ghiscoding
+* pre-set value for editor validation on paste ([#2368](https://github.com/ghiscoding/slickgrid-universal/issues/2368)) ([2df5cc5](https://github.com/ghiscoding/slickgrid-universal/commit/2df5cc5d529bff41513b05ecb322efc64d234d64)) - by @zewa666
+* remove all Deprecated code ([#2302](https://github.com/ghiscoding/slickgrid-universal/issues/2302)) ([f42c46c](https://github.com/ghiscoding/slickgrid-universal/commit/f42c46cd1f05b5c72c62f552f124b5bfe776f8b0)) - by @ghiscoding
+* **styling:** add missing row highlight assignment for Dark Mode ([6ae55de](https://github.com/ghiscoding/slickgrid-universal/commit/6ae55de071325e37123a4eb32bd57a0d00581a17)) - by @ghiscoding
+
+### Code Refactoring
+
+* drop FieldType enums and keep only field type literal ([#2299](https://github.com/ghiscoding/slickgrid-universal/issues/2299)) ([3858ed0](https://github.com/ghiscoding/slickgrid-universal/commit/3858ed06a9496ddfa94d769ce1d415b68e18c993)) - by @ghiscoding
+* drop OperatorType enums and keep only type literal ([#2301](https://github.com/ghiscoding/slickgrid-universal/issues/2301)) ([5dd0807](https://github.com/ghiscoding/slickgrid-universal/commit/5dd08079460dc9af798ab29527997a6d4b31abdd)) - by @ghiscoding
+* replacing multiple TypeScript `enum` with `type` to decrease build size ([#2300](https://github.com/ghiscoding/slickgrid-universal/issues/2300)) ([ea79395](https://github.com/ghiscoding/slickgrid-universal/commit/ea79395cf663b3abce8e43cf27ba6ffea7cfe113)) - by @ghiscoding
+* rewrite Grid Menu using CSS flexbox instead of width and calc() ([#2282](https://github.com/ghiscoding/slickgrid-universal/issues/2282)) ([2f5141a](https://github.com/ghiscoding/slickgrid-universal/commit/2f5141a12a765510e30a7155a7bf714e3462cfc1)) - by @ghiscoding
+
## [9.13.0](https://github.com/ghiscoding/slickgrid-universal/compare/v9.12.0...v9.13.0) (2026-01-30)
### Features
diff --git a/demos/aurelia/CHANGELOG.md b/demos/aurelia/CHANGELOG.md
index 05e7c2bc4a..daa98ab2f1 100644
--- a/demos/aurelia/CHANGELOG.md
+++ b/demos/aurelia/CHANGELOG.md
@@ -4,6 +4,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [10.0.0-beta.0](https://github.com/ghiscoding/slickgrid-universal/compare/v9.12.0...v10.0.0-beta.0) (2026-02-14)
+
+### โ BREAKING CHANGES
+
+* migrate Row/Hybrid Selection flag into a single `enableSelection` flag (#2331)
+* drop Cell/Row Selection Models & keep only HybridSelectionModel (#2330)
+* remove all Deprecated code (#2302)
+* drop OperatorType enums and keep only type literal (#2301)
+* replacing multiple TypeScript `enum` with `type` to decrease build size (#2300)
+* make Row Detail plugin as optional in all framework wrappers (#2291)
+* switch to column `hidden` property and always keep all columns (#2281)
+
+### Features
+
+* add custom menu slot renderers ([#2375](https://github.com/ghiscoding/slickgrid-universal/issues/2375)) ([7ebbda5](https://github.com/ghiscoding/slickgrid-universal/commit/7ebbda58233bb5ce94b81b5b2b578af0ea6a068d)) - by @ghiscoding
+* auto-enabled external resources with their associated flags ([#2362](https://github.com/ghiscoding/slickgrid-universal/issues/2362)) ([16dd8a7](https://github.com/ghiscoding/slickgrid-universal/commit/16dd8a77dd5d136a5a99321f0fc4c50571fdb0c0)) - by @ghiscoding
+* drop Cell/Row Selection Models & keep only HybridSelectionModel ([#2330](https://github.com/ghiscoding/slickgrid-universal/issues/2330)) ([4398cf4](https://github.com/ghiscoding/slickgrid-universal/commit/4398cf42e03f6971b81db8cea4ed11138c0aa452)) - by @ghiscoding
+* make Row Detail plugin as optional in all framework wrappers ([#2291](https://github.com/ghiscoding/slickgrid-universal/issues/2291)) ([fa1a14c](https://github.com/ghiscoding/slickgrid-universal/commit/fa1a14c16c987bfaf7725c46e8114b20ea5a505d)) - by @ghiscoding
+* migrate Row/Hybrid Selection flag into a single `enableSelection` flag ([#2331](https://github.com/ghiscoding/slickgrid-universal/issues/2331)) ([5be5e6a](https://github.com/ghiscoding/slickgrid-universal/commit/5be5e6a862ecd024cf43d404769d65c6c1dd335e)) - by @ghiscoding
+* switch to column `hidden` property and always keep all columns ([#2281](https://github.com/ghiscoding/slickgrid-universal/issues/2281)) ([075c649](https://github.com/ghiscoding/slickgrid-universal/commit/075c64961cb7400500df46b792866d39fba2d9e0)) - by @ghiscoding
+* **tooltip:** add global tooltip observation for non-grid elements ([#2371](https://github.com/ghiscoding/slickgrid-universal/issues/2371)) ([1bbc8de](https://github.com/ghiscoding/slickgrid-universal/commit/1bbc8de895e370843286eadd08574efc552ad8fd)) - by @ghiscoding
+
+### Bug Fixes
+
+* **plugin:** SlickCustomTooltip should work with parent+child tooltips ([#2374](https://github.com/ghiscoding/slickgrid-universal/issues/2374)) ([8af7f45](https://github.com/ghiscoding/slickgrid-universal/commit/8af7f45eb19f0a00da2f3de7c729504be7d043eb)) - by @ghiscoding
+* remove all Deprecated code ([#2302](https://github.com/ghiscoding/slickgrid-universal/issues/2302)) ([f42c46c](https://github.com/ghiscoding/slickgrid-universal/commit/f42c46cd1f05b5c72c62f552f124b5bfe776f8b0)) - by @ghiscoding
+
+### Code Refactoring
+
+* drop OperatorType enums and keep only type literal ([#2301](https://github.com/ghiscoding/slickgrid-universal/issues/2301)) ([5dd0807](https://github.com/ghiscoding/slickgrid-universal/commit/5dd08079460dc9af798ab29527997a6d4b31abdd)) - by @ghiscoding
+* replacing multiple TypeScript `enum` with `type` to decrease build size ([#2300](https://github.com/ghiscoding/slickgrid-universal/issues/2300)) ([ea79395](https://github.com/ghiscoding/slickgrid-universal/commit/ea79395cf663b3abce8e43cf27ba6ffea7cfe113)) - by @ghiscoding
+
## [9.13.0](https://github.com/ghiscoding/slickgrid-universal/compare/v9.12.0...v9.13.0) (2026-01-30)
### Features
diff --git a/demos/aurelia/package.json b/demos/aurelia/package.json
index c684cf4ed8..441828e1cb 100644
--- a/demos/aurelia/package.json
+++ b/demos/aurelia/package.json
@@ -1,7 +1,7 @@
{
"name": "aurelia-slickgrid-demo",
"private": true,
- "version": "9.13.0",
+ "version": "10.0.0-beta.0",
"description": "Aurelia-Slickgrid demos",
"keywords": [
"aurelia",
@@ -31,6 +31,7 @@
"@fnando/sparkline": "catalog:",
"@formkit/tempo": "catalog:",
"@popperjs/core": "catalog:",
+ "@slickgrid-universal/aurelia-row-detail-plugin": "workspace:*",
"@slickgrid-universal/common": "workspace:*",
"@slickgrid-universal/composite-editor-component": "workspace:*",
"@slickgrid-universal/custom-tooltip-plugin": "workspace:*",
@@ -45,6 +46,7 @@
"aurelia-slickgrid": "workspace:*",
"bootstrap": "catalog:",
"i18next": "catalog:",
+ "jspdf": "catalog:",
"rxjs": "catalog:"
},
"devDependencies": {
@@ -59,7 +61,7 @@
"sass": "catalog:",
"tslib": "catalog:",
"typescript": "catalog:",
- "vite": "catalog:vite7",
+ "vite": "catalog:",
"vite-plugin-sass-dts": "^1.3.35"
}
}
diff --git a/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelEditor.ts b/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelEditor.ts
index 008e259c40..1e291b379b 100644
--- a/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelEditor.ts
+++ b/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelEditor.ts
@@ -33,7 +33,7 @@ export class CustomAureliaViewModelEditor implements Editor {
elmBindingContext?: IBindingContext;
constructor(private args: any) {
- this.grid = args && args.grid;
+ this.grid = args?.grid;
this.init();
}
diff --git a/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelFilter.ts b/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelFilter.ts
index d44cc0d24f..f5b6ebbb3a 100644
--- a/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelFilter.ts
+++ b/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelFilter.ts
@@ -3,14 +3,13 @@ import type { ICustomElementController } from '@aurelia/runtime-html';
import {
AureliaUtilService,
emptyElement,
- OperatorType,
type Column,
type ColumnFilter,
type Filter,
type FilterArguments,
type FilterCallback,
type GridOption,
- type OperatorString,
+ type OperatorType,
type SearchTerm,
type SlickGrid,
type ViewModelBindableInputData,
@@ -23,7 +22,7 @@ export class CustomAureliaViewModelFilter implements Filter {
searchTerms: SearchTerm[] = [];
columnDef!: Column;
callback!: FilterCallback;
- operator: OperatorType | OperatorString = OperatorType.equal;
+ operator: OperatorType = 'EQ';
/** Aurelia ViewModel Reference */
vm?: { controller?: ICustomElementController } | null;
diff --git a/demos/aurelia/src/examples/slickgrid/custom-inputFilter.ts b/demos/aurelia/src/examples/slickgrid/custom-inputFilter.ts
index fad4c145b6..5972dfba15 100644
--- a/demos/aurelia/src/examples/slickgrid/custom-inputFilter.ts
+++ b/demos/aurelia/src/examples/slickgrid/custom-inputFilter.ts
@@ -1,13 +1,12 @@
import {
emptyElement,
- OperatorType,
type Column,
type ColumnFilter,
type Filter,
type FilterArguments,
type FilterCallback,
type GridOption,
- type OperatorString,
+ type OperatorType,
type SearchTerm,
type SlickGrid,
} from 'aurelia-slickgrid';
@@ -20,7 +19,7 @@ export class CustomInputFilter implements Filter {
searchTerms: SearchTerm[] = [];
columnDef!: Column;
callback!: FilterCallback;
- operator: OperatorType | OperatorString = OperatorType.equal;
+ operator: OperatorType = 'EQ';
/** Getter for the Filter Operator */
get columnFilter(): ColumnFilter {
diff --git a/demos/aurelia/src/examples/slickgrid/example10.ts b/demos/aurelia/src/examples/slickgrid/example10.ts
index 4aff989b69..8788d246e7 100644
--- a/demos/aurelia/src/examples/slickgrid/example10.ts
+++ b/demos/aurelia/src/examples/slickgrid/example10.ts
@@ -148,7 +148,7 @@ export class Example10 {
this.gridOptions1 = {
enableAutoResize: false,
enableCellNavigation: true,
- enableRowSelection: true,
+ enableSelection: true,
enableCheckboxSelector: true,
enableFiltering: true,
checkboxSelector: {
@@ -160,7 +160,7 @@ export class Example10 {
// selectableOverride: (row: number, dataContext: any, grid: SlickGrid) => (dataContext.id % 2 === 1)
},
multiSelect: false,
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: true,
},
@@ -196,12 +196,12 @@ export class Example10 {
hideInColumnTitleRow: true,
applySelectOnAllPages: true, // when clicking "Select All", should we apply it to all pages (defaults to true)
},
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
enableCheckboxSelector: true,
- enableRowSelection: true,
gridHeight: 255,
gridWidth: 800,
enablePagination: true,
@@ -293,7 +293,7 @@ export class Example10 {
}
onGrid1SelectedRowsChanged(_e: Event, args: any) {
- const grid = args && args.grid;
+ const grid = args?.grid;
if (Array.isArray(args.rows)) {
this.selectedTitle = args.rows.map((idx: number) => {
const item = grid.getDataItem(idx);
diff --git a/demos/aurelia/src/examples/slickgrid/example11.html b/demos/aurelia/src/examples/slickgrid/example11.html
index 22c7ce799c..71309fb273 100644
--- a/demos/aurelia/src/examples/slickgrid/example11.html
+++ b/demos/aurelia/src/examples/slickgrid/example11.html
@@ -25,7 +25,7 @@
Adding an item, will always be showing as the 1st item in the grid because that is the best visual place to add it
Add/Update an item requires a valid Slickgrid Selection Model, you have 2 choices to deal with this:
-
You can enable "enableCheckboxSelector" or "enableRowSelection" to True
+
You can enable "enableCheckboxSelector" or "enableSelection" to True
Click on any of the buttons below to test this out
diff --git a/demos/aurelia/src/examples/slickgrid/example11.ts b/demos/aurelia/src/examples/slickgrid/example11.ts
index 039b136ba1..ec160a7e25 100644
--- a/demos/aurelia/src/examples/slickgrid/example11.ts
+++ b/demos/aurelia/src/examples/slickgrid/example11.ts
@@ -134,7 +134,7 @@ export class Example11 {
editable: true,
enableColumnPicker: true,
enableCellNavigation: true,
- enableRowSelection: true,
+ enableSelection: true,
};
}
diff --git a/demos/aurelia/src/examples/slickgrid/example12.ts b/demos/aurelia/src/examples/slickgrid/example12.ts
index 3091b3ebc2..a03b844c10 100644
--- a/demos/aurelia/src/examples/slickgrid/example12.ts
+++ b/demos/aurelia/src/examples/slickgrid/example12.ts
@@ -4,7 +4,6 @@ import { TextExportService } from '@slickgrid-universal/text-export';
import { resolve } from 'aurelia';
// import { TOptions as I18NOptions } from 'i18next';
import {
- DelimiterType,
Filters,
Formatters,
type AureliaGridInstance,
@@ -178,7 +177,7 @@ export class Example12 {
hideInColumnTitleRow: true,
},
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
showCustomFooter: true, // display some metrics in the bottom custom footer
customFooterOptions: {
metricTexts: {
@@ -292,7 +291,7 @@ export class Example12 {
exportToFile(type = 'csv') {
this.textExportService.exportToFile({
- delimiter: type === 'csv' ? DelimiterType.comma : DelimiterType.tab,
+ delimiter: type === 'csv' ? ',' : '\t',
filename: 'myExport',
format: type === 'csv' ? 'csv' : 'txt',
});
diff --git a/demos/aurelia/src/examples/slickgrid/example14.html b/demos/aurelia/src/examples/slickgrid/example14.html
index 2710620da4..d6d7b2de22 100644
--- a/demos/aurelia/src/examples/slickgrid/example14.html
+++ b/demos/aurelia/src/examples/slickgrid/example14.html
@@ -24,8 +24,25 @@
-
Grid 1 (with Header Grouping & Colspan)
-
+
+ Grid 1 (with Header Grouping & Colspan)
+
+
+
+
diff --git a/demos/aurelia/src/examples/slickgrid/example14.ts b/demos/aurelia/src/examples/slickgrid/example14.ts
index 66642904ce..f7daa332e4 100644
--- a/demos/aurelia/src/examples/slickgrid/example14.ts
+++ b/demos/aurelia/src/examples/slickgrid/example14.ts
@@ -4,6 +4,7 @@ import { type AureliaGridInstance, type Column, type GridOption, type ItemMetada
import './example14.scss'; // provide custom CSS/SASS styling
export class Example14 {
+ aureliaGrid1!: AureliaGridInstance;
aureliaGrid2!: AureliaGridInstance;
gridObj2: any;
columnDefinitions1: Column[] = [];
@@ -13,6 +14,7 @@ export class Example14 {
dataset1: any[] = [];
dataset2: any[] = [];
hideSubTitle = false;
+ isColspanSpreading = false;
constructor() {
this.definedGrid1();
@@ -25,6 +27,10 @@ export class Example14 {
this.dataset2 = this.getData(500);
}
+ aureliaGridReady1(aureliaGrid: AureliaGridInstance) {
+ this.aureliaGrid1 = aureliaGrid;
+ }
+
aureliaGridReady2(aureliaGrid: AureliaGridInstance) {
this.aureliaGrid2 = aureliaGrid;
this.gridObj2 = aureliaGrid.slickGrid;
@@ -65,6 +71,7 @@ export class Example14 {
gridMenu: {
iconButtonContainer: 'preheader', // we can display the grid menu icon in either the preheader or in the column header (default)
},
+ spreadHiddenColspan: this.isColspanSpreading,
};
}
@@ -156,6 +163,13 @@ export class Example14 {
};
}
+ spreadColspan() {
+ this.isColspanSpreading = !this.isColspanSpreading;
+ this.aureliaGrid1.slickGrid?.setOptions({ spreadHiddenColspan: this.isColspanSpreading });
+ this.aureliaGrid1.slickGrid?.resetActiveCell();
+ this.aureliaGrid1.slickGrid?.invalidate();
+ }
+
toggleSubTitle() {
this.hideSubTitle = !this.hideSubTitle;
const action = this.hideSubTitle ? 'add' : 'remove';
diff --git a/demos/aurelia/src/examples/slickgrid/example16.ts b/demos/aurelia/src/examples/slickgrid/example16.ts
index fd8854884a..489f96b3fb 100644
--- a/demos/aurelia/src/examples/slickgrid/example16.ts
+++ b/demos/aurelia/src/examples/slickgrid/example16.ts
@@ -1,12 +1,4 @@
-import {
- ExtensionName,
- Filters,
- Formatters,
- type AureliaGridInstance,
- type Column,
- type GridOption,
- type OnEventArgs,
-} from 'aurelia-slickgrid';
+import { Filters, Formatters, type AureliaGridInstance, type Column, type GridOption, type OnEventArgs } from 'aurelia-slickgrid';
export class Example16 {
aureliaGrid!: AureliaGridInstance;
@@ -24,7 +16,7 @@ export class Example16 {
}
get rowMoveInstance() {
- return this.aureliaGrid?.extensionService.getExtensionInstanceByName(ExtensionName.rowMoveManager);
+ return this.aureliaGrid?.extensionService.getExtensionInstanceByName('rowMoveManager');
}
attached() {
@@ -87,8 +79,8 @@ export class Example16 {
hideInFilterHeaderRow: false,
hideInColumnTitleRow: true,
},
- enableRowSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
diff --git a/demos/aurelia/src/examples/slickgrid/example18.ts b/demos/aurelia/src/examples/slickgrid/example18.ts
index f740bad981..24f7665ba3 100644
--- a/demos/aurelia/src/examples/slickgrid/example18.ts
+++ b/demos/aurelia/src/examples/slickgrid/example18.ts
@@ -253,12 +253,13 @@ export class Example18 {
initialGroupBy: ['duration'],
},
darkMode: this._darkMode,
- enableTextExport: true,
- enableExcelExport: true,
excelExportOptions: { sanitizeDataExport: true },
textExportOptions: { sanitizeDataExport: true },
externalResources: [this.excelExportService, this.pdfExportService, this.textExportService],
- enablePdfExport: true,
+ // -- NOTE: registered resources are auto-enabled
+ // enableTextExport: true,
+ // enablePdfExport: true,
+ // enableExcelExport: true,
pdfExportOptions: {
repeatHeadersOnEachPage: true, // defaults to true
documentTitle: 'Grouping Grid',
diff --git a/demos/aurelia/src/examples/slickgrid/example19-detail-view.ts b/demos/aurelia/src/examples/slickgrid/example19-detail-view.ts
index fec60a3283..6ca7ffd50c 100644
--- a/demos/aurelia/src/examples/slickgrid/example19-detail-view.ts
+++ b/demos/aurelia/src/examples/slickgrid/example19-detail-view.ts
@@ -1,4 +1,4 @@
-import type { SlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin';
+import type { AureliaSlickRowDetailView } from '@slickgrid-universal/aurelia-row-detail-plugin';
import { bindable } from 'aurelia';
import type { SlickDataView, SlickGrid } from 'aurelia-slickgrid';
import './example19-detail-view.scss';
@@ -19,7 +19,7 @@ export class Example19DetailView {
@bindable() model!: Item;
// you also have access to the following objects (it must match the exact property names shown below)
- @bindable() addon!: SlickRowDetailView; // row detail addon instance
+ @bindable() addon!: AureliaSlickRowDetailView; // row detail addon instance
@bindable() grid!: SlickGrid;
@bindable() dataView!: SlickDataView;
diff --git a/demos/aurelia/src/examples/slickgrid/example19.ts b/demos/aurelia/src/examples/slickgrid/example19.ts
index 592d9df426..83d513ea5e 100644
--- a/demos/aurelia/src/examples/slickgrid/example19.ts
+++ b/demos/aurelia/src/examples/slickgrid/example19.ts
@@ -1,5 +1,6 @@
+import { AureliaSlickRowDetailView } from '@slickgrid-universal/aurelia-row-detail-plugin';
import { bindable } from 'aurelia';
-import { Editors, ExtensionName, Filters, Formatters, type AureliaGridInstance, type Column, type GridOption } from 'aurelia-slickgrid';
+import { Editors, Filters, Formatters, type AureliaGridInstance, type Column, type GridOption } from 'aurelia-slickgrid';
import { ExampleDetailPreload } from './example-detail-preload.js';
import { Example19DetailView } from './example19-detail-view.js';
@@ -32,7 +33,7 @@ export class Example19 {
// return this.extensions.rowDetailView.instance || {};
// OR option 2
- return this.aureliaGrid?.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView);
+ return this.aureliaGrid?.extensionService.getExtensionInstanceByName('rowDetailView');
}
attached() {
@@ -134,6 +135,7 @@ export class Example19 {
rowTopOffsetRenderType: 'top', // RowDetail and/or RowSpan don't render well with "transform", you should use "top"
darkMode: this._darkMode,
datasetIdPropertyName: 'rowId', // optionally use a different "id"
+ externalResources: [AureliaSlickRowDetailView],
rowDetailView: {
// optionally change the column index position of the icon (defaults to 0)
// columnIndexPosition: 1,
@@ -171,7 +173,7 @@ export class Example19 {
// Optionally pass your Parent Component reference to your Child Component (row detail component)
parentRef: this,
},
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: true,
},
diff --git a/demos/aurelia/src/examples/slickgrid/example2.ts b/demos/aurelia/src/examples/slickgrid/example2.ts
index 913fc8a181..6030b3f685 100644
--- a/demos/aurelia/src/examples/slickgrid/example2.ts
+++ b/demos/aurelia/src/examples/slickgrid/example2.ts
@@ -129,7 +129,7 @@ export class Example2 {
minWidth: 100,
formatter: customEnableButtonFormatter,
onCellClick: (_e, args) => {
- this.toggleCompletedProperty(args && args.dataContext);
+ this.toggleCompletedProperty(args?.dataContext);
},
},
];
diff --git a/demos/aurelia/src/examples/slickgrid/example21.ts b/demos/aurelia/src/examples/slickgrid/example21.ts
index 991e5370ed..0e266bb342 100644
--- a/demos/aurelia/src/examples/slickgrid/example21.ts
+++ b/demos/aurelia/src/examples/slickgrid/example21.ts
@@ -1,5 +1,5 @@
import { bindable } from 'aurelia';
-import { Formatters, type AureliaGridInstance, type Column, type GridOption, type OperatorString } from 'aurelia-slickgrid';
+import { Formatters, type AureliaGridInstance, type Column, type GridOption, type OperatorType } from 'aurelia-slickgrid';
import './example21.scss';
export class Example21 {
@@ -11,7 +11,7 @@ export class Example21 {
gridOptions!: GridOption;
dataset: any[] = [];
hideSubTitle = false;
- operatorList: OperatorString[] = ['=', '<', '<=', '>', '>=', '<>', 'StartsWith', 'EndsWith'];
+ operatorList: OperatorType[] = ['=', '<', '<=', '>', '>=', '<>', 'StartsWith', 'EndsWith'];
constructor() {
// define the grid options & columns and then create the grid itself
@@ -99,7 +99,7 @@ export class Example21 {
alwaysShowVerticalScroll: false,
enableColumnPicker: true,
enableCellNavigation: true,
- enableRowSelection: true,
+ enableSelection: true,
};
}
@@ -150,7 +150,7 @@ export class Example21 {
updateFilter() {
this.aureliaGrid?.filterService.updateSingleFilter({
columnId: `${this.selectedColumn.id || ''}`,
- operator: this.selectedOperator as OperatorString,
+ operator: this.selectedOperator as OperatorType,
searchTerms: [this.searchValue || ''],
});
}
diff --git a/demos/aurelia/src/examples/slickgrid/example23.ts b/demos/aurelia/src/examples/slickgrid/example23.ts
index 7bc4498c6c..989b13e618 100644
--- a/demos/aurelia/src/examples/slickgrid/example23.ts
+++ b/demos/aurelia/src/examples/slickgrid/example23.ts
@@ -7,7 +7,6 @@ import { resolve } from 'aurelia';
import {
Filters,
Formatters,
- OperatorType,
type AureliaGridInstance,
type Column,
type CurrentFilter,
@@ -109,7 +108,7 @@ export class Example23 {
filter: {
model: Filters.sliderRange,
maxValue: 100, // or you can use the options as well
- operator: OperatorType.rangeInclusive, // defaults to inclusive
+ operator: 'RangeInclusive', // defaults to inclusive
options: {
hideSliderNumbers: false, // you can hide/show the slider numbers on both side
min: 0,
@@ -157,7 +156,7 @@ export class Example23 {
filterable: true,
filter: {
model: Filters.input,
- operator: OperatorType.rangeExclusive, // defaults to exclusive
+ operator: 'RangeExclusive', // defaults to exclusive
},
},
{
@@ -260,11 +259,11 @@ export class Example23 {
}
refreshMetrics(_e: Event, args: any) {
- if (args && args.current >= 0) {
+ if (args?.current >= 0) {
setTimeout(() => {
this.metrics = {
startTime: new Date(),
- itemCount: (args && args.current) || 0,
+ itemCount: args?.current || 0,
totalItemCount: this.dataset.length || 0,
};
});
@@ -304,8 +303,8 @@ export class Example23 {
switch (newPredefinedFilter) {
case 'currentYearTasks':
filters = [
- { columnId: 'finish', operator: OperatorType.rangeInclusive, searchTerms: [`${currentYear}-01-01`, `${currentYear}-12-31`] },
- { columnId: 'completed', operator: OperatorType.equal, searchTerms: [true] },
+ { columnId: 'finish', operator: 'RangeInclusive', searchTerms: [`${currentYear}-01-01`, `${currentYear}-12-31`] },
+ { columnId: 'completed', operator: '=', searchTerms: [true] },
];
break;
case 'nextYearTasks':
diff --git a/demos/aurelia/src/examples/slickgrid/example24.ts b/demos/aurelia/src/examples/slickgrid/example24.ts
index cf673d10f5..a24fd42b0d 100644
--- a/demos/aurelia/src/examples/slickgrid/example24.ts
+++ b/demos/aurelia/src/examples/slickgrid/example24.ts
@@ -3,7 +3,6 @@ import { ExcelExportService } from '@slickgrid-universal/excel-export';
// import { TOptions as I18NOptions } from 'i18next';
import { resolve } from 'aurelia';
import {
- ExtensionName,
Filters,
Formatters,
type AureliaGridInstance,
@@ -78,11 +77,11 @@ export class Example24 {
}
get cellMenuInstance() {
- return this.aureliaGrid?.extensionService.getExtensionInstanceByName(ExtensionName.cellMenu);
+ return this.aureliaGrid?.extensionService.getExtensionInstanceByName('cellMenu');
}
get contextMenuInstance() {
- return this.aureliaGrid?.extensionService.getExtensionInstanceByName(ExtensionName.contextMenu);
+ return this.aureliaGrid?.extensionService.getExtensionInstanceByName('contextMenu');
}
attached() {
@@ -429,7 +428,7 @@ export class Example24 {
// optionally and conditionally define when the the menu is usable,
// this should be used with a custom formatter to show/hide/disable the menu
menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return dataContext.id < 21; // say we want to display the menu only from Task 0 to 20
},
// which column to show the command list? when not defined it will be shown over all columns
@@ -460,7 +459,7 @@ export class Example24 {
},
// only show command to 'Help' when the task is Not Completed
itemVisibilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -526,7 +525,7 @@ export class Example24 {
textCssClass: 'italic',
// only enable this option when the task is Not Completed
itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
// you can use the 'action' callback and/or subscribe to the 'onCallback' event, they both have the same arguments
@@ -548,7 +547,7 @@ export class Example24 {
disabled: true,
// only shown when the task is Not Completed
itemVisibilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -579,7 +578,7 @@ export class Example24 {
// subscribe to Context Menu onOptionSelected event (or use the action callback on each option)
onOptionSelected: (_e: any, args: any) => {
// change Priority
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext?.hasOwnProperty('priority')) {
dataContext.priority = args.item.option;
this.aureliaGrid.gridService.updateItem(dataContext);
diff --git a/demos/aurelia/src/examples/slickgrid/example25.ts b/demos/aurelia/src/examples/slickgrid/example25.ts
index 7257d4adf6..6b3fb6d577 100644
--- a/demos/aurelia/src/examples/slickgrid/example25.ts
+++ b/demos/aurelia/src/examples/slickgrid/example25.ts
@@ -4,7 +4,6 @@ import { GraphqlService, type GraphqlResult, type GraphqlServiceApi } from '@sli
import {
Filters,
Formatters,
- OperatorType,
type AureliaGridInstance,
type Column,
type GridOption,
@@ -85,7 +84,7 @@ export class Example25 {
filter: {
model: Filters.multipleSelect,
collectionAsync: this.getLanguages(),
- operator: OperatorType.inContains,
+ operator: 'IN_CONTAINS',
collectionOptions: {
addBlankEntry: true,
// the data is not at the root of the array, so we must tell the Select Filter where to pull the data
@@ -118,7 +117,7 @@ export class Example25 {
filter: {
model: Filters.multipleSelect,
collectionAsync: this.getLanguages(),
- operator: OperatorType.inContains,
+ operator: 'IN_CONTAINS',
collectionOptions: {
addBlankEntry: true,
// the data is not at the root of the array, so we must tell the Select Filter where to pull the data
@@ -277,7 +276,7 @@ export class Example25 {
setFiltersDynamically() {
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
- this.aureliaGrid.filterService.updateFilters([{ columnId: 'countryName', searchTerms: ['G'], operator: OperatorType.startsWith }]);
+ this.aureliaGrid.filterService.updateFilters([{ columnId: 'countryName', searchTerms: ['G'], operator: 'StartsWith' }]);
}
setSortingDynamically() {
diff --git a/demos/aurelia/src/examples/slickgrid/example26.ts b/demos/aurelia/src/examples/slickgrid/example26.ts
index 5da11df4b8..499b058450 100644
--- a/demos/aurelia/src/examples/slickgrid/example26.ts
+++ b/demos/aurelia/src/examples/slickgrid/example26.ts
@@ -4,7 +4,6 @@ import {
Editors,
Filters,
Formatters,
- OperatorType,
SlickGlobalEditorLock,
type AureliaGridInstance,
type Column,
@@ -198,7 +197,7 @@ export class Example26 {
collectionFilterBy: {
property: 'value',
value: 0,
- operator: OperatorType.notEqual,
+ operator: '!=',
},
model: Editors.singleSelect,
},
diff --git a/demos/aurelia/src/examples/slickgrid/example3.ts b/demos/aurelia/src/examples/slickgrid/example3.ts
index 433368c564..cb556556d1 100644
--- a/demos/aurelia/src/examples/slickgrid/example3.ts
+++ b/demos/aurelia/src/examples/slickgrid/example3.ts
@@ -5,7 +5,6 @@ import {
Editors,
Filters,
Formatters,
- OperatorType,
SlickGlobalEditorLock,
SortComparers,
type AureliaGridInstance,
@@ -29,7 +28,7 @@ const NB_ITEMS = 100;
// you can create custom validator to pass to an inline editor
const myCustomTitleValidator: EditorValidator = (value: any) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
- // const grid = args && args.grid;
+ // const grid = args?.grid;
// const gridOptions = grid.getOptions() as GridOption;
// const i18n = gridOptions.i18n;
@@ -210,7 +209,7 @@ export class Example3 {
collectionFilterBy: {
property: 'value',
value: 0,
- operator: OperatorType.notEqual,
+ operator: 'NE',
},
model: Editors.singleSelect,
// validator: (value, args) => {
@@ -433,7 +432,7 @@ export class Example3 {
separatorBetweenTextLabels: ' ',
},
model: Filters.multipleSelect,
- operator: OperatorType.inContains,
+ operator: 'IN_CONTAINS',
},
},
];
diff --git a/demos/aurelia/src/examples/slickgrid/example30.ts b/demos/aurelia/src/examples/slickgrid/example30.ts
index eedf04f3af..dfe20edcdc 100644
--- a/demos/aurelia/src/examples/slickgrid/example30.ts
+++ b/demos/aurelia/src/examples/slickgrid/example30.ts
@@ -487,7 +487,7 @@ export class Example30 {
},
externalResources: [new ExcelExportService(), new SlickCustomTooltip(), this.compositeEditorInstance],
enableFiltering: true,
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
@@ -495,7 +495,7 @@ export class Example30 {
showPreHeaderPanel: true,
preHeaderPanelHeight: 28,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
multiSelect: false,
checkboxSelector: {
hideInFilterHeaderRow: false,
diff --git a/demos/aurelia/src/examples/slickgrid/example31.ts b/demos/aurelia/src/examples/slickgrid/example31.ts
index 3da46675cb..9eb561115d 100644
--- a/demos/aurelia/src/examples/slickgrid/example31.ts
+++ b/demos/aurelia/src/examples/slickgrid/example31.ts
@@ -5,7 +5,6 @@ import { RxJsResource } from '@slickgrid-universal/rxjs-observable';
import {
Editors,
Filters,
- OperatorType,
type AureliaGridInstance,
type Column,
type GridOption,
@@ -103,7 +102,7 @@ export class Example31 {
enableCellNavigation: true,
enableFiltering: true,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
enablePagination: true, // you could optionally disable the Pagination
pagination: {
pageSizes: [10, 20, 50, 100, 500],
@@ -112,8 +111,8 @@ export class Example31 {
presets: {
// you can also type operator as string, e.g.: operator: 'EQ'
filters: [
- // { columnId: 'name', searchTerms: ['w'], operator: OperatorType.startsWith },
- { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
+ // { columnId: 'name', searchTerms: ['w'], operator: 'StartsWith' },
+ { columnId: 'gender', searchTerms: ['male'], operator: '=' },
],
sorters: [
// direction can be written as 'asc' (uppercase or lowercase) and/or use the SortDirection type
@@ -384,7 +383,7 @@ export class Example31 {
setFiltersDynamically() {
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
this.aureliaGrid?.filterService.updateFilters([
- // { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
+ // { columnId: 'gender', searchTerms: ['male'], operator: '=' },
{ columnId: 'name', searchTerms: ['A'], operator: 'a*' },
]);
}
diff --git a/demos/aurelia/src/examples/slickgrid/example32.ts b/demos/aurelia/src/examples/slickgrid/example32.ts
index fcb61dba44..7a4d4beac2 100644
--- a/demos/aurelia/src/examples/slickgrid/example32.ts
+++ b/demos/aurelia/src/examples/slickgrid/example32.ts
@@ -494,13 +494,13 @@ export class Example32 {
},
externalResources: [new ExcelExportService()],
enableFiltering: true,
- enableRowSelection: true,
+ enableSelection: true,
enableCheckboxSelector: true,
checkboxSelector: {
hideInFilterHeaderRow: false,
hideInColumnTitleRow: true,
},
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
@@ -585,7 +585,7 @@ export class Example32 {
// just for demo purposes, set it back to its original width
const columns = this.aureliaGrid.slickGrid.getColumns() as Column[];
columns.forEach((col) => (col.width = col.originalWidth));
- this.aureliaGrid.slickGrid.setColumns(columns);
+ this.aureliaGrid.slickGrid.updateColumns();
this.aureliaGrid.slickGrid.autosizeColumns();
this.isUsingDefaultResize = true;
}
diff --git a/demos/aurelia/src/examples/slickgrid/example33.html b/demos/aurelia/src/examples/slickgrid/example33.html
index 80ec67fbca..f184dd4453 100644
--- a/demos/aurelia/src/examples/slickgrid/example33.html
+++ b/demos/aurelia/src/examples/slickgrid/example33.html
@@ -33,6 +33,24 @@
+
+
Lazy loading collection...
diff --git a/demos/aurelia/src/examples/slickgrid/example33.ts b/demos/aurelia/src/examples/slickgrid/example33.ts
index d50506dadf..0371f6f92c 100644
--- a/demos/aurelia/src/examples/slickgrid/example33.ts
+++ b/demos/aurelia/src/examples/slickgrid/example33.ts
@@ -1,10 +1,10 @@
import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin';
import { ExcelExportService } from '@slickgrid-universal/excel-export';
import {
+ createDomElement,
Editors,
Filters,
Formatters,
- OperatorType,
type AureliaGridInstance,
type Column,
type EditCommand,
@@ -141,6 +141,31 @@ export class Example33 {
// maxHeight: 30,
},
},
+ {
+ id: 'button',
+ name: 'Button Tooltip',
+ field: 'title',
+ width: 100,
+ minWidth: 100,
+ filterable: true,
+ excludeFromExport: true,
+ formatter: (_row, _cell, value) => {
+ const button = createDomElement('button', {
+ className: 'btn btn-outline-secondary btn-icon btn-sm',
+ title: 'This is the button tooltip',
+ });
+ const icon = createDomElement('i', { className: 'mdi mdi-information', title: 'icon tooltip' });
+ const text = createDomElement('span', { textContent: 'Hello Task' });
+ button.appendChild(icon);
+ button.appendChild(text);
+ button.addEventListener('click', () => alert(`Clicked button for ${value}`));
+ return button;
+ },
+ // define tooltip options here OR for the entire grid via the grid options (cell tooltip options will have precedence over grid options)
+ customTooltip: {
+ useRegularTooltip: true, // note regular tooltip will try to find a "title" attribute in the cell formatter (it won't work without a cell formatter)
+ },
+ },
{
id: 'cost',
name: 'Cost',
@@ -320,7 +345,7 @@ export class Example33 {
collectionOptions: { separatorBetweenTextLabels: ' ' },
options: { minHeight: 70 } as MultipleSelectOption,
model: Filters.multipleSelect,
- operator: OperatorType.inContains,
+ operator: 'IN_CONTAINS',
},
},
{
@@ -403,19 +428,21 @@ export class Example33 {
headerFormatter: this.headerFormatter,
headerRowFormatter: this.headerRowFormatter,
usabilityOverride: (args) => args.cell !== 0 && args?.column?.id !== 'action', // don't show on first/last columns
+ observeAllTooltips: true, // observe all elements with title/data-slick-tooltip attributes (not just SlickGrid elements)
+ observeTooltipContainer: 'body', // defaults to 'body', target a specific container (only works when observeAllTooltips is enabled)
},
presets: {
filters: [{ columnId: 'prerequisites', searchTerms: [1, 3, 5, 7, 9, 12, 15, 18, 21, 25, 28, 29, 30, 32, 34] }],
},
- rowHeight: 33,
+ rowHeight: 38,
enableFiltering: true,
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
showCustomFooter: true,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
checkboxSelector: {
hideInFilterHeaderRow: false,
hideInColumnTitleRow: true,
@@ -432,7 +459,7 @@ export class Example33 {
onCommand: (e, args) => this.executeCommand(e, args),
onOptionSelected: (_e, args) => {
// change "Completed" property with new option selected from the Cell Menu
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && dataContext.hasOwnProperty('completed')) {
dataContext.completed = args.item.option;
this.aureliaGrid.gridService.updateItem(dataContext);
@@ -457,7 +484,7 @@ export class Example33 {
id: i,
title: 'Task ' + i,
duration: Math.round(Math.random() * 100),
- description: `This is a sample task description.\nIt can be multiline\r\rAnother line...`,
+ description: i > 500 ? null : `This is a sample task description.\nIt can be multiline\r\rAnother line...`,
percentComplete: Math.floor(Math.random() * (100 - 5 + 1) + 5),
start: new Date(randomYear, randomMonth, randomDay),
finish: randomFinish < new Date() ? '' : randomFinish, // make sure the random date is earlier than today
@@ -565,4 +592,18 @@ export class Example33 {
document.querySelector('.subtitle')?.classList[action]('hidden');
this.aureliaGrid.resizerService.resizeGrid(0);
}
+
+ setFiltersDynamically(operator: string) {
+ const operatorType = operator === '=' ? '=' : '!=';
+ this.aureliaGrid.filterService.updateFilters(
+ [
+ {
+ columnId: 'desc',
+ operator: operatorType,
+ searchTerms: [''],
+ },
+ ],
+ true
+ );
+ }
}
diff --git a/demos/aurelia/src/examples/slickgrid/example38.ts b/demos/aurelia/src/examples/slickgrid/example38.ts
index 707be0a852..c7b0c96843 100644
--- a/demos/aurelia/src/examples/slickgrid/example38.ts
+++ b/demos/aurelia/src/examples/slickgrid/example38.ts
@@ -93,7 +93,7 @@ export class Example38 {
enableCellNavigation: true,
enableFiltering: true,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
enableGrouping: true,
headerMenu: {
hideFreezeColumnsCommand: false,
diff --git a/demos/aurelia/src/examples/slickgrid/example4.ts b/demos/aurelia/src/examples/slickgrid/example4.ts
index 32d776f7c3..947053c703 100644
--- a/demos/aurelia/src/examples/slickgrid/example4.ts
+++ b/demos/aurelia/src/examples/slickgrid/example4.ts
@@ -4,7 +4,6 @@ import { ExcelExportService } from '@slickgrid-universal/excel-export';
import {
Filters,
Formatters,
- OperatorType,
type AureliaGridInstance,
type Column,
type GridOption,
@@ -97,12 +96,12 @@ export class Example4 {
collectionFilterBy: [
{
property: 'value',
- operator: OperatorType.notEqual,
+ operator: '!=',
value: 360,
},
{
property: 'value',
- operator: OperatorType.notEqual,
+ operator: '!=',
value: 365,
},
],
@@ -313,12 +312,12 @@ export class Example4 {
}
refreshMetrics(_e: Event, args: any) {
- if (args && args.current >= 0) {
+ if (args?.current >= 0) {
setTimeout(() => {
this.metrics = {
startTime: new Date(),
endTime: new Date(),
- itemCount: (args && args.current) || 0,
+ itemCount: args?.current || 0,
totalItemCount: this.dataset.length || 0,
};
});
diff --git a/demos/aurelia/src/examples/slickgrid/example41.ts b/demos/aurelia/src/examples/slickgrid/example41.ts
index 13e3cf9df0..8e0b733887 100644
--- a/demos/aurelia/src/examples/slickgrid/example41.ts
+++ b/demos/aurelia/src/examples/slickgrid/example41.ts
@@ -49,11 +49,12 @@ export class Example41 {
gridWidth: 800,
rowHeight: 33,
enableCellNavigation: true,
- enableRowSelection: true,
+ enableSelection: true,
enableRowMoveManager: true,
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
+ selectionType: 'row',
},
rowMoveManager: {
columnIndexPosition: 0,
diff --git a/demos/aurelia/src/examples/slickgrid/example42.ts b/demos/aurelia/src/examples/slickgrid/example42.ts
index 1f57abdbe4..76a608ec62 100644
--- a/demos/aurelia/src/examples/slickgrid/example42.ts
+++ b/demos/aurelia/src/examples/slickgrid/example42.ts
@@ -2,7 +2,6 @@ import { bindable } from 'aurelia';
import {
Filters,
Formatters,
- OperatorType,
type AureliaGridInstance,
type Column,
type GridOption,
@@ -74,7 +73,7 @@ export class Example42 {
filter: {
model: Filters.sliderRange,
maxValue: 100, // or you can use the options as well
- operator: OperatorType.rangeInclusive, // defaults to inclusive
+ operator: 'RangeInclusive', // defaults to inclusive
options: {
hideSliderNumbers: false, // you can hide/show the slider numbers on both side
min: 0,
@@ -120,7 +119,7 @@ export class Example42 {
filterable: true,
filter: {
model: Filters.input,
- operator: OperatorType.rangeExclusive, // defaults to exclusive
+ operator: 'RangeExclusive', // defaults to exclusive
},
},
{
diff --git a/demos/aurelia/src/examples/slickgrid/example43.ts b/demos/aurelia/src/examples/slickgrid/example43.ts
index 696cd6e89a..5283c13bd9 100644
--- a/demos/aurelia/src/examples/slickgrid/example43.ts
+++ b/demos/aurelia/src/examples/slickgrid/example43.ts
@@ -427,7 +427,8 @@ export class Example43 {
}
}
- // update column definitions
+ // 1. update column definitions via grid.setColumns()
+ // this will shift colspan/rowspan to the left or right accordingly
if (this.showEmployeeId) {
this.columnDefinitions.unshift({ id: 'employeeID', name: 'Employee ID', field: 'employeeID', width: 100 });
} else {
@@ -435,6 +436,22 @@ export class Example43 {
}
this.aureliaGrid.slickGrid.setColumns(this.columnDefinitions);
+ // --- OR ---
+ // 2. OR update via "hidden" column flag & increase/decrease column index accordingly in the metadata
+ // this approach will keep colspan/rowspan "as-is" but will hide the EmployeeID column
+ /*
+ const colDirIdx = newShowEmployeeId ? -1 : 1;
+ for (const row of Object.keys(this.metadata)) {
+ newMetadata[row] = { columns: {} };
+ for (const col of Object.keys((this.metadata as any)[row].columns)) {
+ newMetadata[row].columns[Number(col) + colDirIdx] = (this.metadata as any)[row].columns[col];
+ }
+ }
+ this.aureliaGrid.slickGrid?.setOptions({ frozenColumn: newShowEmployeeId ? 0 : 1 });
+ this.aureliaGrid.slickGrid?.updateColumnById('employeeID', { hidden: !newShowEmployeeId });
+ this.aureliaGrid.slickGrid?.updateColumns();
+ */
+
// update & remap rowspans
this.metadata = newMetadata;
this.aureliaGrid.slickGrid.remapAllColumnsRowSpan();
diff --git a/demos/aurelia/src/examples/slickgrid/example45.ts b/demos/aurelia/src/examples/slickgrid/example45.ts
index 038861db07..94e7e2e107 100644
--- a/demos/aurelia/src/examples/slickgrid/example45.ts
+++ b/demos/aurelia/src/examples/slickgrid/example45.ts
@@ -1,6 +1,7 @@
import { faker } from '@faker-js/faker';
+import { AureliaSlickRowDetailView } from '@slickgrid-universal/aurelia-row-detail-plugin';
import { bindable } from 'aurelia';
-import { ExtensionName, type AureliaGridInstance, type Column, type GridOption, type SlickRowDetailView } from 'aurelia-slickgrid';
+import { type AureliaGridInstance, type Column, type GridOption } from 'aurelia-slickgrid';
import { Example45DetailView, type Distributor, type OrderData } from './example45-detail-view.js';
import { Example45Preload } from './example45-preload.js';
@@ -21,7 +22,7 @@ export class Example45 {
hideSubTitle = false;
get rowDetailInstance() {
- return this.aureliaGrid?.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView) as SlickRowDetailView;
+ return this.aureliaGrid?.extensionService.getExtensionInstanceByName('rowDetailView') as AureliaSlickRowDetailView;
}
aureliaGridReady(aureliaGrid: AureliaGridInstance) {
@@ -101,6 +102,7 @@ export class Example45 {
rowTopOffsetRenderType: 'top', // RowDetail and/or RowSpan don't render well with "transform", you should use "top"
darkMode: this._darkMode,
rowHeight: 33,
+ externalResources: [AureliaSlickRowDetailView],
rowDetailView: {
process: (item) => this.simulateServerAsyncCall(item),
loadOnce: false, // you can't use loadOnce with inner grid because only HTML template are re-rendered, not JS events
diff --git a/demos/aurelia/src/examples/slickgrid/example46.ts b/demos/aurelia/src/examples/slickgrid/example46.ts
index 378e4db940..28380b5829 100644
--- a/demos/aurelia/src/examples/slickgrid/example46.ts
+++ b/demos/aurelia/src/examples/slickgrid/example46.ts
@@ -120,7 +120,7 @@ export class Example46 {
sanitizeDataExport: true,
},
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
multiSelect: false,
checkboxSelector: {
// columnIndexPosition: 1,
diff --git a/demos/aurelia/src/examples/slickgrid/example47-detail-view.ts b/demos/aurelia/src/examples/slickgrid/example47-detail-view.ts
index d11a5b648b..a79c5cee02 100644
--- a/demos/aurelia/src/examples/slickgrid/example47-detail-view.ts
+++ b/demos/aurelia/src/examples/slickgrid/example47-detail-view.ts
@@ -1,4 +1,4 @@
-import type { SlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin';
+import type { AureliaSlickRowDetailView } from '@slickgrid-universal/aurelia-row-detail-plugin';
import { bindable } from 'aurelia';
import type { SlickDataView, SlickGrid } from 'aurelia-slickgrid';
import { showToast } from './utilities.js';
@@ -20,7 +20,7 @@ export class Example47DetailView {
@bindable() model!: Item;
// you also have access to the following objects (it must match the exact property names shown below)
- @bindable() addon!: SlickRowDetailView; // row detail addon instance
+ @bindable() addon!: AureliaSlickRowDetailView; // row detail addon instance
@bindable() grid!: SlickGrid;
@bindable() dataView!: SlickDataView;
diff --git a/demos/aurelia/src/examples/slickgrid/example47.ts b/demos/aurelia/src/examples/slickgrid/example47.ts
index 007581037e..77b5cbc57e 100644
--- a/demos/aurelia/src/examples/slickgrid/example47.ts
+++ b/demos/aurelia/src/examples/slickgrid/example47.ts
@@ -1,3 +1,4 @@
+import { AureliaSlickRowDetailView } from '@slickgrid-universal/aurelia-row-detail-plugin';
import { bindable } from 'aurelia';
import {
Aggregators,
@@ -57,6 +58,7 @@ export class Example47 {
// option 1
// return this.extensions.rowDetailView.instance || {};
+ // return this.aureliaGrid?.extensions.rowDetailView.instance || {};
// OR option 2
return this.aureliaGrid?.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView);
@@ -181,6 +183,7 @@ export class Example47 {
enableRowDetailView: true,
rowTopOffsetRenderType: 'top', // RowDetail and/or RowSpan don't render well with "transform", you should use "top"
darkMode: this._darkMode,
+ externalResources: [AureliaSlickRowDetailView],
rowDetailView: {
// optionally change the column index position of the icon (defaults to 0)
// columnIndexPosition: 1,
@@ -209,7 +212,7 @@ export class Example47 {
// Optionally pass your Parent Component reference to your Child Component (row detail component)
parentRef: this,
},
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: true,
},
diff --git a/demos/aurelia/src/examples/slickgrid/example48.html b/demos/aurelia/src/examples/slickgrid/example48.html
index e484fb2e9a..143d058e6f 100644
--- a/demos/aurelia/src/examples/slickgrid/example48.html
+++ b/demos/aurelia/src/examples/slickgrid/example48.html
@@ -16,7 +16,7 @@
SlickHybridSelectionModel This Selection Model is an hybrid approach that uses a combination of the row or cell selections
- depending on certain conditions. Use enableHybridSelection grid option to enable the new Hybrid Selection Model.
+ depending on certain conditions.
1. clicking on the first column (id) will use RowSelectionModel because of our configuration of
@@ -54,6 +54,7 @@
+
Spreadsheet with drag-fill, hybrid selection model. Type a few values in the grid and then select those cells and use the bottom right
drag handle spread the selection and auto-fill the values to other cells. Use onDragReplaceCells event to customize the
- drag-fill behavior. Use enableHybridSelection grid option to enable the new Hybrid Selection Model.
+ drag-fill behavior. Use { enableSelection: true, selectionOptions: { selectionType: 'mixed' }}
+ grid option to enable the new Hybrid Selection Model.
diff --git a/demos/aurelia/src/examples/slickgrid/example49.ts b/demos/aurelia/src/examples/slickgrid/example49.ts
index 5c292a7acf..c720da9654 100644
--- a/demos/aurelia/src/examples/slickgrid/example49.ts
+++ b/demos/aurelia/src/examples/slickgrid/example49.ts
@@ -76,10 +76,11 @@ export class Example49 {
editorNavigateOnArrows: true, // enable editor navigation using arrow keys
// enable new hybrid selection model (rows & cells)
- enableHybridSelection: true,
- rowSelectionOptions: {
- selectActiveRow: true,
+ enableSelection: true,
+ selectionOptions: {
rowSelectColumnIds: ['selector'],
+ selectActiveRow: true,
+ selectionType: 'mixed',
},
// when using the ExcelCopyBuffer, you can see what the selection range is
diff --git a/demos/aurelia/src/examples/slickgrid/example5.ts b/demos/aurelia/src/examples/slickgrid/example5.ts
index 9357d4595e..cb02baa067 100644
--- a/demos/aurelia/src/examples/slickgrid/example5.ts
+++ b/demos/aurelia/src/examples/slickgrid/example5.ts
@@ -3,7 +3,6 @@ import { newInstanceOf, resolve } from '@aurelia/kernel';
import { GridOdataService, type OdataOption, type OdataServiceApi } from '@slickgrid-universal/odata';
import {
Filters,
- OperatorType,
type AureliaGridInstance,
type Column,
type GridOption,
@@ -96,13 +95,13 @@ export class Example5 {
hideInColumnTitleRow: true,
},
compoundOperatorAltTexts: {
- // where '=' is any of the `OperatorString` type shown above
+ // where '=' is any of the `OperatorType` type shown above
text: { Custom: { operatorAlt: '%%', descAlt: 'SQL Like' } },
},
enableCellNavigation: true,
enableFiltering: true,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
enablePagination: true, // you could optionally disable the Pagination
pagination: {
pageSizes: [10, 20, 50, 100, 500, 50000],
@@ -111,7 +110,7 @@ export class Example5 {
},
presets: {
// you can also type operator as string, e.g.: operator: 'EQ'
- filters: [{ columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal }],
+ filters: [{ columnId: 'gender', searchTerms: ['male'], operator: '=' }],
sorters: [
// direction can be written as 'asc' (uppercase or lowercase) and/or use the SortDirection type
{ columnId: 'name', direction: 'asc' },
@@ -125,7 +124,7 @@ export class Example5 {
enableSelect: this.isSelectEnabled,
enableExpand: this.isExpandEnabled,
filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => {
- if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
+ if (columnFilterOperator === 'Custom' && columnDef?.id === 'name') {
let matchesSearch = searchValues[0].replace(/\*/g, '.*');
matchesSearch = matchesSearch.slice(0, 1) + CARET_HTML_ESCAPED + matchesSearch.slice(1);
matchesSearch = matchesSearch.slice(0, -1) + "$'";
@@ -411,7 +410,7 @@ export class Example5 {
setFiltersDynamically() {
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
this.aureliaGrid.filterService.updateFilters([
- // { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
+ // { columnId: 'gender', searchTerms: ['male'], operator: '=' },
{ columnId: 'name', searchTerms: ['A'], operator: 'a*' },
]);
}
diff --git a/demos/aurelia/src/examples/slickgrid/example50.ts b/demos/aurelia/src/examples/slickgrid/example50.ts
index 90a5cfc7c4..c16ae4fcc9 100644
--- a/demos/aurelia/src/examples/slickgrid/example50.ts
+++ b/demos/aurelia/src/examples/slickgrid/example50.ts
@@ -58,8 +58,8 @@ export class Example50 {
gridHeight: 225,
gridWidth: 800,
rowHeight: 33,
- enableHybridSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
selectionType: 'row',
},
};
diff --git a/demos/aurelia/src/examples/slickgrid/example51.html b/demos/aurelia/src/examples/slickgrid/example51.html
new file mode 100644
index 0000000000..5255b2d467
--- /dev/null
+++ b/demos/aurelia/src/examples/slickgrid/example51.html
@@ -0,0 +1,75 @@
+
+
+ Example 51: Menus with Slots
+
+
+ code
+
+
+
+
+
+
+
+
+
+ Menu Slots Demo with Custom Renderer
+
+
+ Click on the menu buttons to see the new single slot functionality working across all menu types (Header Menu, Cell
+ Menu, Context Menu, Grid Menu):
+
+
+ Note: The demo focuses on the custom rendering capability via slotRenderer and
+ defaultMenuItemRenderer, which work across all menu plugins (SlickHeaderMenu, SlickCellMenu, SlickContextMenu,
+ SlickGridMenu). Also note that the keyboard shortcuts displayed in the menus (e.g., Alt+โ, F5) are for
+ demo purposes only and do not actually trigger any actions.
+
+
-
+
+ reactGrid1Ready($event.detail)}
+ />
diff --git a/demos/react/src/examples/slickgrid/Example15.tsx b/demos/react/src/examples/slickgrid/Example15.tsx
index d69b346c23..24f37ad400 100644
--- a/demos/react/src/examples/slickgrid/Example15.tsx
+++ b/demos/react/src/examples/slickgrid/Example15.tsx
@@ -51,8 +51,6 @@ const Example15: React.FC = () => {
/** Clear the Grid State from Local Storage and reset the grid to it's original state */
function clearGridStateFromLocalStorage() {
- // reactGridRef.current?.slickGrid.setColumns(reactGridRef.current?.gridService.getAllColumnDefinitions());
- // reactGridRef.current?.slickGrid.autosizeColumns();
reactGridRef.current?.gridService.resetGrid(getColumnDefinitions());
reactGridRef.current?.paginationService!.changeItemPerPage(DEFAULT_PAGE_SIZE);
setTimeout(() => (localStorage[LOCAL_STORAGE_KEY] = null));
diff --git a/demos/react/src/examples/slickgrid/Example16.tsx b/demos/react/src/examples/slickgrid/Example16.tsx
index bc13249039..05675ee4da 100644
--- a/demos/react/src/examples/slickgrid/Example16.tsx
+++ b/demos/react/src/examples/slickgrid/Example16.tsx
@@ -81,8 +81,8 @@ const Example16: React.FC = () => {
hideInFilterHeaderRow: false,
hideInColumnTitleRow: true,
},
- enableRowSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
diff --git a/demos/react/src/examples/slickgrid/Example18.tsx b/demos/react/src/examples/slickgrid/Example18.tsx
index 0ec066a34c..2deea88e13 100644
--- a/demos/react/src/examples/slickgrid/Example18.tsx
+++ b/demos/react/src/examples/slickgrid/Example18.tsx
@@ -245,16 +245,17 @@ const Example18: React.FC = () => {
initialGroupBy: ['duration'],
},
darkMode,
- enableTextExport: true,
- enableExcelExport: true,
excelExportOptions: { sanitizeDataExport: true },
textExportOptions: { sanitizeDataExport: true },
- enablePdfExport: true,
pdfExportOptions: {
repeatHeadersOnEachPage: true, // defaults to true
documentTitle: 'Grouping Grid',
},
externalResources: [excelExportService, pdfExportService, textExportService],
+ // -- NOTE: registered resources are auto-enabled
+ // enableTextExport: true,
+ // enablePdfExport: true,
+ // enableExcelExport: true,
};
setColumnDefinitions(columnDefinitions);
diff --git a/demos/react/src/examples/slickgrid/Example19.tsx b/demos/react/src/examples/slickgrid/Example19.tsx
index bfeb2fc795..00bb3a839f 100644
--- a/demos/react/src/examples/slickgrid/Example19.tsx
+++ b/demos/react/src/examples/slickgrid/Example19.tsx
@@ -1,16 +1,7 @@
import { type EventPubSubService } from '@slickgrid-universal/event-pub-sub';
+import { ReactSlickRowDetailView } from '@slickgrid-universal/react-row-detail-plugin';
import React, { useEffect, useRef, useState } from 'react';
-import {
- Editors,
- ExtensionName,
- Filters,
- Formatters,
- SlickgridReact,
- SlickRowDetailView,
- type Column,
- type GridOption,
- type SlickgridReactInstance,
-} from 'slickgrid-react';
+import { Editors, Filters, Formatters, SlickgridReact, type Column, type GridOption, type SlickgridReactInstance } from 'slickgrid-react';
import { ExampleDetailPreload } from './Example-detail-preload.js';
import Example19DetailView from './Example19-detail-view.js';
@@ -41,7 +32,7 @@ const Example19: React.FC = () => {
}, []);
function rowDetailInstance() {
- return reactGridRef.current?.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView) as SlickRowDetailView;
+ return reactGridRef.current?.extensionService.getExtensionInstanceByName('rowDetailView') as ReactSlickRowDetailView;
}
const getColumnsDefinition = (): Column[] => {
@@ -173,8 +164,8 @@ const Example19: React.FC = () => {
darkMode,
datasetIdPropertyName: 'rowId',
preRegisterExternalExtensions: (pubSubService) => {
- const rowDetail = new SlickRowDetailView(pubSubService as EventPubSubService);
- return [{ name: ExtensionName.rowDetailView, instance: rowDetail }];
+ const rowDetail = new ReactSlickRowDetailView(pubSubService as EventPubSubService);
+ return [{ name: 'rowDetailView', instance: rowDetail }];
},
rowDetailView: {
process: (item) => simulateServerAsyncCall(item),
@@ -192,7 +183,7 @@ const Example19: React.FC = () => {
return true;
},
},
- rowSelectionOptions: {
+ selectionOptions: {
selectActiveRow: true,
},
};
diff --git a/demos/react/src/examples/slickgrid/Example21.tsx b/demos/react/src/examples/slickgrid/Example21.tsx
index 9e039ec98b..37f19d1744 100644
--- a/demos/react/src/examples/slickgrid/Example21.tsx
+++ b/demos/react/src/examples/slickgrid/Example21.tsx
@@ -1,12 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
-import {
- Formatters,
- SlickgridReact,
- type Column,
- type GridOption,
- type OperatorString,
- type SlickgridReactInstance,
-} from 'slickgrid-react';
+import { Formatters, SlickgridReact, type Column, type GridOption, type OperatorType, type SlickgridReactInstance } from 'slickgrid-react';
import './example21.scss';
const Example21: React.FC = () => {
@@ -14,7 +7,7 @@ const Example21: React.FC = () => {
const [dataset] = useState(getData());
const [gridOptions, setGridOptions] = useState(undefined);
const reactGridRef = useRef(null);
- const [operatorList] = useState(['=', '<', '<=', '>', '>=', '<>', 'StartsWith', 'EndsWith']);
+ const [operatorList] = useState(['=', '<', '<=', '>', '>=', '<>', 'StartsWith', 'EndsWith']);
const [selectedOperator, setSelectedOperator] = useState('');
const [searchValue, setSearchValue] = useState('');
const [selectedColumn, setSelectedColumn] = useState();
@@ -107,7 +100,7 @@ const Example21: React.FC = () => {
alwaysShowVerticalScroll: false,
enableColumnPicker: true,
enableCellNavigation: true,
- enableRowSelection: true,
+ enableSelection: true,
};
setColumnDefinitions(columnDefinitions);
@@ -163,7 +156,7 @@ const Example21: React.FC = () => {
function updateFilter() {
reactGridRef.current?.filterService.updateSingleFilter({
columnId: `${selectedColumn?.id ?? ''}`,
- operator: selectedOperator as OperatorString,
+ operator: selectedOperator as OperatorType,
searchTerms: [searchValue || ''],
});
}
diff --git a/demos/react/src/examples/slickgrid/Example23.tsx b/demos/react/src/examples/slickgrid/Example23.tsx
index 86ed925a91..b1d3c6ce8f 100644
--- a/demos/react/src/examples/slickgrid/Example23.tsx
+++ b/demos/react/src/examples/slickgrid/Example23.tsx
@@ -7,7 +7,6 @@ import { withTranslation } from 'react-i18next';
import {
Filters,
Formatters,
- OperatorType,
SlickgridReact,
type Column,
type CurrentFilter,
@@ -104,7 +103,7 @@ const Example23: React.FC = () => {
filter: {
model: Filters.sliderRange,
maxValue: 100, // or you can use the options as well
- operator: OperatorType.rangeInclusive, // defaults to inclusive
+ operator: 'RangeInclusive', // defaults to inclusive
options: {
hideSliderNumbers: false, // you can hide/show the slider numbers on both side
min: 0,
@@ -152,7 +151,7 @@ const Example23: React.FC = () => {
filterable: true,
filter: {
model: Filters.input,
- operator: OperatorType.rangeExclusive, // defaults to exclusive
+ operator: 'RangeExclusive', // defaults to exclusive
},
},
{
@@ -310,8 +309,8 @@ const Example23: React.FC = () => {
switch (newPredefinedFilter) {
case 'currentYearTasks':
filters = [
- { columnId: 'finish', operator: OperatorType.rangeInclusive, searchTerms: [`${currentYear}-01-01`, `${currentYear}-12-31`] },
- { columnId: 'completed', operator: OperatorType.equal, searchTerms: [true] },
+ { columnId: 'finish', operator: 'RangeInclusive', searchTerms: [`${currentYear}-01-01`, `${currentYear}-12-31`] },
+ { columnId: 'completed', operator: '=', searchTerms: [true] },
];
break;
case 'nextYearTasks':
diff --git a/demos/react/src/examples/slickgrid/Example24.tsx b/demos/react/src/examples/slickgrid/Example24.tsx
index ec2e2c4095..839f631e9f 100644
--- a/demos/react/src/examples/slickgrid/Example24.tsx
+++ b/demos/react/src/examples/slickgrid/Example24.tsx
@@ -3,7 +3,6 @@ import i18next from 'i18next';
import React, { useEffect, useRef, useState } from 'react';
import { withTranslation } from 'react-i18next';
import {
- ExtensionName,
Filters,
Formatters,
SlickgridReact,
@@ -85,11 +84,11 @@ const Example24: React.FC = () => {
}
function cellMenuInstance() {
- return reactGridRef.current?.extensionService.getExtensionInstanceByName(ExtensionName.cellMenu);
+ return reactGridRef.current?.extensionService.getExtensionInstanceByName('cellMenu');
}
function contextMenuInstance() {
- return reactGridRef.current?.extensionService.getExtensionInstanceByName(ExtensionName.contextMenu);
+ return reactGridRef.current?.extensionService.getExtensionInstanceByName('contextMenu');
}
/* Define grid Options and Columns */
@@ -348,7 +347,7 @@ const Example24: React.FC = () => {
onCommand: (_e, args) => executeCommand(_e, args),
onOptionSelected: (_e, args) => {
// change "Completed" property with new option selected from the Cell Menu
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && dataContext.hasOwnProperty('completed')) {
dataContext.completed = args.item.option;
reactGridRef.current?.gridService.updateItem(dataContext);
@@ -429,7 +428,7 @@ const Example24: React.FC = () => {
// optionally and conditionally define when the the menu is usable,
// this should be used with a custom formatter to show/hide/disable the menu
menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return dataContext.id < 21; // say we want to display the menu only from Task 0 to 20
},
// which column to show the command list? when not defined it will be shown over all columns
@@ -460,7 +459,7 @@ const Example24: React.FC = () => {
},
// only show command to 'Help' when the task is Not Completed
itemVisibilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -526,7 +525,7 @@ const Example24: React.FC = () => {
textCssClass: 'italic',
// only enable this option when the task is Not Completed
itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
// you can use the 'action' callback and/or subscribe to the 'onCallback' event, they both have the same arguments
@@ -548,7 +547,7 @@ const Example24: React.FC = () => {
disabled: true,
// only shown when the task is Not Completed
itemVisibilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -579,7 +578,7 @@ const Example24: React.FC = () => {
// subscribe to Context Menu onOptionSelected event (or use the action callback on each option)
onOptionSelected: (_e, args) => {
// change Priority
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && dataContext.hasOwnProperty('priority')) {
dataContext.priority = args.item.option;
reactGridRef.current?.gridService.updateItem(dataContext);
diff --git a/demos/react/src/examples/slickgrid/Example25.tsx b/demos/react/src/examples/slickgrid/Example25.tsx
index a18b7fbbad..25d5893fb0 100644
--- a/demos/react/src/examples/slickgrid/Example25.tsx
+++ b/demos/react/src/examples/slickgrid/Example25.tsx
@@ -1,14 +1,6 @@
import { GraphqlService, type GraphqlResult, type GraphqlServiceApi } from '@slickgrid-universal/graphql';
import React, { useEffect, useState } from 'react';
-import {
- Filters,
- Formatters,
- OperatorType,
- SlickgridReact,
- type Column,
- type GridOption,
- type MultipleSelectOption,
-} from 'slickgrid-react';
+import { Filters, Formatters, SlickgridReact, type Column, type GridOption, type MultipleSelectOption } from 'slickgrid-react';
import './example25.scss'; // provide custom CSS/SASS styling
const COUNTRIES_API = 'https://countries.trevorblades.com/';
@@ -78,7 +70,7 @@ const Example25: React.FC = () => {
filter: {
model: Filters.multipleSelect,
collectionAsync: getLanguages(),
- operator: OperatorType.inContains,
+ operator: 'IN_CONTAINS',
collectionOptions: {
addBlankEntry: true,
// the data is not at the root of the array, so we must tell the Select Filter where to pull the data
@@ -106,7 +98,7 @@ const Example25: React.FC = () => {
filter: {
model: Filters.multipleSelect,
collectionAsync: getLanguages(),
- operator: OperatorType.inContains,
+ operator: 'IN_CONTAINS',
collectionOptions: {
addBlankEntry: true,
// the data is not at the root of the array, so we must tell the Select Filter where to pull the data
@@ -268,7 +260,7 @@ const Example25: React.FC = () => {
// function setFiltersDynamically() {
// // we can Set Filters Dynamically (or different filters) afterward through the FilterService
// reactGridRef.current?.filterService.updateFilters([
- // { columnId: 'countryName', searchTerms: ['G'], operator: OperatorType.startsWith },
+ // { columnId: 'countryName', searchTerms: ['G'], operator: 'StartsWith' },
// ]);
// }
diff --git a/demos/react/src/examples/slickgrid/Example3.tsx b/demos/react/src/examples/slickgrid/Example3.tsx
index 9d6570f883..9be182fa8e 100644
--- a/demos/react/src/examples/slickgrid/Example3.tsx
+++ b/demos/react/src/examples/slickgrid/Example3.tsx
@@ -4,7 +4,6 @@ import {
Editors,
Filters,
Formatters,
- OperatorType,
SlickGlobalEditorLock,
SlickgridReact,
SortComparers,
@@ -31,7 +30,7 @@ const NB_ITEMS = 100;
// you can create custom validator to pass to an inline editor
const myCustomTitleValidator: EditorValidator = (value: any) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
- // const grid = args && args.grid;
+ // const grid = args?.grid;
// const gridOptions = grid.getOptions() as GridOption;
// const i18n = gridOptions.i18n;
@@ -197,7 +196,7 @@ const Example3: React.FC = () => {
collectionFilterBy: {
property: 'value',
value: 0,
- operator: OperatorType.notEqual,
+ operator: '!=',
},
model: Editors.singleSelect,
// validator: (value, args) => {
@@ -418,7 +417,7 @@ const Example3: React.FC = () => {
separatorBetweenTextLabels: ' ',
},
model: Filters.multipleSelect,
- operator: OperatorType.inContains,
+ operator: 'IN_CONTAINS',
},
},
]);
diff --git a/demos/react/src/examples/slickgrid/Example30.tsx b/demos/react/src/examples/slickgrid/Example30.tsx
index c3fc545d9a..b246c7b786 100644
--- a/demos/react/src/examples/slickgrid/Example30.tsx
+++ b/demos/react/src/examples/slickgrid/Example30.tsx
@@ -482,7 +482,7 @@ const Example30: React.FC = () => {
},
externalResources: [new ExcelExportService(), new SlickCustomTooltip(), compositeEditorInstanceRef.current!],
enableFiltering: true,
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
@@ -490,7 +490,7 @@ const Example30: React.FC = () => {
showPreHeaderPanel: true,
preHeaderPanelHeight: 28,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
multiSelect: false,
checkboxSelector: {
hideInFilterHeaderRow: false,
diff --git a/demos/react/src/examples/slickgrid/Example31.tsx b/demos/react/src/examples/slickgrid/Example31.tsx
index d8d6ff8e71..0d8de70ba0 100644
--- a/demos/react/src/examples/slickgrid/Example31.tsx
+++ b/demos/react/src/examples/slickgrid/Example31.tsx
@@ -5,7 +5,6 @@ import { Observable, of, type Subject } from 'rxjs';
import {
Editors,
Filters,
- OperatorType,
SlickgridReact,
type Column,
type GridOption,
@@ -100,7 +99,7 @@ const Example31: React.FC = () => {
enableCellNavigation: true,
enableFiltering: true,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
enablePagination: true, // you could optionally disable the Pagination
pagination: {
pageSizes: [10, 20, 50, 100, 500],
@@ -109,7 +108,7 @@ const Example31: React.FC = () => {
},
presets: {
// you can also type operator as string, e.g.: operator: 'EQ'
- filters: [{ columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal }],
+ filters: [{ columnId: 'gender', searchTerms: ['male'], operator: '=' }],
sorters: [
// direction can be written as 'asc' (uppercase or lowercase) and/or use the SortDirection type
{ columnId: 'name', direction: 'asc' },
@@ -386,7 +385,7 @@ const Example31: React.FC = () => {
function setFiltersDynamically() {
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
reactGridRef.current?.filterService.updateFilters([
- // { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
+ // { columnId: 'gender', searchTerms: ['male'], operator: '=' },
{ columnId: 'name', searchTerms: ['A'], operator: 'a*' },
]);
}
diff --git a/demos/react/src/examples/slickgrid/Example32.tsx b/demos/react/src/examples/slickgrid/Example32.tsx
index 074e920f97..220bfb1eba 100644
--- a/demos/react/src/examples/slickgrid/Example32.tsx
+++ b/demos/react/src/examples/slickgrid/Example32.tsx
@@ -491,13 +491,13 @@ const Example32: React.FC = () => {
},
externalResources: [new ExcelExportService()],
enableFiltering: true,
- enableRowSelection: true,
+ enableSelection: true,
enableCheckboxSelector: true,
checkboxSelector: {
hideInFilterHeaderRow: false,
hideInColumnTitleRow: true,
},
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
@@ -585,7 +585,7 @@ const Example32: React.FC = () => {
// just for demo purposes, set it back to its original width
const columns = reactGridRef.current?.slickGrid.getColumns() as Column[];
columns.forEach((col) => (col.width = col.originalWidth));
- reactGridRef.current?.slickGrid.setColumns(columns);
+ reactGridRef.current?.slickGrid.updateColumns();
reactGridRef.current?.slickGrid.autosizeColumns();
setIsUsingDefaultResize(true);
}
diff --git a/demos/react/src/examples/slickgrid/Example33.tsx b/demos/react/src/examples/slickgrid/Example33.tsx
index 3f8acb60b4..fd4523d19e 100644
--- a/demos/react/src/examples/slickgrid/Example33.tsx
+++ b/demos/react/src/examples/slickgrid/Example33.tsx
@@ -2,10 +2,10 @@ import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin';
import { ExcelExportService } from '@slickgrid-universal/excel-export';
import React, { useEffect, useRef, useState } from 'react';
import {
+ createDomElement,
Editors,
Filters,
Formatters,
- OperatorType,
SlickgridReact,
type Column,
type EditCommand,
@@ -139,6 +139,31 @@ const Example33: React.FC = () => {
// maxHeight: 30,
},
},
+ {
+ id: 'button',
+ name: 'Button Tooltip',
+ field: 'title',
+ width: 100,
+ minWidth: 100,
+ filterable: true,
+ excludeFromExport: true,
+ formatter: (_row: number, _cell: number, value: any) => {
+ const button = createDomElement('button', {
+ className: 'btn btn-outline-secondary btn-icon btn-sm',
+ title: 'This is the button tooltip',
+ });
+ const icon = createDomElement('i', { className: 'mdi mdi-information', title: 'icon tooltip' });
+ const text = createDomElement('span', { textContent: 'Hello Task' });
+ button.appendChild(icon);
+ button.appendChild(text);
+ button.addEventListener('click', () => alert(`Clicked button for ${value}`));
+ return button;
+ },
+ // define tooltip options here OR for the entire grid via the grid options (cell tooltip options will have precedence over grid options)
+ customTooltip: {
+ useRegularTooltip: true, // note regular tooltip will try to find a "title" attribute in the cell formatter (it won't work without a cell formatter)
+ },
+ },
{
id: 'cost',
name: 'Cost',
@@ -318,7 +343,7 @@ const Example33: React.FC = () => {
collectionOptions: { separatorBetweenTextLabels: ' ' },
options: { minHeight: 70 } as MultipleSelectOption,
model: Filters.multipleSelect,
- operator: OperatorType.inContains,
+ operator: 'IN_CONTAINS',
},
},
{
@@ -401,19 +426,21 @@ const Example33: React.FC = () => {
headerFormatter,
headerRowFormatter,
usabilityOverride: (args) => args.cell !== 0 && args?.column?.id !== 'action', // don't show on first/last columns
+ observeAllTooltips: true, // observe all elements with title/data-slick-tooltip attributes (not just SlickGrid elements)
+ observeTooltipContainer: 'body', // defaults to 'body', target a specific container (only works when observeAllTooltips is enabled)
},
presets: {
filters: [{ columnId: 'prerequisites', searchTerms: [1, 3, 5, 7, 9, 12, 15, 18, 21, 25, 28, 29, 30, 32, 34] }],
},
- rowHeight: 33,
+ rowHeight: 38,
enableFiltering: true,
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
showCustomFooter: true,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
checkboxSelector: {
hideInFilterHeaderRow: false,
hideInColumnTitleRow: true,
@@ -430,7 +457,7 @@ const Example33: React.FC = () => {
onCommand: (e, args) => executeCommand(e, args),
onOptionSelected: (_e, args) => {
// change "Completed" property with new option selected from the Cell Menu
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && dataContext.hasOwnProperty('completed')) {
dataContext.completed = args.item.option;
reactGridRef.current?.gridService.updateItem(dataContext);
@@ -464,7 +491,7 @@ const Example33: React.FC = () => {
id: i,
title: 'Task ' + i,
duration: Math.round(Math.random() * 100),
- description: `This is a sample task description.\nIt can be multiline\r\rAnother line...`,
+ description: i > 500 ? null : `This is a sample task description.\nIt can be multiline\r\rAnother line...`,
percentComplete: Math.floor(Math.random() * (100 - 5 + 1) + 5),
start: new Date(randomYear, randomMonth, randomDay),
finish: randomFinish < new Date() ? '' : randomFinish, // make sure the random date is earlier than today
@@ -574,6 +601,20 @@ const Example33: React.FC = () => {
reactGridRef.current?.resizerService.resizeGrid(0);
}
+ function setFiltersDynamically(operator: string) {
+ const operatorType = operator === '=' ? '=' : '!=';
+ reactGridRef.current?.filterService.updateFilters(
+ [
+ {
+ columnId: 'desc',
+ operator: operatorType,
+ searchTerms: [''],
+ },
+ ],
+ true
+ );
+ }
+
return !gridOptions ? (
''
) : (
@@ -625,6 +666,24 @@ const Example33: React.FC = () => {
value={serverWaitDelay}
onInput={($event) => handleServerDelayInputChange($event)}
/>
+
+
Lazy loading collection...
diff --git a/demos/react/src/examples/slickgrid/Example38.tsx b/demos/react/src/examples/slickgrid/Example38.tsx
index 9cd4aecdb0..635ddb2a07 100644
--- a/demos/react/src/examples/slickgrid/Example38.tsx
+++ b/demos/react/src/examples/slickgrid/Example38.tsx
@@ -97,7 +97,7 @@ const Example38: React.FC = () => {
enableCellNavigation: true,
enableFiltering: true,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
enableGrouping: true,
presets: {
// NOTE: pagination preset is NOT supported with infinite scroll
diff --git a/demos/react/src/examples/slickgrid/Example4.tsx b/demos/react/src/examples/slickgrid/Example4.tsx
index 876eee3977..1f72903327 100644
--- a/demos/react/src/examples/slickgrid/Example4.tsx
+++ b/demos/react/src/examples/slickgrid/Example4.tsx
@@ -4,7 +4,6 @@ import { useEffect, useState } from 'react';
import {
Filters,
Formatters,
- OperatorType,
SlickgridReact,
type Column,
type GridOption,
@@ -78,12 +77,12 @@ const Example4: React.FC = () => {
collectionFilterBy: [
{
property: 'value',
- operator: OperatorType.notEqual,
+ operator: '!=',
value: 360,
},
{
property: 'value',
- operator: OperatorType.notEqual,
+ operator: '!=',
value: 365,
},
],
diff --git a/demos/react/src/examples/slickgrid/Example41.tsx b/demos/react/src/examples/slickgrid/Example41.tsx
index 8fea240bfc..62847530ea 100644
--- a/demos/react/src/examples/slickgrid/Example41.tsx
+++ b/demos/react/src/examples/slickgrid/Example41.tsx
@@ -53,11 +53,12 @@ const Example41: React.FC = () => {
gridWidth: 800,
rowHeight: 33,
enableCellNavigation: true,
- enableRowSelection: true,
+ enableSelection: true,
enableRowMoveManager: true,
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
+ selectionType: 'row',
},
rowMoveManager: {
columnIndexPosition: 0,
diff --git a/demos/react/src/examples/slickgrid/Example42.tsx b/demos/react/src/examples/slickgrid/Example42.tsx
index 557157c929..7a13751cce 100644
--- a/demos/react/src/examples/slickgrid/Example42.tsx
+++ b/demos/react/src/examples/slickgrid/Example42.tsx
@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from 'react';
import {
Filters,
Formatters,
- OperatorType,
SlickgridReact,
type Column,
type GridOption,
@@ -68,7 +67,7 @@ const Example42: React.FC = () => {
filter: {
model: Filters.sliderRange,
maxValue: 100, // or you can use the options as well
- operator: OperatorType.rangeInclusive, // defaults to inclusive
+ operator: 'RangeInclusive', // defaults to inclusive
options: {
hideSliderNumbers: false, // you can hide/show the slider numbers on both side
min: 0,
@@ -114,7 +113,7 @@ const Example42: React.FC = () => {
filterable: true,
filter: {
model: Filters.input,
- operator: OperatorType.rangeExclusive, // defaults to exclusive
+ operator: 'RangeExclusive', // defaults to exclusive
},
},
{
diff --git a/demos/react/src/examples/slickgrid/Example43.tsx b/demos/react/src/examples/slickgrid/Example43.tsx
index 6ade36e3df..b34f643539 100644
--- a/demos/react/src/examples/slickgrid/Example43.tsx
+++ b/demos/react/src/examples/slickgrid/Example43.tsx
@@ -418,7 +418,8 @@ export default function Example43() {
}
}
- // update column definitions
+ // 1. update column definitions via grid.setColumns()
+ // this will shift colspan/rowspan to the left or right accordingly
const cols: Column[] = reactGrid?.slickGrid.getColumns() || [];
if (newShowEmployeeId) {
cols.unshift({ id: 'employeeID', name: 'Employee ID', field: 'employeeID', width: 100 });
@@ -427,6 +428,22 @@ export default function Example43() {
}
reactGrid?.slickGrid.setColumns(cols || []);
+ // --- OR ---
+ // 2. OR update via "hidden" column flag & increase/decrease column index accordingly in the metadata
+ // this approach will keep colspan/rowspan "as-is" but will hide the EmployeeID column
+ /*
+ const colDirIdx = newShowEmployeeId ? -1 : 1;
+ for (const row of Object.keys(this.metadata)) {
+ newMetadata[row] = { columns: {} };
+ for (const col of Object.keys((this.metadata as any)[row].columns)) {
+ newMetadata[row].columns[Number(col) + colDirIdx] = (this.metadata as any)[row].columns[col];
+ }
+ }
+ reactGrid?.slickGrid?.setOptions({ frozenColumn: newShowEmployeeId ? 0 : 1 });
+ reactGrid?.slickGrid?.updateColumnById('employeeID', { hidden: !newShowEmployeeId });
+ reactGrid?.slickGrid?.updateColumns();
+ */
+
// update & remap rowspans
metadataRef.current = newMetadata;
reactGrid?.slickGrid.remapAllColumnsRowSpan();
diff --git a/demos/react/src/examples/slickgrid/Example45.tsx b/demos/react/src/examples/slickgrid/Example45.tsx
index cba90b4e52..7943cf4fde 100644
--- a/demos/react/src/examples/slickgrid/Example45.tsx
+++ b/demos/react/src/examples/slickgrid/Example45.tsx
@@ -1,14 +1,8 @@
import { faker } from '@faker-js/faker';
import { type EventPubSubService } from '@slickgrid-universal/event-pub-sub';
+import { ReactSlickRowDetailView } from '@slickgrid-universal/react-row-detail-plugin';
import React, { useEffect, useRef, useState } from 'react';
-import {
- ExtensionName,
- SlickgridReact,
- SlickRowDetailView,
- type Column,
- type GridOption,
- type SlickgridReactInstance,
-} from 'slickgrid-react';
+import { SlickgridReact, type Column, type GridOption, type SlickgridReactInstance } from 'slickgrid-react';
import Example45DetailView, { type Distributor, type OrderData } from './Example45-detail-view.js';
import { Example45Preload } from './Example45-preload.js';
@@ -47,7 +41,7 @@ const Example45: React.FC = () => {
}, [isUsingInnerGridStatePresets]);
function rowDetailInstance() {
- return reactGridRef.current?.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView) as SlickRowDetailView;
+ return reactGridRef.current?.extensionService.getExtensionInstanceByName('rowDetailView') as ReactSlickRowDetailView;
}
function getColumnDefinitions(): Column[] {
@@ -164,8 +158,8 @@ const Example45: React.FC = () => {
rowHeight: 33,
darkMode,
preRegisterExternalExtensions: (pubSubService) => {
- const rowDetail = new SlickRowDetailView(pubSubService as EventPubSubService);
- return [{ name: ExtensionName.rowDetailView, instance: rowDetail }];
+ const rowDetail = new ReactSlickRowDetailView(pubSubService as EventPubSubService);
+ return [{ name: 'rowDetailView', instance: rowDetail }];
},
rowDetailView: {
process: (item) => simulateServerAsyncCall(item),
diff --git a/demos/react/src/examples/slickgrid/Example46.tsx b/demos/react/src/examples/slickgrid/Example46.tsx
index 3244806113..a03cabe60c 100644
--- a/demos/react/src/examples/slickgrid/Example46.tsx
+++ b/demos/react/src/examples/slickgrid/Example46.tsx
@@ -135,7 +135,7 @@ const Example46: React.FC = () => {
sanitizeDataExport: true,
},
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
multiSelect: false,
checkboxSelector: {
// columnIndexPosition: 1,
diff --git a/demos/react/src/examples/slickgrid/Example47.tsx b/demos/react/src/examples/slickgrid/Example47.tsx
index d1989e5093..41ecbf76b5 100644
--- a/demos/react/src/examples/slickgrid/Example47.tsx
+++ b/demos/react/src/examples/slickgrid/Example47.tsx
@@ -1,4 +1,5 @@
import { type EventPubSubService } from '@slickgrid-universal/event-pub-sub';
+import { ReactSlickRowDetailView } from '@slickgrid-universal/react-row-detail-plugin';
import React, { useEffect, useRef, useState } from 'react';
import {
Aggregators,
@@ -8,7 +9,6 @@ import {
Formatters,
GroupTotalFormatters,
SlickgridReact,
- SlickRowDetailView,
SortComparers,
SortDirectionNumber,
type Column,
@@ -60,7 +60,7 @@ const Example47: React.FC = () => {
}
function rowDetailInstance() {
- return reactGridRef.current?.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView) as SlickRowDetailView;
+ return reactGridRef.current?.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView) as ReactSlickRowDetailView;
}
const getColumnsDefinition = (): Column[] => {
@@ -200,8 +200,8 @@ const Example47: React.FC = () => {
rowTopOffsetRenderType: 'top', // RowDetail and/or RowSpan don't render well with "transform", you should use "top"
darkMode,
preRegisterExternalExtensions: (pubSubService) => {
- const rowDetail = new SlickRowDetailView(pubSubService as EventPubSubService);
- return [{ name: ExtensionName.rowDetailView, instance: rowDetail }];
+ const rowDetail = new ReactSlickRowDetailView(pubSubService as EventPubSubService);
+ return [{ name: 'rowDetailView', instance: rowDetail }];
},
rowDetailView: {
process: (item) => simulateServerAsyncCall(item),
@@ -211,7 +211,7 @@ const Example47: React.FC = () => {
preloadComponent: ExampleDetailPreload,
viewComponent: Example47DetailView,
},
- rowSelectionOptions: {
+ selectionOptions: {
selectActiveRow: true,
},
};
diff --git a/demos/react/src/examples/slickgrid/Example48.tsx b/demos/react/src/examples/slickgrid/Example48.tsx
index c9997e6ee8..f1b82d6265 100644
--- a/demos/react/src/examples/slickgrid/Example48.tsx
+++ b/demos/react/src/examples/slickgrid/Example48.tsx
@@ -91,9 +91,10 @@ const Example48: React.FC = () => {
externalResources: [new ExcelExportService()],
// enable new hybrid selection model (rows & cells)
- enableHybridSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
rowSelectColumnIds: ['id'],
+ selectionType: 'mixed',
},
// when using the ExcelCopyBuffer, you can see what the selection range is
@@ -111,8 +112,8 @@ const Example48: React.FC = () => {
...gridOptions1,
// you can also enable checkbox selection & row selection, make sure to use `rowSelectColumnIds: ['id', '_checkbox_selector']`
enableCheckboxSelector: true,
- enableRowSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
@@ -211,8 +212,7 @@ const Example48: React.FC = () => {
{hideSubTitle ? null : (
SlickHybridSelectionModel This Selection Model is an hybrid approach that uses a combination of the row or cell
- selections depending on certain conditions. Use enableHybridSelection grid option to enable the new Hybrid Selection
- Model.
+ selections depending on certain conditions.
1. clicking on the first column (id) will use RowSelectionModel because of our configuration of
diff --git a/demos/react/src/examples/slickgrid/Example49.tsx b/demos/react/src/examples/slickgrid/Example49.tsx
index 09c5debe6a..2a011ddf22 100644
--- a/demos/react/src/examples/slickgrid/Example49.tsx
+++ b/demos/react/src/examples/slickgrid/Example49.tsx
@@ -75,10 +75,11 @@ const Example49: React.FC = () => {
editorNavigateOnArrows: true, // enable editor navigation using arrow keys
// enable new hybrid selection model (rows & cells)
- enableHybridSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
selectActiveRow: true,
rowSelectColumnIds: ['selector'],
+ selectionType: 'mixed',
},
// when using the ExcelCopyBuffer, you can see what the selection range is
@@ -180,7 +181,9 @@ const Example49: React.FC = () => {
Spreadsheet with drag-fill, hybrid selection model. Type a few values in the grid and then select those cells and use the bottom
right drag handle spread the selection and auto-fill the values to other cells. Use onDragReplaceCells event to
- customize the drag-fill behavior. Use enableHybridSelection grid option to enable the new Hybrid Selection Model.
+ customize the drag-fill behavior. Use{' '}
+ { enableSelection: true, selectionOptions: { selectionType: 'mixed' }}
+ grid option to enable the new Hybrid Selection Model.
diff --git a/demos/react/src/examples/slickgrid/Example5.tsx b/demos/react/src/examples/slickgrid/Example5.tsx
index b29bcf9adf..78ae5bbf0e 100644
--- a/demos/react/src/examples/slickgrid/Example5.tsx
+++ b/demos/react/src/examples/slickgrid/Example5.tsx
@@ -3,7 +3,6 @@ import { GridOdataService, type OdataOption, type OdataServiceApi } from '@slick
import { useEffect, useRef, useState } from 'react';
import {
Filters,
- OperatorType,
SlickgridReact,
type Column,
type GridOption,
@@ -65,13 +64,13 @@ const Example5: React.FC = () => {
hideInColumnTitleRow: true,
},
compoundOperatorAltTexts: {
- // where '=' is any of the `OperatorString` type shown above
+ // where '=' is any of the `OperatorType` type shown above
text: { Custom: { operatorAlt: '%%', descAlt: 'SQL Like' } },
},
enableCellNavigation: true,
enableFiltering: true,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
enablePagination: true, // you could optionally disable the Pagination
pagination: {
pageSizes: [10, 20, 50, 100, 500, 50000],
@@ -80,7 +79,7 @@ const Example5: React.FC = () => {
},
presets: {
// you can also type operator as string, e.g.: operator: 'EQ'
- filters: [{ columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal }],
+ filters: [{ columnId: 'gender', searchTerms: ['male'], operator: '=' }],
sorters: [
// direction can be written as 'asc' (uppercase or lowercase) and/or use the SortDirection type
{ columnId: 'name', direction: 'asc' },
@@ -94,7 +93,7 @@ const Example5: React.FC = () => {
enableSelect: isSelectEnabled,
enableExpand: isExpandEnabled,
filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => {
- if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
+ if (columnFilterOperator === 'Custom' && columnDef?.id === 'name') {
let matchesSearch = searchValues[0].replace(/\*/g, '.*');
matchesSearch = matchesSearch.slice(0, 1) + CARET_HTML_ESCAPED + matchesSearch.slice(1);
matchesSearch = matchesSearch.slice(0, -1) + "$'";
@@ -431,7 +430,7 @@ const Example5: React.FC = () => {
function setFiltersDynamically() {
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
reactGridRef.current?.filterService.updateFilters([
- // { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
+ // { columnId: 'gender', searchTerms: ['male'], operator: '=' },
{ columnId: 'name', searchTerms: ['A'], operator: 'a*' },
]);
}
diff --git a/demos/react/src/examples/slickgrid/Example50.tsx b/demos/react/src/examples/slickgrid/Example50.tsx
index a86cbdb2e6..d316e3b060 100644
--- a/demos/react/src/examples/slickgrid/Example50.tsx
+++ b/demos/react/src/examples/slickgrid/Example50.tsx
@@ -54,8 +54,8 @@ const Example50: React.FC = () => {
gridHeight: 225,
gridWidth: 800,
rowHeight: 33,
- enableHybridSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
selectionType: 'row',
},
};
diff --git a/demos/react/src/examples/slickgrid/Example51.tsx b/demos/react/src/examples/slickgrid/Example51.tsx
new file mode 100644
index 0000000000..28c4504a7e
--- /dev/null
+++ b/demos/react/src/examples/slickgrid/Example51.tsx
@@ -0,0 +1,694 @@
+import { format as tempoFormat } from '@formkit/tempo';
+import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin';
+import React, { useEffect, useRef, useState } from 'react';
+import {
+ Aggregators,
+ createDomElement,
+ Filters,
+ Formatters,
+ SlickgridReact,
+ SortComparers,
+ SortDirectionNumber,
+ type Column,
+ type GridOption,
+ type Grouping,
+ type MenuCommandItem,
+ type SlickgridReactInstance,
+} from 'slickgrid-react';
+import './example51.scss'; // provide custom CSS/SASS styling
+
+const NB_ITEMS = 2000;
+
+interface ReportItem {
+ id: number;
+ title: string;
+ duration: number;
+ cost: number;
+ percentComplete: number;
+ start: Date;
+ finish: Date;
+ action?: string;
+}
+
+const Example51: React.FC = () => {
+ const [columnDefinitions, setColumnDefinitions] = useState[]>([]);
+ const [gridOptions, setGridOptions] = useState();
+ const [dataset] = useState(loadData(NB_ITEMS));
+ const [hideSubTitle, setHideSubTitle] = useState(false);
+
+ const reactGridRef = useRef(null);
+
+ useEffect(() => {
+ defineGrid();
+ }, []);
+
+ function reactGridReady(reactGrid: SlickgridReactInstance) {
+ reactGridRef.current = reactGrid;
+ }
+
+ /* Define grid Options and Columns */
+ function defineGrid() {
+ const columnDefinitions: Column[] = [
+ {
+ id: 'title',
+ name: 'Title',
+ field: 'title',
+ sortable: true,
+ filterable: true,
+ minWidth: 100,
+ // Demo: Header Menu with Slot - complete custom HTML with keyboard shortcuts
+ header: {
+ menu: {
+ commandItems: [
+ {
+ command: 'sort-asc',
+ title: 'Sort Ascending',
+ positionOrder: 50,
+ // Slot renderer replaces entire menu item content (can be HTML string or native DOM elements)
+ slotRenderer: (cmdItem) => `
+
+ Example 51: Menus with Slots
+
+ see
+
+ code
+
+
+
+
+
+
+
+
+ Menu Slots Demo with Custom Renderer
+
+
+ Click on the menu buttons to see the new single slot functionality working across all menu types (Header Menu,
+ Cell Menu, Context Menu, Grid Menu):
+
+
+
+ Note: The demo focuses on the custom rendering capability via slotRenderer and
+ defaultMenuItemRenderer, which work across all menu plugins (SlickHeaderMenu, SlickCellMenu, SlickContextMenu,
+ SlickGridMenu). Also note that the keyboard shortcuts displayed in the menus (e.g., Alt+โ, F5) are for
+ demo purposes only and do not actually trigger any actions.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ reactGridReady($event.detail)}
+ />
+
+
+ );
+};
+
+export default Example51;
diff --git a/demos/react/src/examples/slickgrid/Example6.tsx b/demos/react/src/examples/slickgrid/Example6.tsx
index a90768193e..689eb5298e 100644
--- a/demos/react/src/examples/slickgrid/Example6.tsx
+++ b/demos/react/src/examples/slickgrid/Example6.tsx
@@ -11,9 +11,7 @@ import { withTranslation } from 'react-i18next';
import {
Filters,
Formatters,
- OperatorType,
SlickgridReact,
- SortDirection,
type Column,
type CursorPageInfo,
type GridOption,
@@ -222,14 +220,14 @@ const Example6: React.FC = () => {
{ columnId: 'finish', width: 130 },
],
filters: [
- { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
- { columnId: 'name', searchTerms: ['Joh*oe'], operator: OperatorType.startsWithEndsWith },
+ { columnId: 'gender', searchTerms: ['male'], operator: '=' },
+ { columnId: 'name', searchTerms: ['Joh*oe'], operator: 'StartsWithEndsWith' },
{ columnId: 'company', searchTerms: ['xyz'], operator: 'IN' },
- { columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: OperatorType.rangeInclusive },
+ { columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: 'RangeInclusive' },
],
sorters: [
{ columnId: 'name', direction: 'asc' },
- { columnId: 'company', direction: SortDirection.DESC },
+ { columnId: 'company', direction: 'DESC' },
],
pagination: { pageNumber: isWithCursorRef.current ? 1 : 2, pageSize: 20 },
},
@@ -240,7 +238,7 @@ const Example6: React.FC = () => {
addLocaleIntoQuery: true,
extraQueryArguments: [{ field: 'userId', value: 123 }],
filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => {
- if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
+ if (columnFilterOperator === 'Custom' && columnDef?.id === 'name') {
return { field: fieldName, operator: 'Like', value: searchValues[0] };
}
return;
@@ -339,11 +337,11 @@ const Example6: React.FC = () => {
const presetHighestDay = `${currentYear}-02-15`;
reactGridRef.current?.filterService.updateFilters([
- { columnId: 'gender', searchTerms: ['female'], operator: OperatorType.equal },
- { columnId: 'name', searchTerms: ['Jane'], operator: OperatorType.startsWith },
+ { columnId: 'gender', searchTerms: ['female'], operator: '=' },
+ { columnId: 'name', searchTerms: ['Jane'], operator: 'StartsWith' },
{ columnId: 'company', searchTerms: ['acme'], operator: 'IN' },
- { columnId: 'billingAddressZip', searchTerms: ['11'], operator: OperatorType.greaterThanOrEqual },
- { columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: OperatorType.rangeInclusive },
+ { columnId: 'billingAddressZip', searchTerms: ['11'], operator: '>=' },
+ { columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: 'RangeInclusive' },
]);
}
@@ -360,14 +358,14 @@ const Example6: React.FC = () => {
const presetHighestDay = `${currentYear}-02-15`;
reactGridRef.current?.filterService.updateFilters([
- { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
- { columnId: 'name', searchTerms: ['Joh*oe'], operator: OperatorType.startsWithEndsWith },
+ { columnId: 'gender', searchTerms: ['male'], operator: '=' },
+ { columnId: 'name', searchTerms: ['Joh*oe'], operator: 'StartsWithEndsWith' },
{ columnId: 'company', searchTerms: ['xyz'], operator: 'IN' },
- { columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: OperatorType.rangeInclusive },
+ { columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: 'RangeInclusive' },
]);
reactGridRef.current?.sortService.updateSorting([
{ columnId: 'name', direction: 'asc' },
- { columnId: 'company', direction: SortDirection.DESC },
+ { columnId: 'company', direction: 'DESC' },
]);
setTimeout(() => {
reactGridRef.current?.paginationService?.changeItemPerPage(20);
diff --git a/demos/react/src/examples/slickgrid/Example9.tsx b/demos/react/src/examples/slickgrid/Example9.tsx
index 2f0141cb23..04fe1413fc 100644
--- a/demos/react/src/examples/slickgrid/Example9.tsx
+++ b/demos/react/src/examples/slickgrid/Example9.tsx
@@ -2,7 +2,6 @@ import i18next from 'i18next';
import React, { useEffect, useRef, useState } from 'react';
import { withTranslation } from 'react-i18next';
import {
- ExtensionName,
Filters,
Formatters,
SlickgridReact,
@@ -296,7 +295,7 @@ const Example9: React.FC = () => {
function toggleGridMenu(e: MouseEvent) {
if (reactGridRef.current?.extensionService) {
- const gridMenuInstance = reactGridRef.current.extensionService.getExtensionInstanceByName(ExtensionName.gridMenu);
+ const gridMenuInstance = reactGridRef.current.extensionService.getExtensionInstanceByName('gridMenu');
gridMenuInstance.showGridMenu(e, { dropSide: 'right' });
}
}
diff --git a/demos/react/src/examples/slickgrid/custom-inputFilter.tsx b/demos/react/src/examples/slickgrid/custom-inputFilter.tsx
index 58229792fc..db1f53d002 100644
--- a/demos/react/src/examples/slickgrid/custom-inputFilter.tsx
+++ b/demos/react/src/examples/slickgrid/custom-inputFilter.tsx
@@ -1,13 +1,12 @@
import {
emptyElement,
- OperatorType,
type Column,
type ColumnFilter,
type Filter,
type FilterArguments,
type FilterCallback,
type GridOption,
- type OperatorString,
+ type OperatorType,
type SearchTerm,
type SlickGrid,
} from 'slickgrid-react';
@@ -20,7 +19,7 @@ export class CustomInputFilter implements Filter {
searchTerms: SearchTerm[] = [];
columnDef!: Column;
callback!: FilterCallback;
- operator: OperatorType | OperatorString = OperatorType.equal;
+ operator: OperatorType = 'EQ';
/** Getter for the Filter Operator */
get columnFilter(): ColumnFilter {
diff --git a/demos/react/src/examples/slickgrid/example51.scss b/demos/react/src/examples/slickgrid/example51.scss
new file mode 100644
index 0000000000..539ad2ecd2
--- /dev/null
+++ b/demos/react/src/examples/slickgrid/example51.scss
@@ -0,0 +1,144 @@
+body {
+ --slick-menu-item-height: 30px;
+ --slick-menu-line-height: 30px;
+ --slick-column-picker-item-height: 28px;
+ --slick-column-picker-line-height: 28px;
+ --slick-menu-item-border-radius: 4px;
+ --slick-menu-item-hover-border: 1px solid #148dff;
+ --slick-column-picker-item-hover-color: #fff;
+ --slick-column-picker-item-border-radius: 4px;
+ --slick-column-picker-item-hover-border: 1px solid #148dff;
+ --slick-menu-item-hover-color: #fff;
+ --slick-tooltip-background-color: #4c4c4c;
+ --slick-tooltip-color: #fff;
+ --slick-tooltip-font-size: 14px;
+ .slick-cell-menu,
+ .slick-context-menu,
+ .slick-grid-menu,
+ .slick-header-menu {
+ .slick-menu-item:hover:not(.slick-menu-item-disabled) {
+ color: #0a34b5;
+ }
+ }
+ .slick-menu-footer {
+ padding: 4px 6px;
+ border-top: 1px solid #c0c0c0;
+ }
+}
+
+kbd {
+ background-color: #eee;
+ color: #202020;
+}
+.key-hint {
+ background: #eee;
+ border: 1px solid #ccc;
+ border-radius: 2px;
+ padding: 2px 4px;
+ font-size: 10px;
+ margin-left: 10px;
+ white-space: nowrap;
+ display: inline-flex;
+ align-items: center;
+ height: 20px;
+
+ &.beta,
+ &.danger,
+ &.warn {
+ color: white;
+ font-size: 8px;
+ font-weight: bold;
+ }
+ &.beta {
+ background: #4444ff;
+ border: 1px solid #5454ff;
+ }
+
+ &.danger {
+ background: #ff4444;
+ border: 1px solid #fb5a5a;
+ }
+
+ &.warn {
+ background: #ff9800;
+ border: 1px solid #fba321;
+ }
+}
+
+.edit-cell {
+ // background: #eee;
+ border: 1px solid #ccc;
+ border-radius: 2px;
+ padding: 2px 4px;
+ font-size: 10px;
+ margin-left: 10px;
+ display: inline-flex;
+ align-items: center;
+ height: 18px;
+}
+
+.export-timestamp {
+ background-color: #4c4c4c;
+ color: #fff;
+ padding: 8px;
+ border-radius: 4px;
+ position: absolute;
+ z-index: 999999;
+}
+
+.advanced-export-icon,
+.edit-cell-icon,
+.recalc-icon {
+ width: 20px;
+ height: 20px;
+ border-radius: 3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 4px;
+ transition: transform 0.2s;
+ color: white;
+ font-size: 10px;
+}
+.advanced-export-icon {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+.edit-cell-icon {
+ background: linear-gradient(135deg, #00c853 0%, #64dd17 100%);
+}
+.recalc-icon {
+ background: linear-gradient(135deg, #c800a3 0%, #a31189 100%);
+}
+
+.round-tag {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ display: inline-block;
+ background: #44ff44;
+ box-shadow: 0 0 4px #44ff44;
+ margin-left: 10px;
+}
+
+.menu-item {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ justify-content: space-between;
+
+ .menu-item-label.warn {
+ flex: 1;
+ color: #f09000;
+ }
+}
+.menu-item-icon {
+ margin-right: 4px;
+ font-size: 18px;
+ &.warn {
+ color: #ff9800;
+ }
+}
+
+.menu-item-label {
+ flex: 1;
+}
diff --git a/demos/react/src/examples/slickgrid/utilities.ts b/demos/react/src/examples/slickgrid/utilities.ts
index b4c1bff6ef..ae78294c55 100644
--- a/demos/react/src/examples/slickgrid/utilities.ts
+++ b/demos/react/src/examples/slickgrid/utilities.ts
@@ -30,13 +30,6 @@ export function showToast(msg: string, type: 'danger' | 'info' | 'warning', time
}, time);
return;
}
-
- // @deprecated, remove fallback in next major release
- // otherwise, fallback (when popover is not supported): keep the div visible as regular HTML and remove after timeout.
- divContainer.style.left = '50%';
- divContainer.style.top = '20px';
- divContainer.style.transform = 'translateX(-50%)';
- setTimeout(() => divContainer.remove(), time);
}
export function zeroPadding(input: string | number) {
diff --git a/demos/react/test/cypress/e2e/example10.cy.ts b/demos/react/test/cypress/e2e/example10.cy.ts
index 0dd651b40f..22544dc7ab 100644
--- a/demos/react/test/cypress/e2e/example10.cy.ts
+++ b/demos/react/test/cypress/e2e/example10.cy.ts
@@ -46,11 +46,8 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should have 2 rows (Task 12,Task 13) selected in 2nd grid after typing in a search filter', () => {
cy.get('#grid2').find('.filter-title').type('Task 1');
-
cy.get('#grid2').find('.slick-row').should('not.have.length', 0);
-
cy.get('[data-test=grid2-selections]').should('contain', '');
-
cy.get('#grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
});
@@ -125,7 +122,7 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
describe('Pagination', () => {
it('should Clear all Filters on 2nd Grid', () => {
- cy.get('#grid2').find('button.slick-grid-menu-button').click();
+ cy.get('#grid2').find('button.slick-grid-menu-button').trigger('click').click();
cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item').first().find('span').contains('Clear all Filters').click();
});
@@ -141,112 +138,72 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
.then((pageNumber) => expect(pageNumber).to.eq('2'));
cy.get('@grid1').find('[data-test=page-count]').contains('99');
-
cy.get('@grid1').find('[data-test=item-from]').contains('6');
-
cy.get('@grid1').find('[data-test=item-to]').contains('10');
-
cy.get('@grid1').find('[data-test=total-items]').contains('495');
// 2nd Grid
cy.get('@grid2').find('[data-test=page-count]').contains('105');
-
cy.get('@grid2').find('[data-test=item-from]').contains('1');
-
cy.get('@grid2').find('[data-test=item-to]').contains('5');
-
cy.get('@grid2').find('[data-test=total-items]').contains('525');
});
it('should change Page Number in Grid1 and expect the Pagination to have correct values', () => {
cy.get('#slickGridContainer-grid1').as('grid1');
-
cy.get('@grid1').find('[data-test=page-number-input]').clear().type('52').type('{enter}');
-
cy.get('@grid1').find('[data-test=page-count]').contains('99');
-
cy.get('@grid1').find('[data-test=item-from]').contains('256');
-
cy.get('@grid1').find('[data-test=item-to]').contains('260');
-
cy.get('@grid1').find('[data-test=total-items]').contains('495');
});
it('should change Page Number and Page Size in Grid2 and expect the Pagination to have correct values', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('[data-test=page-number-input]').clear().type('34').type('{enter}');
-
cy.get('@grid2').find('[data-test=page-count]').contains('105');
-
cy.get('@grid2').find('[data-test=item-from]').contains('166');
-
cy.get('@grid2').find('[data-test=item-to]').contains('170');
-
cy.get('@grid2').find('[data-test=total-items]').contains('525');
-
cy.get('@grid2').find('#items-per-page-label').select('75');
-
cy.get('@grid2').find('[data-test=page-count]').contains('7');
-
cy.get('@grid2').find('[data-test=item-from]').contains('1');
-
cy.get('@grid2').find('[data-test=item-to]').contains('75');
});
it('should go back to Grid1 and expect the same value before changing Pagination of Grid2', () => {
cy.get('#slickGridContainer-grid1').as('grid1');
-
cy.get('@grid1').find('[data-test=page-count]').contains('99');
-
cy.get('@grid1').find('[data-test=item-from]').contains('256');
-
cy.get('@grid1').find('[data-test=item-to]').contains('260');
-
cy.get('@grid1').find('[data-test=total-items]').contains('495');
});
it('should display page 0 of 0 with 0 items when applied filter returning an empty dataset', () => {
cy.get('#slickGridContainer-grid1').as('grid1');
-
cy.get('@grid1').find('.filter-title').type('000');
-
cy.get('.slick-empty-data-warning:visible').contains('No data to display.');
-
cy.get('@grid1').find('[data-test=page-count]').contains('0');
-
cy.get('@grid1').find('[data-test=item-from]').should('not.be.visible');
-
cy.get('@grid1').find('[data-test=item-to]').should('not.be.visible');
-
cy.get('@grid1').find('[data-test=total-items]').contains('0');
});
it('should erase part of the filter to have "00" and expect 4 items in total with 1 page', () => {
cy.get('#slickGridContainer-grid1').as('grid1');
-
cy.get('@grid1').find('.filter-title').type('{backspace}');
-
cy.get('.slick-empty-data-warning').contains('No data to display.').should('not.be.visible');
-
cy.get('@grid1').find('[data-test=page-count]').contains('1');
-
cy.get('@grid1').find('[data-test=item-from]').contains('1');
-
cy.get('@grid1').find('[data-test=item-to]').contains('4');
-
cy.get('@grid1').find('[data-test=total-items]').contains('4');
});
it('should also expect Grid2 to be unchanged (after changing Pagination in Grid1 in previous tests)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('[data-test=page-count]').contains('7');
-
cy.get('@grid2').find('[data-test=item-from]').contains('1');
-
cy.get('@grid2').find('[data-test=item-to]').contains('75');
-
cy.get('@grid2').find('[data-test=total-items]').contains('525');
});
@@ -265,35 +222,24 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should go to Page 3 of 2nd Grid and have 2 rows selected in that Page and also have 4 rows selected in the entire grid (Task 3,Task 12,Task 13,Task 522)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('#items-per-page-label').select('5');
-
cy.get('@grid2').find('[data-test=page-number-input]').clear().type('3').type('{enter}');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
});
it('should go to last Page of 2nd Grid and have 1 rows selected in that Page and also have 4 rows selected in the entire grid (Task 3,Task 12,Task 13,Task 522)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('.icon-seek-end').click();
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 1);
});
it(`should go to first Page of 2nd Grid and select another row (Task 1) in that Page, wich will now be (Task1,Task3) and now have 5 rows selected in the entire grid (Task 1,Task 3,Task 12,Task 13,Task 522)`, () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('.icon-seek-first').click().wait(10);
-
cy.get('@grid2').find('.slick-row:nth(1) .slick-cell:nth(0) input[type=checkbox]').click({ force: true });
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
cy.window().then((win) => {
@@ -313,23 +259,16 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should go back to Page 3 of 2nd Grid and have 2 rows selected in that Page and also retain 5 selected rows in the entire grid (Task 1,Task 3,Task 12,Task 13,Task 522)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('#items-per-page-label').select('5');
-
cy.get('@grid2').find('[data-test=page-number-input]').clear().type('3').type('{enter}');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
});
it('should go to last Page of 2nd Grid and still have 1 row selected in that Page and also retain 5 selected rows in the entire grid (Task 1,Task 3,Task 12,Task 13,Task 522)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('.icon-seek-end').click();
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 1);
});
});
@@ -366,27 +305,18 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should go to Page 61 of Grid1 and expect to find "Task 300" still be selected', () => {
cy.get('#slickGridContainer-grid1').as('grid1');
-
cy.get('@grid1').find('[data-test=page-number-input]').clear().type('61').type('{enter}');
-
cy.get('[data-test=grid1-selections]').contains('Task 300');
-
cy.get('.slick-cell.l0.r0.slick-cell-checkboxsel.selected').should('exist');
-
cy.get('[data-test=grid1-selections]').contains('Task 300');
});
it('should go to a different page for next test to confirm that it will then go to page 1', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('[data-test=page-number-input]').clear().type('22').type('{enter}');
-
cy.get('@grid2').find('[data-test=page-count]').contains('105');
-
cy.get('@grid2').find('[data-test=item-from]').contains('106');
-
cy.get('@grid2').find('[data-test=item-to]').contains('110');
-
cy.get('@grid2').find('[data-test=total-items]').contains('525');
});
@@ -453,35 +383,26 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should have 5 rows (Task 1,Task 3,Task 12,Task 13,Task 522) selected in the entire 2nd grid BUT only 2 shown in the DOM in the top portion of the grid (because SlickGrid uses virtual rendering)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
});
it('should scroll to the bottom of 2nd Grid and still have 5 rows (Task 1,Task 3,Task 12,Task 13,Task 522) selected and find 2 row selected because we now have 2 rows that got rendered (first and last)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('.slick-viewport-top.slick-viewport-left').scrollTo('bottom').wait(10);
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
});
it('should have 2 rows (Task 3,Task 13) selected in 2nd grid after typing in a search filter (3)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('.filter-title').type('3');
-
cy.get('@grid2').find('.slick-viewport-top.slick-viewport-left').scrollTo('top').wait(10);
-
cy.get('@grid2').find('.slick-row').should('not.have.length', 0);
cy.wait(50);
cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 13');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
cy.window().then((win) => {
@@ -514,9 +435,7 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
describe('Re-enable Pagination', () => {
it('should re-enable the Pagination and expect to see it show it again below the grid at Page 1', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('[data-test=toggle-pagination-grid2]').click();
-
cy.get('#slickGridContainer-grid2 .slick-pagination').should('exist');
cy.get('@grid2')
@@ -525,13 +444,9 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
.then((pageNumber) => expect(pageNumber).to.eq('1'));
cy.get('@grid2').find('[data-test=page-number-input]').click();
-
cy.get('@grid2').find('[data-test=page-count]').contains('105');
-
cy.get('@grid2').find('[data-test=item-from]').contains('1');
-
cy.get('@grid2').find('[data-test=item-to]').contains('5');
-
cy.get('@grid2').find('[data-test=total-items]').contains('525');
cy.window().then((win) => {
@@ -549,13 +464,9 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should have 2 rows (Task 3,Task 13) selected in 2nd grid after typing in a search filter (3)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('.filter-title').type('3');
-
cy.get('@grid2').find('.slick-row').should('not.have.length', 0);
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 13');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
cy.window().then((win) => {
@@ -583,11 +494,8 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
.then((pageNumber) => expect(pageNumber).to.eq('1'));
cy.get('@grid2').find('[data-test=page-count]').contains('3');
-
cy.get('@grid2').find('[data-test=item-from]').contains('1');
-
cy.get('@grid2').find('[data-test=item-to]').contains('5');
-
cy.get('@grid2').find('[data-test=total-items]').contains('179');
});
});
diff --git a/demos/react/test/cypress/e2e/example14.cy.ts b/demos/react/test/cypress/e2e/example14.cy.ts
index 402545254b..91f7db34b6 100644
--- a/demos/react/test/cypress/e2e/example14.cy.ts
+++ b/demos/react/test/cypress/e2e/example14.cy.ts
@@ -21,16 +21,16 @@ describe('Example 14 - Column Span & Header Grouping', () => {
});
it('should have a frozen grid on page load with 3 columns on the left and 4 columns on the right', () => {
- cy.get('#grid2').find(`[data-row=0]`).should('have.length', 2);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]`).children().should('have.length', 3);
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0]`).children().should('have.length', 4);
+ cy.get('#grid2').find('[data-row=0]').should('have.length', 2);
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 3);
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0]').children().should('have.length', 4);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]> .slick-cell:nth(0)`).should('contain', '0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]> .slick-cell:nth(1)`).should('contain', 'Task 0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]> .slick-cell:nth(2)`).should('contain', '5 days');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(0)').should('contain', '0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(1)').should('contain', 'Task 0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(2)').should('contain', '5 days');
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0]> .slick-cell:nth(0)`).should('contain', '01/01/2009');
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0]> .slick-cell:nth(1)`).should('contain', '01/05/2009');
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0]> .slick-cell:nth(0)').should('contain', '01/01/2009');
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0]> .slick-cell:nth(1)').should('contain', '01/05/2009');
});
it('should have exact Column Pre-Header & Column Header Titles in the grid again', () => {
@@ -48,14 +48,14 @@ describe('Example 14 - Column Span & Header Grouping', () => {
it('should click on the "Remove Frozen Columns" button to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => {
cy.get('[data-test="remove-frozen-column-button"]').click();
- cy.get('#grid2').find(`[data-row=0]`).should('have.length', 1);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]`).children().should('have.length', 7);
+ cy.get('#grid2').find('[data-row=0]').should('have.length', 1);
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 7);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)`).should('contain', '0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)`).should('contain', 'Task 0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)`).should('contain', '5 days');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)`).should('contain', '01/01/2009');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)`).should('contain', '01/05/2009');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)').should('contain', '01/01/2009');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)').should('contain', '01/05/2009');
});
it('should have exact Column Pre-Header & Column Header Titles in the grid once again', () => {
@@ -73,16 +73,16 @@ describe('Example 14 - Column Span & Header Grouping', () => {
it('should click on the "Set 3 Frozen Columns" button to switch frozen columns grid and expect 3 frozen columns on the left and 4 columns on the right', () => {
cy.contains('Set 3 Frozen Columns').click({ force: true });
- cy.get('#grid2').find(`[data-row=0]`).should('have.length', 2);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]`).children().should('have.length', 3);
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0]`).children().should('have.length', 4);
+ cy.get('#grid2').find('[data-row=0]').should('have.length', 2);
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 3);
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0]').children().should('have.length', 4);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)`).should('contain', '0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)`).should('contain', 'Task 0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)`).should('contain', '5 days');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days');
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0] > .slick-cell:nth(0)`).should('contain', '01/01/2009');
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0] > .slick-cell:nth(1)`).should('contain', '01/05/2009');
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0] > .slick-cell:nth(0)').should('contain', '01/01/2009');
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0] > .slick-cell:nth(1)').should('contain', '01/05/2009');
});
it('should have still exact Column Pre-Header & Column Header Titles in the grid', () => {
@@ -102,14 +102,14 @@ describe('Example 14 - Column Span & Header Grouping', () => {
cy.contains('Unfreeze Columns/Rows').click({ force: true });
- cy.get('#grid2').find(`[data-row=0]`).should('have.length', 1);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]`).children().should('have.length', 7);
+ cy.get('#grid2').find('[data-row=0]').should('have.length', 1);
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 7);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)`).should('contain', '0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)`).should('contain', 'Task 0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)`).should('contain', '5 days');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)`).should('contain', '01/01/2009');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)`).should('contain', '01/05/2009');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)').should('contain', '01/01/2009');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)').should('contain', '01/05/2009');
});
it('should reapply 3 frozen columns on 2nd grid', () => {
@@ -209,7 +209,7 @@ describe('Example 14 - Column Span & Header Grouping', () => {
});
describe('Colspan checks on 1st grid', () => {
- it('should hide Finish column and still expect "5 days" to spread accross 3 column', () => {
+ it('should hide Finish column and expect "5 days" spread to drop from 3 to 2 columns (1x hidden column)', () => {
cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0').should('contain', 'Task 1');
cy.get('#grid1').find('[data-row=2] .slick-cell.l0.r5').should('contain', 'Task 2');
cy.get('#grid1').find('[data-row=1] .slick-cell.l1.r3').should('contain', '5 days');
@@ -232,11 +232,64 @@ describe('Example 14 - Column Span & Header Grouping', () => {
.should('contain', 'Hide Column')
.click();
+ // goto right
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0').click();
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0.active').should('contain', 'Task 1').type('{rightArrow}');
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l1.r3.active').should('contain', '5 days').type('{rightArrow}');
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l4.r4.active').contains(/\d+$/).type('{rightArrow}');
+ cy.get('#grid1')
+ .find('[data-row=1] .slick-cell.l5.r5')
+ .contains(/(true|false)+$/);
+
+ // goto left
+ cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5.active')
+ .contains(/(true|false)+$/)
+ .type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4.active').contains(/\d+$/).type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3.active').should('contain', '5 days').type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0.active').should('contain', 'Task 1');
+ });
+
+ it('should reset Column Picker, click on Spread Hidden Coolumn button then hide Finish column and still expect "5 days" to spread accross 3 column', () => {
+ cy.get('#grid1').find('.slick-header-column:nth(1)').trigger('mouseover').trigger('contextmenu').invoke('show');
+
+ cy.get('.slick-column-picker')
+ .find('.slick-column-picker-list')
+ .children('li:nth-child(4)')
+ .children('label')
+ .should('contain', 'Period - Finish')
+ .click();
+
+ cy.get('.slick-column-picker .close').click();
+
+ cy.get('[data-test="spread-colspan-button"]').click();
cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0').should('contain', 'Task 1');
- cy.get('#grid1').find('[data-row=2] .slick-cell.l0.r4').should('contain', 'Task 2');
+ cy.get('#grid1').find('[data-row=2] .slick-cell.l0.r5').should('contain', 'Task 2');
cy.get('#grid1').find('[data-row=1] .slick-cell.l1.r3').should('contain', '5 days');
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l4.r4').contains(/\d+$/);
+ cy.get('#grid1')
+ .find('[data-row=1] .slick-cell.l5.r5')
+ .contains(/(true|false)+$/);
+
+ cy.get('#grid1')
+ .find('.slick-pane-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(3)')
+ .trigger('mouseover')
+ .children('.slick-header-menu-button')
+ .invoke('show')
+ .click();
+
+ cy.get('.slick-header-menu .slick-menu-command-list')
+ .should('be.visible')
+ .children('.slick-menu-item:nth-of-type(3)')
+ .children('.slick-menu-content')
+ .should('contain', 'Hide Column')
+ .click();
+
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0').should('contain', 'Task 1');
+ cy.get('#grid1').find('[data-row=2] .slick-cell.l0.r5').should('contain', 'Task 2');
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l1.r4').should('contain', '5 days');
cy.get('#grid1')
- .find('[data-row=1] .slick-cell.l4.r4')
+ .find('[data-row=1] .slick-cell.l5.r5')
.contains(/(true|false)+$/);
});
@@ -292,15 +345,13 @@ describe('Example 14 - Column Span & Header Grouping', () => {
describe('First Grid - Key Navigation', () => {
it('should start at Task 1 and expect "Duration" to have colspan of 3 and show "% Complete" and "Effort Driven"', () => {
cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0').should('contain', 'Task 1').click().type('{rightArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').should('have.class', 'active').should('contain', '5 days').type('{rightArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4').should('have.class', 'active').contains(/\d+$/).type('{rightArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5')
- .should('have.class', 'active')
- .contains(/(true|false)+$/);
+ cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3.active').should('contain', '5 days').type('{rightArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4.active').contains(/\d+$/).type('{rightArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5.active').contains(/(true|false)+$/);
cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5.active').type('{leftArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4').should('have.class', 'active').contains(/\d+$/).type('{leftArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').should('have.class', 'active').should('contain', '5 days');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4.active').contains(/\d+$/).type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3.active').should('contain', '5 days');
cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0').should('contain', 'Task 1');
});
@@ -312,16 +363,17 @@ describe('Example 14 - Column Span & Header Grouping', () => {
.children('label')
.should('contain', 'Period - Finish')
.click();
+ cy.get('.slick-column-picker .close').click();
cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0').should('contain', 'Task 1').click().type('{rightArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').should('have.class', 'active').should('contain', '5 days').type('{rightArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4')
+ cy.get('#grid1 [data-row=1] > .slick-cell.l1.r4.active').should('contain', '5 days').type('{rightArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5')
.should('have.class', 'active')
.contains(/(true|false)+$/);
- cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4.active').type('{leftArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').should('have.class', 'active').should('contain', '5 days').type('{leftArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0').should('have.class', 'active');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5.active').type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l1.r4.active').should('contain', '5 days').type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0.active');
cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0').should('contain', 'Task 1');
});
});
diff --git a/demos/react/test/cypress/e2e/example16.cy.ts b/demos/react/test/cypress/e2e/example16.cy.ts
index a1a2073bb5..78dabd7ae7 100644
--- a/demos/react/test/cypress/e2e/example16.cy.ts
+++ b/demos/react/test/cypress/e2e/example16.cy.ts
@@ -408,7 +408,7 @@ describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => {
});
it('should add Edit/Delete columns and expect 2 new columns added at the beginning of the grid', () => {
- const newExpectedColumns = ['', '', ...fullTitles];
+ const newExpectedColumns = ['', '', '', '', 'Title', '% Complete', 'Start', 'Finish', 'Duration', 'Completed'];
cy.get('[data-test="add-crud-columns-btn"]').click();
cy.get('#grid16')
diff --git a/demos/react/test/cypress/e2e/example18.cy.ts b/demos/react/test/cypress/e2e/example18.cy.ts
index 55b380a1a1..b9807b0bc8 100644
--- a/demos/react/test/cypress/e2e/example18.cy.ts
+++ b/demos/react/test/cypress/e2e/example18.cy.ts
@@ -122,11 +122,10 @@ describe('Example 18 - Draggable Grouping & Aggregators', () => {
it('should expand all rows with "Expand All" from context menu and expect all the Groups to be expanded and the Toogle All icon to be collapsed', () => {
cy.get('#grid18').find('.slick-row .slick-cell:nth(1)').rightclick({ force: true });
- cy.get('.slick-context-menu .slick-menu-command-list')
- .find('.slick-menu-item')
- .find('.slick-menu-content')
- .contains('Expand all Groups')
- .click();
+ cy.get('.slick-context-menu .slick-menu-command-list').find('.slick-menu-content').contains('Export in CSV format');
+ cy.get('.slick-context-menu .slick-menu-command-list').find('.slick-menu-content').contains('Export to Excel');
+ cy.get('.slick-context-menu .slick-menu-command-list').find('.slick-menu-content').contains('Export to PDF');
+ cy.get('.slick-context-menu .slick-menu-command-list').find('.slick-menu-content').contains('Expand all Groups').click();
cy.get('#grid18').find('.slick-group-toggle.collapsed').should('have.length', 0);
diff --git a/demos/react/test/cypress/e2e/example32.cy.ts b/demos/react/test/cypress/e2e/example32.cy.ts
index c2c8d5f769..1c8eb7b69e 100644
--- a/demos/react/test/cypress/e2e/example32.cy.ts
+++ b/demos/react/test/cypress/e2e/example32.cy.ts
@@ -181,7 +181,12 @@ describe('Example 32 - Columns Resize by Content', () => {
const yesterdayDate = format(addDay(new Date(), -1), 'YYYY-MM-DD');
const todayDate = format(new Date(), 'YYYY-MM-DD');
- cy.get(`[data-vc-date=${yesterdayDate}]`).should('have.attr', 'data-vc-date-disabled');
+ // Check if yesterday's date element exists (may not be visible when 1st day of the month is a Sunday, e.g. 2026-02-01)
+ cy.get(`[data-vc-date=${yesterdayDate}]`).then(($el) => {
+ if ($el.length > 0) {
+ expect($el).to.have.attr('data-vc-date-disabled');
+ }
+ });
cy.get(`[data-vc-date=${todayDate}]`).should('not.have.attr', 'data-vc-date-disabled');
// make grid readonly again
@@ -379,16 +384,21 @@ describe('Example 32 - Columns Resize by Content', () => {
});
it('should be able to edit "Duration" when "autoEditByKeypress" is enabled and by clicking once on second row and expect next row to become editable', () => {
- cy.get('[data-row="2"] .slick-cell.l2.r2').contains(/[0-9]* days/);
- cy.get('[data-row="2"] .slick-cell.l2.r2').click();
- cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 0);
-
- cy.get('[data-row="2"] .slick-cell.l2.r2').type('123');
- cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 1);
- cy.get('[data-row="2"] .slick-cell.l2.r2').type('{enter}');
- cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 0);
-
- cy.get('[data-row="2"] .slick-cell.l2.r2').should('contain', '123 days');
+ // Check if yesterday's date element exists (may not be visible when we run the test on the 1st day of the month and it is a Sunday, e.g. 2026-02-01)
+ cy.get('[data-row="2"] .slick-cell.l2.r2').then(($el) => {
+ if ($el.length > 0) {
+ cy.wrap($el).contains(/[0-9]* days/);
+ cy.wrap($el).click();
+ cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 0);
+
+ cy.get('[data-row="2"] .slick-cell.l2.r2').type('123');
+ cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 1);
+ cy.get('[data-row="2"] .slick-cell.l2.r2').type('{enter}');
+ cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 0);
+
+ cy.get('[data-row="2"] .slick-cell.l2.r2').should('contain', '123 days');
+ }
+ });
});
it('should click on "Auto-Edit by keyboard OFF" button', () => {
diff --git a/demos/react/test/cypress/e2e/example33.cy.ts b/demos/react/test/cypress/e2e/example33.cy.ts
index 3db64520b9..998a33b382 100644
--- a/demos/react/test/cypress/e2e/example33.cy.ts
+++ b/demos/react/test/cypress/e2e/example33.cy.ts
@@ -5,6 +5,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
'Duration',
'Description',
'Description 2',
+ 'Button Tooltip',
'Cost',
'% Complete',
'Start',
@@ -13,7 +14,6 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
'Prerequisites',
'Action',
];
- const GRID_ROW_HEIGHT = 33;
it('should display Example title', () => {
cy.visit(`${Cypress.config('baseUrl')}/example33`);
@@ -32,7 +32,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over 1st row checkbox column and NOT expect any tooltip to show since it is disabled on that column', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(0)`).as('checkbox0-cell');
+ cy.get('[data-row="0"] > .slick-cell:nth(0)').as('checkbox0-cell');
cy.get('@checkbox0-cell').trigger('mouseover');
cy.get('.slick-custom-tooltip').should('not.exist');
@@ -40,7 +40,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over Task 2 cell and expect async tooltip to show', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(1)`).as('task1-cell');
+ cy.get('[data-row="0"] > .slick-cell:nth(1)').as('task1-cell');
cy.get('@task1-cell').should('contain', 'Task 2');
cy.get('@task1-cell').trigger('mouseover');
cy.get('.slick-custom-tooltip').contains('loading...');
@@ -65,7 +65,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over Task 6 cell and expect async tooltip to show', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(1)`).as('task6-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(1)').as('task6-cell');
cy.get('@task6-cell').should('contain', 'Task 6');
cy.get('@task6-cell').trigger('mouseover');
cy.get('.slick-custom-tooltip').contains('loading...');
@@ -87,7 +87,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
it('should mouse over Task 6 cell on "Start" column and expect a delayed tooltip opening via async process', () => {
cy.get('.slick-custom-tooltip').should('not.exist');
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(7)`).as('start6-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(8)').as('start6-cell');
cy.get('@start6-cell').contains(/\d{4}-\d{2}-\d{2}$/); // use regexp to make sure it's a number
cy.get('@start6-cell').trigger('mouseover');
@@ -111,7 +111,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over 6th row Description and expect full cell content to show in a tooltip because cell has ellipsis and is too long for the cell itself', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(3)`).as('desc6-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(3)').as('desc6-cell');
cy.get('@desc6-cell').should('contain', 'This is a sample task description.');
cy.get('@desc6-cell').trigger('mouseover');
@@ -126,7 +126,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over 6th row Description 2 and expect regular tooltip title + concatenated full cell content when using "useRegularTooltipFromFormatterOnly: true"', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(4)`).as('desc2-5-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(4)').as('desc2-5-cell');
cy.get('@desc2-5-cell').should('contain', 'This is a sample task description.');
cy.get('@desc2-5-cell').trigger('mouseover');
@@ -139,8 +139,24 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
cy.get('@desc2-5-cell').trigger('mouseout');
});
+ it('should mouse over Button Tooltip column and verify button and icon tooltips show correctly', () => {
+ cy.get('[data-row="2"] > .slick-cell:nth(5)').as('button-cell');
+
+ // Hover over the button element and expect its tooltip
+ cy.get('@button-cell').find('button').trigger('mouseover');
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip').should('contain', 'This is the button tooltip');
+ cy.get('@button-cell').find('button').trigger('mouseout');
+
+ // Hover over the icon inside the button and expect its tooltip
+ cy.get('@button-cell').find('i.mdi').trigger('mouseover');
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip').should('contain', 'icon tooltip');
+ cy.get('@button-cell').find('i.mdi').trigger('mouseout');
+ });
+
it('should mouse over 2nd row Duration and expect a custom tooltip shown with 4 label/value pairs displayed', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(2)`).as('duration2-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(2)').as('duration2-cell');
cy.get('@duration2-cell').contains(/\d+\sday[s]?$/);
cy.get('@duration2-cell').trigger('mouseover');
@@ -163,7 +179,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over % Complete cell of Task 6 and expect regular tooltip to show with content "x %" where x is a number', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(6)`).as('percentage-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(7)').as('percentage-cell');
cy.get('@percentage-cell').find('.percent-complete-bar').should('exist');
cy.get('@percentage-cell').trigger('mouseover');
@@ -174,7 +190,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over Prerequisite cell of Task 6 and expect regular tooltip to show with content "Task 6, Task 5"', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(10)`).as('prereq-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(11)').as('prereq-cell');
cy.get('@prereq-cell').should('contain', 'Task 6, Task 5');
cy.get('@prereq-cell').trigger('mouseover');
@@ -206,7 +222,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over header-row (filter) Finish column and NOT expect any tooltip to show since it is disabled on that column', () => {
- cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(8)`).as('finish-filter');
+ cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(9)`).as('finish-filter');
cy.get('@finish-filter').trigger('mouseover');
cy.get('.slick-custom-tooltip').should('not.exist');
@@ -214,7 +230,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should open PreRequisite dropdown and expect it be lazily loaded', () => {
- cy.get('.slick-headerrow-columns .slick-headerrow-column:nth(10)').as('checkbox10-header');
+ cy.get('.slick-headerrow-columns .slick-headerrow-column:nth(11)').as('checkbox10-header');
cy.get('@checkbox10-header').click();
cy.get('[data-test="alert-lazy"]').should('be.visible');
cy.get('[data-name="filter-prerequisites"] .ms-loading span').contains('Loading...');
@@ -225,7 +241,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over header-row (filter) Prerequisite column and expect to see tooltip of selected filter options', () => {
- cy.get('.slick-headerrow-columns .slick-headerrow-column:nth(10)').as('checkbox10-header');
+ cy.get('.slick-headerrow-columns .slick-headerrow-column:nth(11)').as('checkbox10-header');
cy.get('@checkbox10-header').trigger('mouseover');
cy.get('.filter-prerequisites .ms-choice span').contains('15 of 1000 selected');
@@ -259,25 +275,57 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over header title on 2nd column with Finish name and NOT expect any tooltip to show since it is disabled on that column', () => {
- cy.get('.slick-header-columns .slick-header-column:nth(8)').as('finish-header');
+ cy.get('.slick-header-columns .slick-header-column:nth(9)').as('finish-header');
cy.get('@finish-header').trigger('mouseover');
cy.get('.slick-custom-tooltip').should('not.exist');
cy.get('@finish-header').trigger('mouseout');
});
+ it('should mouse over "Filters Empty Description" button and expect global tooltip to show with title text', () => {
+ // Test button tooltip
+ cy.get('[data-test="filter-empty-desc"]').trigger('mouseover');
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip .tooltip-body').should('contain', 'Apply filter to show only empty descriptions');
+ cy.get('[data-test="filter-empty-desc"]').trigger('mouseout');
+
+ // Test icon tooltip
+ cy.get('[data-test="filter-empty-desc"] i.mdi').trigger('mouseover');
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip .tooltip-body').should('contain', 'icon tooltip for empty descriptions');
+ cy.get('[data-test="filter-empty-desc"] i.mdi').trigger('mouseout');
+
+ // Verify tooltip is hidden when hovering on another element
+ cy.get('[data-test="server-delay"]').trigger('mouseover');
+ cy.get('.slick-custom-tooltip').should('not.exist');
+ });
+
+ it('should mouse over "Filters Non-Empty Description" button and expect global tooltip to show with title text', () => {
+ // Test button tooltip
+ cy.get('[data-test="filter-non-empty-desc"]').trigger('mouseover');
+ cy.wait(50);
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip .tooltip-body').should('contain', 'Apply filter to show only non-empty descriptions');
+ cy.get('[data-test="filter-non-empty-desc"]').trigger('mouseout');
+
+ // Test icon tooltip
+ cy.get('[data-test="filter-non-empty-desc"] i.mdi').trigger('mouseover');
+ cy.wait(10);
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip .tooltip-body').should('contain', 'icon tooltip for non-empty descriptions');
+ cy.get('[data-test="filter-non-empty-desc"] i.mdi').trigger('mouseout');
+ cy.wait(10);
+ cy.get('.slick-custom-tooltip').should('not.exist');
+ });
+
it('should click Prerequisite editor of 1st row (Task 2) and expect Task1 & 2 to be selected in the multiple-select drop', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(10)`).as('prereq-cell');
+ cy.get('[data-row="0"] > .slick-cell:nth(11)').as('prereq-cell');
cy.get('@prereq-cell').should('contain', 'Task 2, Task 1').click();
cy.get('div.ms-drop[data-name=editor-prerequisites]').find('li.selected').should('have.length', 2);
-
cy.get('div.ms-drop[data-name=editor-prerequisites]').find('li.selected:nth(0) span').should('contain', 'Task 1');
-
cy.get('div.ms-drop[data-name=editor-prerequisites]').find('li.selected:nth(1) span').should('contain', 'Task 2');
-
cy.get('div.ms-drop[data-name=editor-prerequisites]').find('.ms-ok-button').click();
-
cy.get('div.ms-drop[data-name=editor-prerequisites]').should('not.exist');
});
});
diff --git a/demos/react/test/cypress/e2e/example44.cy.ts b/demos/react/test/cypress/e2e/example44.cy.ts
index ab4ee2fcf1..f399ed0364 100644
--- a/demos/react/test/cypress/e2e/example44.cy.ts
+++ b/demos/react/test/cypress/e2e/example44.cy.ts
@@ -1,4 +1,4 @@
-describe('Example 44 - Column & Row Span', { retries: 1 }, () => {
+describe('Example 44 - Column & Row Span', { retries: 0 }, () => {
const GRID_ROW_HEIGHT = 30;
const fullTitles = [
'Title',
@@ -463,4 +463,102 @@ describe('Example 44 - Column & Row Span', { retries: 1 }, () => {
cy.get('[data-row=499] > .slick-cell.l5.r5.active').should('have.length', 1);
});
});
+
+ describe('Hide Columns with colspan/rowspan', () => {
+ it('should hide Title column and expect other colspan/rowspan to simply move over and stay attached to same columns', () => {
+ cy.get('[data-row=499] > .slick-cell.l5.r5.active').type('{ctrl}{home}', { release: false });
+ cy.get('[data-row=2] > .slick-cell.l0.r0.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').trigger('contextmenu').invoke('show');
+
+ cy.get('.slick-column-picker')
+ .find('.slick-column-picker-list')
+ .children('li:nth-child(1)')
+ .children('label')
+ .should('contain', 'Title')
+ .click();
+ cy.get('.slick-column-picker .close').click();
+
+ // Task 2 rowspan should be hidden now
+ cy.get('[data-row=2] > .slick-cell.l0.r0.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.not.eq(GRID_ROW_HEIGHT * 3)
+ );
+ });
+
+ it('should start at "Revenue Growth" second cell down, then type "Arrow Right" key 2x times and expect 4th row "Policy Index" green section to still have a rowspan 3x and colspan of 4x', () => {
+ cy.get('[data-row=3] > .slick-cell.l1.r1.rowspan').as('active_cell').click();
+ cy.get('[data-row=3] > .slick-cell.l1.r1.active').should('have.length', 1);
+ cy.get('@active_cell').type('{rightarrow}{rightarrow}');
+
+ cy.get('[data-row=2] > .slick-cell.l3.r5').should('not.have.class', 'rowspan');
+ cy.get('[data-row=3] > .slick-cell.l3.r7.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+ });
+
+ it('should go up by 1x "Arrow Up" and expect blue section colspan of 3x', () => {
+ cy.get('[data-row=3] > .slick-cell.l3.r7.rowspan').type('{uparrow}');
+ cy.get('[data-row=2] > .slick-cell.l3.r5.active').should('not.have.class', 'rowspan');
+ });
+
+ it('should "Revenue Growth" rowspan should now be at first column and "Policy Index" should now be at third column', () => {
+ cy.get(`[data-row=0] > .slick-cell.l0.r0`).should('not.exist');
+ cy.get(`[data-row=1] > .slick-cell.l0.r0`).should('not.exist');
+ cy.get(`[data-row=2] > .slick-cell.l0.r0`).should('not.exist');
+
+ cy.get('[data-row=0] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+ cy.get('[data-row=3] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5)
+ );
+ cy.get('[data-row=8] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 80)
+ );
+
+ cy.get('[data-row=2] > .slick-cell.l3.r5').should('not.have.class', 'rowspan');
+ cy.get('[data-row=3] > .slick-cell.l3.r7.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+ cy.get('[data-row=8] > .slick-cell.l3.r4.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 492)
+ );
+ });
+
+ it('should show again "Title" column and expect "Revenue Growth" and "Policy Index" columns to be moved to the right by 1 column', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').trigger('contextmenu').invoke('show');
+
+ cy.get('.slick-column-picker')
+ .find('.slick-column-picker-list')
+ .children('li:nth-child(1)')
+ .children('label')
+ .should('contain', 'Title')
+ .click();
+ cy.get('.slick-column-picker .close').click();
+
+ cy.get(`[data-row=0] > .slick-cell.l0.r0`).should('contain', 'Task 0');
+ cy.get(`[data-row=1] > .slick-cell.l0.r0`).should('contain', 'Task 1');
+ cy.get(`[data-row=2] > .slick-cell.l0.r0`).should('contain', 'Task 2');
+
+ cy.get('[data-row=0] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+ cy.get('[data-row=3] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5)
+ );
+ cy.get('[data-row=8] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 80)
+ );
+
+ cy.get('[data-row=2] > .slick-cell.l3.r5').should('not.have.class', 'rowspan');
+ cy.get('[data-row=3] > .slick-cell.l3.r7.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+ cy.get('[data-row=8] > .slick-cell.l3.r4.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 492)
+ );
+ });
+ });
});
diff --git a/demos/react/test/cypress/e2e/example48.cy.ts b/demos/react/test/cypress/e2e/example48.cy.ts
index a9df9df6b5..40d990bb34 100644
--- a/demos/react/test/cypress/e2e/example48.cy.ts
+++ b/demos/react/test/cypress/e2e/example48.cy.ts
@@ -160,19 +160,17 @@ describe('Example 48 - Hybrid Selection Model', () => {
});
it('should click on row 4 and 5 row checkbox and expect 5 full rows to be selected', () => {
- cy.get('#grid48-2 .slick-row[data-row="4"] .slick-cell.l0.r0').as('task4');
cy.get('#grid48-2 .slick-row[data-row="4"] .slick-cell.l1.r1').should('contain', '4');
- cy.get('@task4').click();
+ cy.get('#grid48-2 .slick-row[data-row="4"] input[type=checkbox]').click({ force: true });
cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top');
cy.get('#grid48-2 .slick-row[data-row="4"] .slick-cell.l0.r0').should('have.class', 'selected');
cy.get('#grid48-2 .slick-cell.selected').should('have.length', 8 * 1);
// select another row
- cy.get('#grid48-2 .slick-row[data-row="5"] .slick-cell.l0.r0').as('task5');
cy.get('#grid48-2 .slick-row[data-row="5"] .slick-cell.l1.r1').should('contain', '5');
- cy.get('@task5').click();
+ cy.get('#grid48-2 .slick-row[data-row="5"] input[type=checkbox]').click({ force: true });
cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top');
- cy.get('@task5').should('have.class', 'selected');
+ cy.get('#grid48-2 .slick-row[data-row="5"] .slick-cell.l0.r0').should('have.class', 'selected');
cy.get('#grid48-2 .slick-cell.selected').should('have.length', 8 * 2);
});
});
diff --git a/demos/react/test/cypress/e2e/example51.cy.ts b/demos/react/test/cypress/e2e/example51.cy.ts
new file mode 100644
index 0000000000..80e5148c97
--- /dev/null
+++ b/demos/react/test/cypress/e2e/example51.cy.ts
@@ -0,0 +1,382 @@
+import { format } from '@formkit/tempo';
+
+describe('Example 51 - Menus with Slots', () => {
+ const fullTitles = ['Title', 'Duration', 'Start', 'Finish', 'Cost', '% Complete', 'Action'];
+
+ it('should display Example title', () => {
+ cy.visit(`${Cypress.config('baseUrl')}/example51`);
+ cy.get('h2').should('contain', 'Example 51: Menus with Slots');
+ });
+
+ it('should have exact column titles in the grid', () => {
+ cy.get('#grid51')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(fullTitles[index]));
+ });
+
+ it('should open Context Menu hover "Duration" column and expect built-in and custom items listed in specific order', () => {
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').rightclick({ force: true });
+
+ // 1st item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.edit-cell-icon').contains('โ');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Edit Cell');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('kbd.edit-cell').contains('F2');
+
+ // icon should rotate while hovering
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseover');
+ cy.wait(175); // wait for rotation
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item .edit-cell-icon')
+ .invoke('css', 'transform') // Get the transform property
+ .then((cssTransform) => {
+ const transformValue = cssTransform as unknown as string; // Cast to string
+ cy.getTransformValue(transformValue, true, 'rotate').then((rotationAngle) => {
+ expect(rotationAngle).to.approximately(13, 15); // 15 degrees rotation
+ });
+ });
+
+ // 2nd item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item').find('i.mdi-content-copy').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item').find('span.menu-item-label').contains('Copy');
+
+ // 3rd item - divider
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(2)').should('have.class', 'slick-menu-item-divider');
+
+ // 4th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Clear all Grouping');
+
+ // 5th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item').find('i.mdi-arrow-collapse').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Collapse all Groups');
+
+ // 6th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('i.mdi-arrow-expand').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Expand all Groups');
+
+ // 7th item - divider
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(6)').should('have.class', 'slick-menu-item-divider');
+
+ // 8th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-download').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Export');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7)').find('.sub-item-chevron').should('exist');
+
+ // 9th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(8)').should('have.class', 'slick-menu-item-divider');
+
+ // 10th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(9) .menu-item')
+ .find('i.mdi-delete.text-danger')
+ .should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(9) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Delete Row');
+ });
+
+ it('should open Export->Excel context sub-menu', () => {
+ const subCommands1 = ['Export as Excel', 'Export as CSV', 'Export as PDF'];
+
+ const stub = cy.stub();
+ cy.on('window:alert', stub);
+
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').should('contain', '0');
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').rightclick({ force: true });
+
+ cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains(/^Export$/)
+ .trigger('mouseover');
+
+ cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list')
+ .should('exist')
+ .find('.slick-menu-item .menu-item')
+ .each(($command, index) => expect($command.text()).to.contain(subCommands1[index]));
+
+ // click different sub-menu
+ cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains('Export as Excel')
+ .should('exist')
+ .click()
+ .then(() => expect(stub.getCall(0)).to.be.calledWith('Export to Excel'));
+
+ cy.get('.slick-submenu').should('have.length', 0);
+ });
+
+ it('should open Header Menu from the "Title" column and expect some commands to have keyboard hints on the right side', () => {
+ cy.get('.slick-header-column:nth(0)').trigger('mouseover', { force: true });
+ cy.get('.slick-header-column:nth(0)').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('i.mdi-arrow-expand-horizontal')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Resize by Content');
+
+ // 2nd item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('i.mdi-sort-ascending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Ascending');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('kbd.key-hint').contains('Alt+โ');
+
+ // 4th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-sort-descending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Descending');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('kbd.key-hint').contains('Alt+โ');
+
+ // 5th item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('i.mdi-filter-remove-outline')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Filter');
+
+ // 7th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('i.mdi-sort-variant-off').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Sort');
+
+ // 8th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Hide Column');
+ });
+
+ it('should open Header Menu from the "Duration" column and expect some commands to have tags on the right side', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover', { force: true });
+ cy.get('.slick-header-column:nth(1)').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('i.mdi-arrow-expand-horizontal')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Resize by Content');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('span.key-hint.danger').contains('NEW');
+
+ // 2nd item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('i.mdi-sort-ascending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Ascending');
+
+ // 4th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-sort-descending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Descending');
+
+ // 5th item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('i.mdi-filter-remove-outline')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Filter');
+
+ // 7th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('i.mdi-sort-variant-off').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Sort');
+
+ // 8th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Hide Column');
+ });
+
+ it('should open Header Menu from the "Cost" column and expect first item to have a dynamic tooltip timestamp when hovering', () => {
+ cy.get('#grid51').find('.slick-header-column:nth(4)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.advanced-export-icon').contains('๐');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').contains('Advanced Export');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('kbd.key-hint').contains('Ctrl+E');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').should(
+ 'have.css',
+ 'background-color',
+ 'rgba(0, 0, 0, 0)'
+ );
+
+ // icon should scale up
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseover');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item .advanced-export-icon')
+ .invoke('css', 'transform')
+ .then((cssTransform) => {
+ const transformValue = cssTransform as unknown as string; // Cast to string
+ cy.getTransformValue(transformValue, true, 'scale').then((scaleValue) => {
+ expect(scaleValue).to.be.approximately(1.1, 1.15); // Check the scale value if applied
+ });
+ });
+
+ const today = format(new Date(), 'YYYY-MM-DD');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').should(
+ 'have.css',
+ 'background-color',
+ 'rgb(133, 70, 133)'
+ );
+ cy.get('.slick-custom-tooltip').contains(`๐ Export timestamp: ${today}`);
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseout');
+ cy.get('.slick-custom-tooltip').should('not.exist');
+ cy.get('body').click();
+ });
+
+ it('should open Action Menu from last column "Action" column and expect custom items listed in specific order', () => {
+ cy.get('[data-row="1"] > .slick-cell:nth(6)').click();
+ cy.get('.slick-command-header.with-title.with-close').contains('Cell Actions');
+
+ // 1st item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.mdi-content-copy').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Copy Cell Value');
+
+ // 2nd item - divider
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('.mdi-download').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item span').contains('Export Row');
+
+ // 4th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-download').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('span.menu-item-label').contains('Export');
+
+ // 5th item - divider
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('.edit-cell-icon').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item .edit-cell-icon').contains('โ');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5)').should('have.css', 'background-color', 'rgba(0, 0, 0, 0)');
+
+ // 7th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('.mdi-delete.text-danger').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item span').contains('Delete Row');
+ });
+
+ it('should open Export->Excel cell sub-menu', () => {
+ const subCommands1 = ['Export as Excel', 'Export as CSV', 'Export as PDF'];
+
+ const stub = cy.stub();
+ cy.on('window:alert', stub);
+
+ cy.get('.slick-cell-menu.slick-menu-level-0 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains(/^Export$/)
+ .trigger('mouseover');
+
+ cy.get('.slick-cell-menu.slick-menu-level-1 .slick-menu-command-list')
+ .should('exist')
+ .find('.slick-menu-item .menu-item')
+ .each(($command, index) => expect($command.text()).to.contain(subCommands1[index]));
+
+ // click different sub-menu
+ cy.get('.slick-cell-menu.slick-menu-level-1 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains('Export as Excel')
+ .should('exist')
+ .click()
+ .then(() => expect(stub.getCall(0)).to.be.calledWith('Export row #1 to Excel'));
+
+ cy.get('.slick-submenu').should('have.length', 0);
+ });
+
+ it('should open Grid Menu and expect built-in commands first then custom items listed in specific order', () => {
+ cy.get('.slick-grid-menu-button.mdi-menu').click();
+
+ // 1st item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('.mdi-filter-remove-outline.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Clear all Filters');
+
+ // 2nd item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item')
+ .find('.mdi-sort-variant-off.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item span').contains('Clear all Sorting');
+
+ // 3rd item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('.mdi-flip-vertical.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item span').contains('Toggle Filter Row');
+
+ // 4th item - divider
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(3)').should('have.class', 'slick-menu-item-divider');
+
+ // 5th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item')
+ .find('.mdi-file-excel-outline.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item span').contains('Export to Excel');
+
+ // 6th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('.mdi-download.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item span').contains('Export to CSV');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('span.key-hint.warn').contains('CUSTOM');
+
+ // 7th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('.mdi-refresh.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item span').contains('Refresh Data');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('kbd.key-hint').contains('F5');
+ });
+
+ it('should sort ascending "Duration" even though the header menu item was override without an action callback', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').should('contain', 'Sort Ascending').click();
+
+ cy.get('[data-row=0]').children('.slick-cell:nth(1)').should('contain', '0');
+ cy.get('[data-row=1]').children('.slick-cell:nth(1)').should('contain', '0');
+ cy.get('[data-row=2]').children('.slick-cell:nth(1)').should('contain', '0');
+ });
+
+ it('should sort descending "Duration" even though the header menu item was override without an action callback', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').should('contain', 'Sort Descending').click();
+
+ cy.get('[data-row=0]').children('.slick-cell:nth(1)').should('contain', '100');
+ cy.get('[data-row=1]').children('.slick-cell:nth(1)').should('contain', '100');
+ cy.get('[data-row=2]').children('.slick-cell:nth(1)').should('contain', '100');
+ });
+});
diff --git a/demos/react/test/cypress/support/commands.ts b/demos/react/test/cypress/support/commands.ts
index a35be6228b..3be5a3685b 100644
--- a/demos/react/test/cypress/support/commands.ts
+++ b/demos/react/test/cypress/support/commands.ts
@@ -47,6 +47,7 @@ declare global {
): Chainable>;
saveLocalStorage: () => void;
restoreLocalStorage: () => void;
+ getTransformValue(cssTransformMatrix: string, absoluteValue: boolean, transformType?: 'rotate' | 'scale'): Chainable;
}
}
}
@@ -86,3 +87,35 @@ Cypress.Commands.add('restoreLocalStorage', () => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
});
});
+
+Cypress.Commands.add(
+ 'getTransformValue',
+ (
+ cssTransformMatrix: string,
+ absoluteValue: boolean,
+ transformType: 'rotate' | 'scale' = 'rotate' // Default to 'rotate'
+ ): Cypress.Chainable => {
+ if (!cssTransformMatrix || cssTransformMatrix === 'none') {
+ throw new Error('Transform matrix is undefined or none');
+ }
+
+ const cssTransformMatrixIndexes = cssTransformMatrix.split('(')[1].split(')')[0].split(',');
+
+ if (transformType === 'rotate') {
+ const cssTransformScale = Math.sqrt(
+ +cssTransformMatrixIndexes[0] * +cssTransformMatrixIndexes[0] + +cssTransformMatrixIndexes[1] * +cssTransformMatrixIndexes[1]
+ );
+
+ const cssTransformSin = +cssTransformMatrixIndexes[1] / cssTransformScale;
+ const cssTransformAngle = Math.round(Math.asin(cssTransformSin) * (180 / Math.PI));
+
+ return cy.wrap(absoluteValue ? Math.abs(cssTransformAngle) : cssTransformAngle);
+ } else if (transformType === 'scale') {
+ // Assuming scale is based on the first value in the matrix.
+ const scaleValue = +cssTransformMatrixIndexes[0]; // First value typically represents scaling in x direction.
+ return cy.wrap(scaleValue); // Directly return the scale value.
+ }
+
+ throw new Error('Unsupported transform type');
+ }
+);
diff --git a/demos/react/tsconfig.json b/demos/react/tsconfig.json
index 68ce43f7d8..669403216e 100644
--- a/demos/react/tsconfig.json
+++ b/demos/react/tsconfig.json
@@ -4,7 +4,6 @@
"target": "es2022",
"module": "esnext",
"sourceMap": true,
- "downlevelIteration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"esModuleInterop": true,
diff --git a/demos/vanilla/CHANGELOG.md b/demos/vanilla/CHANGELOG.md
index 0d4cafd829..3c3d9f0ab0 100644
--- a/demos/vanilla/CHANGELOG.md
+++ b/demos/vanilla/CHANGELOG.md
@@ -4,6 +4,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [10.0.0-beta.0](https://github.com/ghiscoding/slickgrid-universal/compare/v9.12.0...v10.0.0-beta.0) (2026-02-14)
+
+### โ BREAKING CHANGES
+
+* migrate Row/Hybrid Selection flag into a single `enableSelection` flag (#2331)
+* drop Cell/Row Selection Models & keep only HybridSelectionModel (#2330)
+* remove all Deprecated code (#2302)
+* drop OperatorType enums and keep only type literal (#2301)
+* replacing multiple TypeScript `enum` with `type` to decrease build size (#2300)
+* switch to column `hidden` property and always keep all columns (#2281)
+
+### Features
+
+* add custom menu slot renderers ([#2375](https://github.com/ghiscoding/slickgrid-universal/issues/2375)) ([7ebbda5](https://github.com/ghiscoding/slickgrid-universal/commit/7ebbda58233bb5ce94b81b5b2b578af0ea6a068d)) - by @ghiscoding
+* auto-enabled external resources with their associated flags ([#2362](https://github.com/ghiscoding/slickgrid-universal/issues/2362)) ([16dd8a7](https://github.com/ghiscoding/slickgrid-universal/commit/16dd8a77dd5d136a5a99321f0fc4c50571fdb0c0)) - by @ghiscoding
+* drop Cell/Row Selection Models & keep only HybridSelectionModel ([#2330](https://github.com/ghiscoding/slickgrid-universal/issues/2330)) ([4398cf4](https://github.com/ghiscoding/slickgrid-universal/commit/4398cf42e03f6971b81db8cea4ed11138c0aa452)) - by @ghiscoding
+* migrate Row/Hybrid Selection flag into a single `enableSelection` flag ([#2331](https://github.com/ghiscoding/slickgrid-universal/issues/2331)) ([5be5e6a](https://github.com/ghiscoding/slickgrid-universal/commit/5be5e6a862ecd024cf43d404769d65c6c1dd335e)) - by @ghiscoding
+* switch to column `hidden` property and always keep all columns ([#2281](https://github.com/ghiscoding/slickgrid-universal/issues/2281)) ([075c649](https://github.com/ghiscoding/slickgrid-universal/commit/075c64961cb7400500df46b792866d39fba2d9e0)) - by @ghiscoding
+* **tooltip:** add global tooltip observation for non-grid elements ([#2371](https://github.com/ghiscoding/slickgrid-universal/issues/2371)) ([1bbc8de](https://github.com/ghiscoding/slickgrid-universal/commit/1bbc8de895e370843286eadd08574efc552ad8fd)) - by @ghiscoding
+
+### Bug Fixes
+
+* **plugin:** SlickCustomTooltip should work with parent+child tooltips ([#2374](https://github.com/ghiscoding/slickgrid-universal/issues/2374)) ([8af7f45](https://github.com/ghiscoding/slickgrid-universal/commit/8af7f45eb19f0a00da2f3de7c729504be7d043eb)) - by @ghiscoding
+* remove all Deprecated code ([#2302](https://github.com/ghiscoding/slickgrid-universal/issues/2302)) ([f42c46c](https://github.com/ghiscoding/slickgrid-universal/commit/f42c46cd1f05b5c72c62f552f124b5bfe776f8b0)) - by @ghiscoding
+
+### Code Refactoring
+
+* drop OperatorType enums and keep only type literal ([#2301](https://github.com/ghiscoding/slickgrid-universal/issues/2301)) ([5dd0807](https://github.com/ghiscoding/slickgrid-universal/commit/5dd08079460dc9af798ab29527997a6d4b31abdd)) - by @ghiscoding
+* replacing multiple TypeScript `enum` with `type` to decrease build size ([#2300](https://github.com/ghiscoding/slickgrid-universal/issues/2300)) ([ea79395](https://github.com/ghiscoding/slickgrid-universal/commit/ea79395cf663b3abce8e43cf27ba6ffea7cfe113)) - by @ghiscoding
+
## [9.13.0](https://github.com/ghiscoding/slickgrid-universal/compare/v9.12.0...v9.13.0) (2026-01-30)
### Features
diff --git a/demos/vanilla/package.json b/demos/vanilla/package.json
index 4aaf764118..606bae1b01 100644
--- a/demos/vanilla/package.json
+++ b/demos/vanilla/package.json
@@ -1,7 +1,7 @@
{
"name": "vanilla-demo",
"private": true,
- "version": "9.13.0",
+ "version": "10.0.0-beta.0",
"scripts": {
"build": "pnpm type-check && vite build",
"type-check": "tsc --noEmit",
@@ -38,6 +38,6 @@
"@types/node": "catalog:",
"sass": "catalog:",
"typescript": "catalog:",
- "vite": "catalog:vite7"
+ "vite": "catalog:"
}
}
diff --git a/demos/vanilla/src/app-routing.ts b/demos/vanilla/src/app-routing.ts
index 54de24e7ce..3a557e7581 100644
--- a/demos/vanilla/src/app-routing.ts
+++ b/demos/vanilla/src/app-routing.ts
@@ -37,6 +37,7 @@ import Example36 from './examples/example36.js';
import Example37 from './examples/example37.js';
import Example38 from './examples/example38.js';
import Example39 from './examples/example39.js';
+import Example40 from './examples/example40.js';
import Icons from './examples/icons.js';
import type { RouterConfig } from './interfaces.js';
@@ -84,6 +85,7 @@ export class AppRouting {
{ route: 'example37', name: 'example37', view: './examples/example37.html', viewModel: Example37, title: 'Example37' },
{ route: 'example38', name: 'example38', view: './examples/example38.html', viewModel: Example38, title: 'Example38' },
{ route: 'example39', name: 'example39', view: './examples/example39.html', viewModel: Example39, title: 'Example39' },
+ { route: 'example40', name: 'example40', view: './examples/example40.html', viewModel: Example40, title: 'Example40' },
{ route: '', redirect: 'example01' },
{ route: '**', redirect: 'example01' },
];
diff --git a/demos/vanilla/src/app.html b/demos/vanilla/src/app.html
index e665df91ee..5e7542fc5a 100644
--- a/demos/vanilla/src/app.html
+++ b/demos/vanilla/src/app.html
@@ -79,6 +79,7 @@
diff --git a/demos/vanilla/src/examples/example16.ts b/demos/vanilla/src/examples/example16.ts
index 794a8e40d7..7141123731 100644
--- a/demos/vanilla/src/examples/example16.ts
+++ b/demos/vanilla/src/examples/example16.ts
@@ -4,11 +4,11 @@ import {
Editors,
Filters,
Formatters,
- OperatorType,
type Column,
type EditCommand,
type GridOption,
type MultipleSelectOption,
+ type OperatorType,
type SliderOption,
type SliderRangeOption,
type VanillaCalendarOption,
@@ -164,6 +164,28 @@ export default class Example16 {
// maxHeight: 30,
},
},
+ {
+ id: 'button',
+ name: 'Button Tooltip',
+ field: 'title',
+ width: 100,
+ minWidth: 100,
+ filterable: true,
+ excludeFromExport: true,
+ formatter: (_row, _cell, value) => {
+ const button = createDomElement('button', { className: 'button is-small', title: 'This is the button tooltip' });
+ const icon = createDomElement('span', { className: 'mdi mdi-information', title: 'icon tooltip' });
+ const text = createDomElement('span', { textContent: 'Hello Task' });
+ button.appendChild(icon);
+ button.appendChild(text);
+ button.addEventListener('click', () => alert(`Clicked button for ${value}`));
+ return button;
+ },
+ // define tooltip options here OR for the entire grid via the grid options (cell tooltip options will have precedence over grid options)
+ customTooltip: {
+ useRegularTooltip: true, // note regular tooltip will try to find a "title" attribute in the cell formatter (it won't work without a cell formatter)
+ },
+ },
{
id: 'cost',
name: 'Cost (in โฌ)',
@@ -353,7 +375,7 @@ export default class Example16 {
collectionOptions: { separatorBetweenTextLabels: ' ' },
options: { minHeight: 70 } as MultipleSelectOption,
model: Filters.multipleSelect,
- operator: OperatorType.inContains,
+ operator: 'IN_CONTAINS',
},
},
{
@@ -449,20 +471,22 @@ export default class Example16 {
headerFormatter: this.headerFormatter,
headerRowFormatter: this.headerRowFormatter,
usabilityOverride: (args) => args.cell !== 0 && args?.column?.id !== 'action', // don't show on first/last columns
+ observeAllTooltips: true, // observe all elements with title/data-slick-tooltip attributes (not just SlickGrid elements)
+ observeTooltipContainer: '.tooltip-container', // defaults to 'body', target a specific container (only works when observeAllTooltips is enabled)
},
presets: {
filters: [{ columnId: 'prerequisites', searchTerms: [1, 3, 5, 7, 9, 12, 15, 18, 21, 25, 28, 29, 30, 32, 34] }],
},
- rowHeight: 33,
+ rowHeight: 38,
headerRowHeight: 35,
enableFiltering: true,
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
showCustomFooter: true,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
checkboxSelector: {
hideInFilterHeaderRow: false,
hideInColumnTitleRow: true,
@@ -479,7 +503,7 @@ export default class Example16 {
onCommand: (e, args) => this.executeCommand(e, args),
onOptionSelected: (_e, args) => {
// change "Completed" property with new option selected from the Cell Menu
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && dataContext.hasOwnProperty('completed')) {
dataContext.completed = args.item.option;
this.sgb.gridService.updateItem(dataContext);
diff --git a/demos/vanilla/src/examples/example17.html b/demos/vanilla/src/examples/example17.html
index 1f048fe417..45a63c547e 100644
--- a/demos/vanilla/src/examples/example17.html
+++ b/demos/vanilla/src/examples/example17.html
@@ -52,14 +52,18 @@
Dragging from 2nd column to make a se
-
Grid 1 - Using SlickCellRangeSelector
+
+ Grid 1 - Using SlickCellRangeSelector with SlickHybridRangeSelector({ selectionType: 'cell' })
+
-
Grid 2 - Using SlickCellRangeSelector and SlickRowSelectionModel
+
+ Grid 2 - Using SlickCellRangeSelector and SlickHybridRangeSelector({ selectionType: 'row' })
+
diff --git a/demos/vanilla/src/examples/example17.ts b/demos/vanilla/src/examples/example17.ts
index e3c83b2df8..a133745b42 100644
--- a/demos/vanilla/src/examples/example17.ts
+++ b/demos/vanilla/src/examples/example17.ts
@@ -3,8 +3,7 @@ import {
Formatters,
GroupTotalFormatters,
SlickCellRangeSelector,
- SlickCellSelectionModel,
- SlickRowSelectionModel,
+ SlickHybridSelectionModel,
type Column,
type GridOption,
type Grouping,
@@ -233,7 +232,8 @@ export default class Example17 {
setOptions() {
this.sgb1.slickGrid?.setSelectionModel(
- new SlickCellSelectionModel({
+ new SlickHybridSelectionModel({
+ selectionType: 'cell',
selectActiveCell: true,
cellRangeSelector: new SlickCellRangeSelector({
selectionCss: {
@@ -248,8 +248,8 @@ export default class Example17 {
);
this.sgb2.slickGrid?.setSelectionModel(
- // or use SlickHybridSelectionModel with `selectionType: 'row'`
- new SlickRowSelectionModel({
+ new SlickHybridSelectionModel({
+ selectionType: 'row',
cellRangeSelector: new SlickCellRangeSelector({
selectionCss: {
border: 'none',
diff --git a/demos/vanilla/src/examples/example19.html b/demos/vanilla/src/examples/example19.html
index 1c68629d34..bd6033355f 100644
--- a/demos/vanilla/src/examples/example19.html
+++ b/demos/vanilla/src/examples/example19.html
@@ -20,7 +20,7 @@
-
Grid - using enableExcelCopyBuffer which uses SlickCellSelectionModel
+
Grid - using enableExcelCopyBuffer which uses SlickHybridSelectionModel
The complete first row and the cells C - E of the second row are not allowing to paste values.
Additionally the columns are configured to exportWithFormatter and a custom formatter that renders the cells coordinates or
diff --git a/demos/vanilla/src/examples/example20.ts b/demos/vanilla/src/examples/example20.ts
index 992e6d04be..1a72a4b4dc 100644
--- a/demos/vanilla/src/examples/example20.ts
+++ b/demos/vanilla/src/examples/example20.ts
@@ -2,7 +2,6 @@ import { BindingEventService } from '@slickgrid-universal/binding';
import {
createDomElement,
Editors,
- ExtensionName,
Filters,
Formatters,
SlickEventHandler,
@@ -177,7 +176,7 @@ export default class Example20 {
// Row Detail View is a special case because of its requirement to create extra column definition dynamically
// so it must be pre-registered before SlickGrid is instantiated, we can do so via this option
this.rowDetail = new SlickRowDetailView(pubSubService);
- return [{ name: ExtensionName.rowDetailView, instance: this.rowDetail }];
+ return [{ name: 'rowDetailView', instance: this.rowDetail }];
},
rowHeight: 33,
rowTopOffsetRenderType: 'top', // RowDetail and/or RowSpan don't render well with "transform", you should use "top"
@@ -198,14 +197,14 @@ export default class Example20 {
// you can override it here in the options or externally by calling the method on the plugin instance
expandableOverride: (_row, dataContext) => dataContext.id % 2 === 1,
},
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
// You could also enable Row Selection as well, but just make sure to disable `useRowClick: false`
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
checkboxSelector: {
hideInFilterHeaderRow: false,
hideSelectAllCheckbox: true,
diff --git a/demos/vanilla/src/examples/example21.ts b/demos/vanilla/src/examples/example21.ts
index 6535a53deb..71a4581a49 100644
--- a/demos/vanilla/src/examples/example21.ts
+++ b/demos/vanilla/src/examples/example21.ts
@@ -1,6 +1,6 @@
import { faker } from '@faker-js/faker';
import { BindingEventService } from '@slickgrid-universal/binding';
-import { createDomElement, ExtensionName, SlickEventHandler, type Column, type GridOption } from '@slickgrid-universal/common';
+import { createDomElement, SlickEventHandler, type Column, type GridOption } from '@slickgrid-universal/common';
import { SlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin';
import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
import { ExampleGridOptions } from './example-grid-options.js';
@@ -140,7 +140,7 @@ export default class Example21 {
// Row Detail View is a special case because of its requirement to create extra column definition dynamically
// so it must be pre-registered before SlickGrid is instantiated, we can do so via this option
this.rowDetail = new SlickRowDetailView(pubSubService);
- return [{ name: ExtensionName.rowDetailView, instance: this.rowDetail }];
+ return [{ name: 'rowDetailView', instance: this.rowDetail }];
},
rowTopOffsetRenderType: 'top', // RowDetail and/or RowSpan don't render well with "transform", you should use "top"
rowHeight: 33,
diff --git a/demos/vanilla/src/examples/example25.ts b/demos/vanilla/src/examples/example25.ts
index 77aab02cbe..19a3f4abc5 100644
--- a/demos/vanilla/src/examples/example25.ts
+++ b/demos/vanilla/src/examples/example25.ts
@@ -3,7 +3,6 @@ import { BindingEventService } from '@slickgrid-universal/binding';
import {
Filters,
Formatters,
- OperatorType,
type Column,
type CurrentFilter,
type GridOption,
@@ -124,7 +123,7 @@ export default class Example25 {
filter: {
model: Filters.sliderRange,
maxValue: 100, // or you can use the options as well
- operator: OperatorType.rangeInclusive, // defaults to inclusive
+ operator: 'RangeInclusive', // defaults to inclusive
options: {
hideSliderNumbers: false, // you can hide/show the slider numbers on both side
min: 0,
@@ -173,7 +172,7 @@ export default class Example25 {
filterable: true,
filter: {
model: Filters.input,
- operator: OperatorType.rangeExclusive, // defaults to exclusive
+ operator: 'RangeExclusive', // defaults to exclusive
},
},
{
@@ -313,10 +312,10 @@ export default class Example25 {
filters = [
{
columnId: 'finish',
- operator: OperatorType.rangeInclusive,
+ operator: 'RangeInclusive',
searchTerms: [`${currentYear}-01-01`, `${currentYear}-12-31`],
},
- { columnId: 'completed', operator: OperatorType.equal, searchTerms: [true] },
+ { columnId: 'completed', operator: '=', searchTerms: [true] },
];
break;
case 'nextYearTasks':
diff --git a/demos/vanilla/src/examples/example26.ts b/demos/vanilla/src/examples/example26.ts
index 566e7013e9..12be05f140 100644
--- a/demos/vanilla/src/examples/example26.ts
+++ b/demos/vanilla/src/examples/example26.ts
@@ -3,7 +3,6 @@ import { BindingEventService } from '@slickgrid-universal/binding';
import {
Aggregators,
Filters,
- OperatorType,
SortComparers,
type Column,
type GridOption,
@@ -120,7 +119,7 @@ export default class Example26 {
enableCellNavigation: true,
enableFiltering: true,
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
enableGrouping: true,
headerMenu: {
hideFreezeColumnsCommand: false,
@@ -137,7 +136,7 @@ export default class Example26 {
enableCount: true,
filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => {
- if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
+ if (columnFilterOperator === 'Custom' && columnDef?.id === 'name') {
let matchesSearch = searchValues[0].replace(/\*/g, '.*');
matchesSearch = matchesSearch.slice(0, 1) + CARET_HTML_ESCAPED + matchesSearch.slice(1);
matchesSearch = matchesSearch.slice(0, -1) + "$'";
diff --git a/demos/vanilla/src/examples/example29.ts b/demos/vanilla/src/examples/example29.ts
index afa8542d58..d8a883a433 100644
--- a/demos/vanilla/src/examples/example29.ts
+++ b/demos/vanilla/src/examples/example29.ts
@@ -76,11 +76,12 @@ export default class Example29 {
gridWidth: 800,
rowHeight: 33,
enableCellNavigation: true,
- enableRowSelection: true,
+ enableSelection: true,
enableRowMoveManager: true,
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
+ selectionType: 'row',
},
rowMoveManager: {
columnIndexPosition: 0,
diff --git a/demos/vanilla/src/examples/example30.ts b/demos/vanilla/src/examples/example30.ts
index a24540570f..23b7b7f4d5 100644
--- a/demos/vanilla/src/examples/example30.ts
+++ b/demos/vanilla/src/examples/example30.ts
@@ -1,7 +1,6 @@
import {
Filters,
Formatters,
- OperatorType,
type Column,
type GridOption,
type MultipleSelectOption,
@@ -80,7 +79,7 @@ export default class Example30 {
filter: {
model: Filters.sliderRange,
maxValue: 100, // or you can use the options as well
- operator: OperatorType.rangeInclusive, // defaults to inclusive
+ operator: 'RangeInclusive', // defaults to inclusive
options: {
hideSliderNumbers: false, // you can hide/show the slider numbers on both side
min: 0,
@@ -126,7 +125,7 @@ export default class Example30 {
filterable: true,
filter: {
model: Filters.input,
- operator: OperatorType.rangeExclusive, // defaults to exclusive
+ operator: 'RangeExclusive', // defaults to exclusive
},
},
{
diff --git a/demos/vanilla/src/examples/example32.ts b/demos/vanilla/src/examples/example32.ts
index c25474e8a6..8b35a7affd 100644
--- a/demos/vanilla/src/examples/example32.ts
+++ b/demos/vanilla/src/examples/example32.ts
@@ -458,7 +458,8 @@ export default class Example32 {
}
}
- // update column definitions
+ // 1. update column definitions via grid.setColumns()
+ // this will shift colspan/rowspan to the left or right accordingly
if (this.showEmployeeId) {
this.columnDefinitions.unshift({ id: 'employeeID', name: 'Employee ID', field: 'employeeID', width: 100 });
} else {
@@ -466,6 +467,22 @@ export default class Example32 {
}
this.sgb.slickGrid?.setColumns(this.columnDefinitions);
+ // --- OR ---
+ // 2. OR update via "hidden" column flag & increase/decrease column index accordingly in the metadata
+ // this approach will keep colspan/rowspan "as-is" but will hide the EmployeeID column
+ /*
+ const colDirIdx = this.showEmployeeId ? -1 : 1;
+ for (const row of Object.keys(this.metadata)) {
+ newMetadata[row] = { columns: {} };
+ for (const col of Object.keys((this.metadata as any)[row].columns)) {
+ newMetadata[row].columns[Number(col) + colDirIdx] = (this.metadata as any)[row].columns[col];
+ }
+ }
+ this.sgb.slickGrid?.setOptions({ frozenColumn: this.showEmployeeId ? 0 : 1 });
+ this.sgb.slickGrid?.updateColumnById('employeeID', { hidden: !this.showEmployeeId });
+ this.sgb.slickGrid?.updateColumns();
+ */
+
// update & remap rowspans
this.metadata = newMetadata;
this.sgb.slickGrid?.remapAllColumnsRowSpan();
diff --git a/demos/vanilla/src/examples/example34.ts b/demos/vanilla/src/examples/example34.ts
index ec6d0afd35..b37a32e23c 100644
--- a/demos/vanilla/src/examples/example34.ts
+++ b/demos/vanilla/src/examples/example34.ts
@@ -134,8 +134,8 @@ export default class Example34 {
enableExcelExport: true,
externalResources: [new ExcelExportService()],
enableCheckboxSelector: true,
- enableRowSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
diff --git a/demos/vanilla/src/examples/example35.ts b/demos/vanilla/src/examples/example35.ts
index bc96449db7..e0ea2b157d 100644
--- a/demos/vanilla/src/examples/example35.ts
+++ b/demos/vanilla/src/examples/example35.ts
@@ -124,7 +124,7 @@ export default class Example35 {
sanitizeDataExport: true,
},
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
multiSelect: false,
checkboxSelector: {
// columnIndexPosition: 1,
diff --git a/demos/vanilla/src/examples/example36.ts b/demos/vanilla/src/examples/example36.ts
index 10d944338a..691e68ee44 100644
--- a/demos/vanilla/src/examples/example36.ts
+++ b/demos/vanilla/src/examples/example36.ts
@@ -2,7 +2,6 @@ import { BindingEventService } from '@slickgrid-universal/binding';
import {
Aggregators,
createDomElement,
- ExtensionName,
Filters,
Formatters,
GroupTotalFormatters,
@@ -199,7 +198,7 @@ export default class Example36 {
// Row Detail View is a special case because of its requirement to create extra column definition dynamically
// so it must be pre-registered before SlickGrid is instantiated, we can do so via this option
this.rowDetail = new SlickRowDetailView(pubSubService);
- return [{ name: ExtensionName.rowDetailView, instance: this.rowDetail }];
+ return [{ name: 'rowDetailView', instance: this.rowDetail }];
},
rowHeight: 33,
rowTopOffsetRenderType: 'top', // RowDetail and/or RowSpan don't render well with "transform", you should use "top"
@@ -216,14 +215,14 @@ export default class Example36 {
// example, if you choosed 6 panelRows, the display will in fact use 5 rows
panelRows: this.detailViewRowCount,
},
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
},
// You could also enable Row Selection as well, but just make sure to disable `useRowClick: false`
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
checkboxSelector: {
hideInFilterHeaderRow: false,
hideSelectAllCheckbox: true,
diff --git a/demos/vanilla/src/examples/example37.html b/demos/vanilla/src/examples/example37.html
index 75f7407042..e7aaafb3af 100644
--- a/demos/vanilla/src/examples/example37.html
+++ b/demos/vanilla/src/examples/example37.html
@@ -22,8 +22,9 @@
SlickHybridSelectionModel This Selection Model is an hybrid approach that uses a combination of the row or cell
- selections depending on certain conditions. Use enableHybridSelection grid option to enable the new Hybrid Selection
- Model.
+ selections depending on certain conditions.
+ Use { enableSelection: true, selectionOptions: { selectionType: 'mixed' }} grid option to enable the
+ new Hybrid Selection Model.
1. clicking on the first column (id) will use RowSelectionModel because of our configuration of
diff --git a/demos/vanilla/src/examples/example37.ts b/demos/vanilla/src/examples/example37.ts
index 6c7f99bd5a..8cf8e6cbbf 100644
--- a/demos/vanilla/src/examples/example37.ts
+++ b/demos/vanilla/src/examples/example37.ts
@@ -136,9 +136,10 @@ export default class Example37 {
externalResources: [new ExcelExportService()],
// enable new hybrid selection model (rows & cells)
- enableHybridSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
rowSelectColumnIds: ['id'],
+ selectionType: 'mixed',
},
// when using the ExcelCopyBuffer, you can see what the selection range is
@@ -154,8 +155,8 @@ export default class Example37 {
...this.gridOptions1,
// you can also enable checkbox selection & row selection, make sure to use `rowSelectColumnIds: ['id', '_checkbox_selector']`
enableCheckboxSelector: true,
- enableRowSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
diff --git a/demos/vanilla/src/examples/example38.html b/demos/vanilla/src/examples/example38.html
index 3227fb5ba8..cd204c6291 100644
--- a/demos/vanilla/src/examples/example38.html
+++ b/demos/vanilla/src/examples/example38.html
@@ -27,7 +27,8 @@
Spreadsheet with drag-fill, hybrid selection model. Type a few values in the grid and then select those cells and use the bottom right
drag handle spread the selection and auto-fill the values to other cells. Use onDragReplaceCells event to customize the
- drag-fill behavior. Use enableHybridSelection grid option to enable the new Hybrid Selection Model.
+ drag-fill behavior. Use { enableSelection: true, selectionOptions: { selectionType: 'mixed' }}
+ grid option to enable the new Hybrid Selection Model.
diff --git a/demos/vanilla/src/examples/example38.ts b/demos/vanilla/src/examples/example38.ts
index cc97146806..decd963a4f 100644
--- a/demos/vanilla/src/examples/example38.ts
+++ b/demos/vanilla/src/examples/example38.ts
@@ -83,10 +83,11 @@ export default class Example38 {
editorNavigateOnArrows: true, // enable editor navigation using arrow keys
// enable new hybrid selection model (rows & cells)
- enableHybridSelection: true,
- rowSelectionOptions: {
- selectActiveRow: true,
+ enableSelection: true,
+ selectionOptions: {
rowSelectColumnIds: ['selector'],
+ selectActiveRow: true,
+ selectionType: 'mixed',
},
// when using the ExcelCopyBuffer, you can see what the selection range is
diff --git a/demos/vanilla/src/examples/example39.ts b/demos/vanilla/src/examples/example39.ts
index 6896c2e729..34b17a7e3f 100644
--- a/demos/vanilla/src/examples/example39.ts
+++ b/demos/vanilla/src/examples/example39.ts
@@ -86,8 +86,8 @@ export default class Example01 {
gridHeight: 225,
gridWidth: 800,
rowHeight: 33,
- enableHybridSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
selectionType: 'row',
},
};
diff --git a/demos/vanilla/src/examples/example40.html b/demos/vanilla/src/examples/example40.html
new file mode 100644
index 0000000000..fa04a7f09b
--- /dev/null
+++ b/demos/vanilla/src/examples/example40.html
@@ -0,0 +1,57 @@
+
+ Example 40 - Menus with Slots
+ (Custom Menu Item Renderer)
+
+
+
+
+
+ Click on the menu buttons to see the new single slot functionality working across all menu types (Header Menu, Cell
+ Menu, Context Menu, Grid Menu):
+
+
+ Note: The demo focuses on the custom rendering capability via slotRenderer and
+ defaultMenuItemRenderer, which work across all menu plugins (SlickHeaderMenu, SlickCellMenu, SlickContextMenu,
+ SlickGridMenu). Also note that the keyboard shortcuts displayed in the menus (e.g., Alt+โ, F5) are for demo
+ purposes only and do not actually trigger any actions.
+
+
+
+
+
+
+
+
+ Clear grouping
+
+
+
+ Collapse all groups
+
+
+
+ Expand all groups
+
+ Group by Duration
+
+ `,
+ action: () => alert('Export to CSV'),
+ },
+ {
+ command: 'refresh-data',
+ title: 'Refresh Data',
+ iconCssClass: 'mdi mdi-refresh',
+ // Demo: slotRenderer with keyboard shortcut
+ slotRenderer: (cmdItem) => {
+ // you can use `createDomElement()` from Slickgrid for easier DOM element creation
+ const menuItemElm = createDomElement('div', { className: 'menu-item' });
+ const iconElm = createDomElement('i', { className: `${cmdItem.iconCssClass} menu-item-icon` });
+ const menuItemLabelElm = createDomElement('span', { className: 'menu-item-label', textContent: cmdItem.title || '' });
+ const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'F5' });
+ menuItemElm.appendChild(iconElm);
+ menuItemElm.appendChild(menuItemLabelElm);
+ menuItemElm.appendChild(kbdElm);
+ return menuItemElm;
+ },
+ action: () => alert('Refresh data'),
+ },
+ ] as Array;
+ },
+ },
+
+ // tooltip plugin
+ externalResources: [new SlickCustomTooltip()],
+ customTooltip: {
+ observeAllTooltips: true,
+ },
+ };
+ }
+
+ clearGrouping() {
+ this.sgb?.dataView?.setGrouping([]);
+ }
+
+ collapseAllGroups() {
+ this.sgb?.dataView?.collapseAllGroups();
+ }
+
+ expandAllGroups() {
+ this.sgb?.dataView?.expandAllGroups();
+ }
+
+ groupByDuration() {
+ // you need to manually add the sort icon(s) in UI
+ this.sgb?.slickGrid?.setSortColumns([{ columnId: 'duration', sortAsc: true }]);
+ this.sgb?.dataView?.setGrouping({
+ getter: 'duration',
+ formatter: (g) => `Duration: ${g.value} (${g.count} items)`,
+ comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.asc),
+ aggregators: [new Aggregators.Avg('percentComplete'), new Aggregators.Sum('cost')],
+ aggregateCollapsed: false,
+ lazyTotalsCalculation: true,
+ } as Grouping);
+ this.sgb?.slickGrid?.invalidate(); // invalidate all rows and re-render
+ }
+
+ loadData(count: number): ReportItem[] {
+ const tmpData: ReportItem[] = [];
+ for (let i = 0; i < count; i++) {
+ const randomDuration = Math.round(Math.random() * 100);
+ const randomYear = 2000 + Math.floor(Math.random() * 10);
+ const randomMonth = Math.floor(Math.random() * 11);
+ const randomDay = Math.floor(Math.random() * 29);
+ const randomPercent = Math.round(Math.random() * 100);
+
+ tmpData[i] = {
+ id: i,
+ title: 'Task ' + i,
+ duration: randomDuration,
+ cost: Math.round(Math.random() * 10000) / 100,
+ percentComplete: randomPercent,
+ start: new Date(randomYear, randomMonth, randomDay),
+ finish: new Date(randomYear, randomMonth + 1, randomDay),
+ };
+ }
+ return tmpData;
+ }
+
+ toggleSubTitle() {
+ this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block';
+ this.sgb.resizerService.resizeGrid();
+ }
+}
diff --git a/demos/vanilla/src/examples/utilities.ts b/demos/vanilla/src/examples/utilities.ts
index 8fcf814778..91a006f06d 100644
--- a/demos/vanilla/src/examples/utilities.ts
+++ b/demos/vanilla/src/examples/utilities.ts
@@ -50,13 +50,6 @@ export function showToast(msg: string, type: 'danger' | 'info' | 'warning', time
}, time);
return;
}
-
- // @deprecated, remove fallback in next major release
- // otherwise, fallback (when popover is not supported): keep the div visible as regular HTML and remove after timeout.
- div.style.left = '50%';
- div.style.top = '20px';
- div.style.transform = 'translateX(-50%)';
- setTimeout(() => div.remove(), time);
}
export function zeroPadding(input: string | number) {
diff --git a/demos/vanilla/vite.config.mts b/demos/vanilla/vite.config.mts
index 3c9ffb1fa0..5db914360a 100644
--- a/demos/vanilla/vite.config.mts
+++ b/demos/vanilla/vite.config.mts
@@ -11,6 +11,13 @@ export default defineConfig(() => {
emptyOutDir: true,
outDir: '../../website',
},
+ css: {
+ preprocessorOptions: {
+ scss: {
+ quietDeps: true,
+ },
+ },
+ },
preview: {
port: 8888,
},
diff --git a/demos/vue/CHANGELOG.md b/demos/vue/CHANGELOG.md
index 42544c56c8..315b1aa6c3 100644
--- a/demos/vue/CHANGELOG.md
+++ b/demos/vue/CHANGELOG.md
@@ -4,6 +4,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [10.0.0-beta.0](https://github.com/ghiscoding/slickgrid-universal/compare/v9.12.0...v10.0.0-beta.0) (2026-02-14)
+
+### โ BREAKING CHANGES
+
+* migrate Row/Hybrid Selection flag into a single `enableSelection` flag (#2331)
+* drop Cell/Row Selection Models & keep only HybridSelectionModel (#2330)
+* remove all Deprecated code (#2302)
+* drop OperatorType enums and keep only type literal (#2301)
+* replacing multiple TypeScript `enum` with `type` to decrease build size (#2300)
+* rename `v-model:data` to `v-model:dataset` (#2298)
+* make Row Detail plugin as optional in all framework wrappers (#2291)
+* switch to column `hidden` property and always keep all columns (#2281)
+
+### Features
+
+* add custom menu slot renderers ([#2375](https://github.com/ghiscoding/slickgrid-universal/issues/2375)) ([7ebbda5](https://github.com/ghiscoding/slickgrid-universal/commit/7ebbda58233bb5ce94b81b5b2b578af0ea6a068d)) - by @ghiscoding
+* auto-enabled external resources with their associated flags ([#2362](https://github.com/ghiscoding/slickgrid-universal/issues/2362)) ([16dd8a7](https://github.com/ghiscoding/slickgrid-universal/commit/16dd8a77dd5d136a5a99321f0fc4c50571fdb0c0)) - by @ghiscoding
+* drop Cell/Row Selection Models & keep only HybridSelectionModel ([#2330](https://github.com/ghiscoding/slickgrid-universal/issues/2330)) ([4398cf4](https://github.com/ghiscoding/slickgrid-universal/commit/4398cf42e03f6971b81db8cea4ed11138c0aa452)) - by @ghiscoding
+* make Row Detail plugin as optional in all framework wrappers ([#2291](https://github.com/ghiscoding/slickgrid-universal/issues/2291)) ([fa1a14c](https://github.com/ghiscoding/slickgrid-universal/commit/fa1a14c16c987bfaf7725c46e8114b20ea5a505d)) - by @ghiscoding
+* migrate Row/Hybrid Selection flag into a single `enableSelection` flag ([#2331](https://github.com/ghiscoding/slickgrid-universal/issues/2331)) ([5be5e6a](https://github.com/ghiscoding/slickgrid-universal/commit/5be5e6a862ecd024cf43d404769d65c6c1dd335e)) - by @ghiscoding
+* rename `v-model:data` to `v-model:dataset` ([#2298](https://github.com/ghiscoding/slickgrid-universal/issues/2298)) ([34f42f2](https://github.com/ghiscoding/slickgrid-universal/commit/34f42f2d14c9a1b39a2695c8a885ff2bee53d0b5)) - by @ghiscoding
+* switch to column `hidden` property and always keep all columns ([#2281](https://github.com/ghiscoding/slickgrid-universal/issues/2281)) ([075c649](https://github.com/ghiscoding/slickgrid-universal/commit/075c64961cb7400500df46b792866d39fba2d9e0)) - by @ghiscoding
+* **tooltip:** add global tooltip observation for non-grid elements ([#2371](https://github.com/ghiscoding/slickgrid-universal/issues/2371)) ([1bbc8de](https://github.com/ghiscoding/slickgrid-universal/commit/1bbc8de895e370843286eadd08574efc552ad8fd)) - by @ghiscoding
+
+### Bug Fixes
+
+* **plugin:** SlickCustomTooltip should work with parent+child tooltips ([#2374](https://github.com/ghiscoding/slickgrid-universal/issues/2374)) ([8af7f45](https://github.com/ghiscoding/slickgrid-universal/commit/8af7f45eb19f0a00da2f3de7c729504be7d043eb)) - by @ghiscoding
+* remove all Deprecated code ([#2302](https://github.com/ghiscoding/slickgrid-universal/issues/2302)) ([f42c46c](https://github.com/ghiscoding/slickgrid-universal/commit/f42c46cd1f05b5c72c62f552f124b5bfe776f8b0)) - by @ghiscoding
+
+### Code Refactoring
+
+* drop OperatorType enums and keep only type literal ([#2301](https://github.com/ghiscoding/slickgrid-universal/issues/2301)) ([5dd0807](https://github.com/ghiscoding/slickgrid-universal/commit/5dd08079460dc9af798ab29527997a6d4b31abdd)) - by @ghiscoding
+* replacing multiple TypeScript `enum` with `type` to decrease build size ([#2300](https://github.com/ghiscoding/slickgrid-universal/issues/2300)) ([ea79395](https://github.com/ghiscoding/slickgrid-universal/commit/ea79395cf663b3abce8e43cf27ba6ffea7cfe113)) - by @ghiscoding
+
## [9.13.0](https://github.com/ghiscoding/slickgrid-universal/compare/v9.12.0...v9.13.0) (2026-01-30)
### Features
diff --git a/demos/vue/package.json b/demos/vue/package.json
index 06cc2cfad1..1e618847ed 100644
--- a/demos/vue/package.json
+++ b/demos/vue/package.json
@@ -1,7 +1,7 @@
{
"name": "slickgrid-vue-demo",
"private": true,
- "version": "9.13.0",
+ "version": "10.0.0-beta.0",
"type": "module",
"author": {
"name": "Ghislain B."
@@ -29,6 +29,7 @@
"@slickgrid-universal/row-detail-view-plugin": "workspace:*",
"@slickgrid-universal/rxjs-observable": "workspace:*",
"@slickgrid-universal/text-export": "workspace:*",
+ "@slickgrid-universal/vue-row-detail-plugin": "workspace:*",
"bootstrap": "catalog:",
"dompurify": "catalog:",
"i18next": "catalog:",
@@ -36,7 +37,7 @@
"rxjs": "catalog:",
"slickgrid-vue": "workspace:*",
"vue": "catalog:",
- "vue-router": "^4.6.4"
+ "vue-router": "^5.0.2"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "catalog:",
@@ -46,6 +47,6 @@
"cypress-real-events": "catalog:",
"sass": "catalog:",
"typescript": "catalog:",
- "vite": "catalog:vite7"
+ "vite": "catalog:"
}
}
diff --git a/demos/vue/src/components/Example01.vue b/demos/vue/src/components/Example01.vue
index 7c139a4c0e..dd4af30a61 100644
--- a/demos/vue/src/components/Example01.vue
+++ b/demos/vue/src/components/Example01.vue
@@ -141,7 +141,7 @@ function toggleDarkModeGrid1() {
@@ -155,7 +155,7 @@ function toggleDarkModeGrid1() {
diff --git a/demos/vue/src/components/Example02.vue b/demos/vue/src/components/Example02.vue
index 47dfbb8926..77873845f2 100644
--- a/demos/vue/src/components/Example02.vue
+++ b/demos/vue/src/components/Example02.vue
@@ -275,7 +275,7 @@ function vueGridReady(grid: SlickgridVueInstance) {
diff --git a/demos/vue/src/components/Example03.vue b/demos/vue/src/components/Example03.vue
index 3dae48b85c..1b6472f73c 100644
--- a/demos/vue/src/components/Example03.vue
+++ b/demos/vue/src/components/Example03.vue
@@ -3,7 +3,6 @@ import {
Editors,
Filters,
Formatters,
- OperatorType,
SlickGlobalEditorLock,
SlickgridVue,
SortComparers,
@@ -42,7 +41,7 @@ let vueGrid!: SlickgridVueInstance;
// you can create custom validator to pass to an inline editor
const myCustomTitleValidator: EditorValidator = (value: any) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
- // const grid = args && args.grid;
+ // const grid = args?.grid;
// const gridOptions = grid.getOptions() as GridOption;
// const i18n = gridOptions.i18n;
@@ -204,7 +203,7 @@ function defineGrid() {
collectionFilterBy: {
property: 'value',
value: 0,
- operator: OperatorType.notEqual,
+ operator: '!=',
},
model: Editors.singleSelect,
// validator: (value, args) => {
@@ -419,7 +418,7 @@ function defineGrid() {
separatorBetweenTextLabels: ' ',
},
model: Filters.multipleSelect,
- operator: OperatorType.inContains,
+ operator: 'IN_CONTAINS',
},
},
];
@@ -757,7 +756,7 @@ function vueGridReady(grid: SlickgridVueInstance) {
= 0) {
+ if (args?.current >= 0) {
setTimeout(() => {
metrics.value = {
startTime: new Date(),
endTime: new Date(),
- itemCount: (args && args.current) || 0,
+ itemCount: args?.current || 0,
totalItemCount: dataset.value.length || 0,
};
});
@@ -431,7 +430,7 @@ function vueGridReady(grid: SlickgridVueInstance) {
{
- if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
+ if (columnFilterOperator === 'Custom' && columnDef?.id === 'name') {
let matchesSearch = searchValues[0].replace(/\*/g, '.*');
matchesSearch = matchesSearch.slice(0, 1) + CARET_HTML_ESCAPED + matchesSearch.slice(1);
matchesSearch = matchesSearch.slice(0, -1) + "$'";
@@ -414,7 +413,7 @@ function gridStateChanged(gridStateChanges: GridStateChange) {
function setFiltersDynamically() {
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
vueGrid.filterService.updateFilters([
- // { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
+ // { columnId: 'gender', searchTerms: ['male'], operator: '=' },
{ columnId: 'name', searchTerms: ['A'], operator: 'a*' },
]);
}
@@ -641,7 +640,7 @@ function vueGridReady(grid: SlickgridVueInstance) {
{
- if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
+ if (columnFilterOperator === 'Custom' && columnDef?.id === 'name') {
// technically speaking GraphQL isn't a database query language like SQL, it's an application query language.
// What that means is that GraphQL won't let you write arbitrary queries out of the box.
// It will only support the types of queries defined in your GraphQL schema.
@@ -373,11 +371,11 @@ function setFiltersDynamically() {
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
vueGrid.filterService.updateFilters([
- { columnId: 'gender', searchTerms: ['female'], operator: OperatorType.equal },
- { columnId: 'name', searchTerms: ['Jane'], operator: OperatorType.startsWith },
+ { columnId: 'gender', searchTerms: ['female'], operator: '=' },
+ { columnId: 'name', searchTerms: ['Jane'], operator: 'StartsWith' },
{ columnId: 'company', searchTerms: ['acme'], operator: 'IN' },
- { columnId: 'billingAddressZip', searchTerms: ['11'], operator: OperatorType.greaterThanOrEqual },
- { columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: OperatorType.rangeInclusive },
+ { columnId: 'billingAddressZip', searchTerms: ['11'], operator: '>=' },
+ { columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: 'RangeInclusive' },
]);
}
@@ -396,18 +394,18 @@ function resetToOriginalPresets() {
vueGrid.filterService.updateFilters([
// you can use OperatorType or type them as string, e.g.: operator: 'EQ'
- { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
- // { columnId: 'name', searchTerms: ['John Doe'], operator: OperatorType.contains },
- { columnId: 'name', searchTerms: ['Joh*oe'], operator: OperatorType.startsWithEndsWith },
+ { columnId: 'gender', searchTerms: ['male'], operator: '=' },
+ // { columnId: 'name', searchTerms: ['John Doe'], operator: 'Contains' },
+ { columnId: 'name', searchTerms: ['Joh*oe'], operator: 'StartsWithEndsWith' },
{ columnId: 'company', searchTerms: ['xyz'], operator: 'IN' },
// use a date range with 2 searchTerms values
- { columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: OperatorType.rangeInclusive },
+ { columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: 'RangeInclusive' },
]);
vueGrid.sortService.updateSorting([
// direction can written as 'asc' (uppercase or lowercase) and/or use the SortDirection type
{ columnId: 'name', direction: 'asc' },
- { columnId: 'company', direction: SortDirection.DESC },
+ { columnId: 'company', direction: 'DESC' },
]);
setTimeout(() => {
vueGrid.paginationService?.changeItemPerPage(20);
@@ -607,7 +605,7 @@ function vueGridReady(grid: SlickgridVueInstance) {
@@ -291,7 +291,7 @@ function vueGrid2Ready(grid: SlickgridVueInstance) {
diff --git a/demos/vue/src/components/Example08.vue b/demos/vue/src/components/Example08.vue
index 66024ce7b7..a2211e4ee4 100644
--- a/demos/vue/src/components/Example08.vue
+++ b/demos/vue/src/components/Example08.vue
@@ -251,7 +251,7 @@ function vueGridReady(grid: SlickgridVueInstance) {
diff --git a/demos/vue/src/components/Example09.vue b/demos/vue/src/components/Example09.vue
index 0242967bf4..0e8cf2e987 100644
--- a/demos/vue/src/components/Example09.vue
+++ b/demos/vue/src/components/Example09.vue
@@ -1,7 +1,6 @@
+
+
+
+ Example 51: Menus with Slots
+
+ see
+
+ code
+
+
+
+
+
+
+
+
+
+
+ Menu Slots Demo with Custom Renderer
+
+
+ Click on the menu buttons to see the new single slot functionality working across all menu types (Header Menu, Cell
+ Menu, Context Menu, Grid Menu):
+
+
+ Note: The demo focuses on the custom rendering capability via slotRenderer and
+ defaultMenuItemRenderer, which work across all menu plugins (SlickHeaderMenu, SlickCellMenu, SlickContextMenu,
+ SlickGridMenu). Also note that the keyboard shortcuts displayed in the menus (e.g., Alt+โ, F5) are for
+ demo purposes only and do not actually trigger any actions.
+
+
+
+
+
+
+
+ Clear grouping
+
+
+
+ Collapse all groups
+
+
+
+ Expand all groups
+
+
+ Group by Duration
+
+
+
+
+
+
+
+
diff --git a/demos/vue/src/components/custom-inputFilter.ts b/demos/vue/src/components/custom-inputFilter.ts
index 163ffd434f..1506a7c806 100644
--- a/demos/vue/src/components/custom-inputFilter.ts
+++ b/demos/vue/src/components/custom-inputFilter.ts
@@ -1,13 +1,12 @@
import {
emptyElement,
- OperatorType,
type Column,
type ColumnFilter,
type Filter,
type FilterArguments,
type FilterCallback,
type GridOption,
- type OperatorString,
+ type OperatorType,
type SearchTerm,
type SlickGrid,
} from 'slickgrid-vue';
@@ -20,7 +19,7 @@ export class CustomInputFilter implements Filter {
searchTerms: SearchTerm[] = [];
columnDef!: Column;
callback!: FilterCallback;
- operator: OperatorType | OperatorString = OperatorType.equal;
+ operator: OperatorType = 'EQ';
/** Getter for the Filter Operator */
get columnFilter(): ColumnFilter {
diff --git a/demos/vue/src/components/custom-viewModelFilter.ts b/demos/vue/src/components/custom-viewModelFilter.ts
index e4ad7de754..b0d1b007be 100644
--- a/demos/vue/src/components/custom-viewModelFilter.ts
+++ b/demos/vue/src/components/custom-viewModelFilter.ts
@@ -1,13 +1,12 @@
import {
emptyElement,
- OperatorType,
type Column,
type ColumnFilter,
type Filter,
type FilterArguments,
type FilterCallback,
type GridOption,
- type OperatorString,
+ type OperatorType,
type SearchTerm,
type SlickGrid,
} from 'slickgrid-vue';
@@ -33,7 +32,7 @@ export class CustomVueComponentFilter implements Filter {
searchTerms: SearchTerm[] = [];
columnDef!: Column;
callback!: FilterCallback;
- operator: OperatorType | OperatorString = OperatorType.equal;
+ operator: OperatorType = 'EQ';
selectedItem: any;
/** Get the Collection */
diff --git a/demos/vue/src/components/utilities.ts b/demos/vue/src/components/utilities.ts
index b4c1bff6ef..ae78294c55 100644
--- a/demos/vue/src/components/utilities.ts
+++ b/demos/vue/src/components/utilities.ts
@@ -30,13 +30,6 @@ export function showToast(msg: string, type: 'danger' | 'info' | 'warning', time
}, time);
return;
}
-
- // @deprecated, remove fallback in next major release
- // otherwise, fallback (when popover is not supported): keep the div visible as regular HTML and remove after timeout.
- divContainer.style.left = '50%';
- divContainer.style.top = '20px';
- divContainer.style.transform = 'translateX(-50%)';
- setTimeout(() => divContainer.remove(), time);
}
export function zeroPadding(input: string | number) {
diff --git a/demos/vue/src/router/index.ts b/demos/vue/src/router/index.ts
index 9b49903f2c..c699a3ecd2 100644
--- a/demos/vue/src/router/index.ts
+++ b/demos/vue/src/router/index.ts
@@ -1,5 +1,5 @@
-import type { RouteRecordRaw } from 'vue-router';
-import { createRouter, createWebHashHistory } from 'vue-router';
+import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router';
+
import Example1 from '../components/Example01.vue';
import Example2 from '../components/Example02.vue';
import Example3 from '../components/Example03.vue';
@@ -50,6 +50,7 @@ import Example47 from '../components/Example47.vue';
import Example48 from '../components/Example48.vue';
import Example49 from '../components/Example49.vue';
import Example50 from '../components/Example50.vue';
+import Example51 from '../components/Example51.vue';
import Home from '../Home.vue';
export const routes: RouteRecordRaw[] = [
@@ -105,6 +106,7 @@ export const routes: RouteRecordRaw[] = [
{ path: '/example48', name: '48- Hybrid Selection Model', component: Example48 },
{ path: '/example49', name: '49- Spreadsheet Drag-Fill', component: Example49 },
{ path: '/example50', name: '50- Master/Detail Grids', component: Example50 },
+ { path: '/example51', name: '51- Menus with Slots', component: Example51 },
];
export const router = createRouter({
diff --git a/demos/vue/test/cypress/e2e/example10.cy.ts b/demos/vue/test/cypress/e2e/example10.cy.ts
index 06d6048e1d..22544dc7ab 100644
--- a/demos/vue/test/cypress/e2e/example10.cy.ts
+++ b/demos/vue/test/cypress/e2e/example10.cy.ts
@@ -46,11 +46,8 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should have 2 rows (Task 12,Task 13) selected in 2nd grid after typing in a search filter', () => {
cy.get('#grid2').find('.filter-title').type('Task 1');
-
cy.get('#grid2').find('.slick-row').should('not.have.length', 0);
-
cy.get('[data-test=grid2-selections]').should('contain', '');
-
cy.get('#grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
});
@@ -141,112 +138,72 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
.then((pageNumber) => expect(pageNumber).to.eq('2'));
cy.get('@grid1').find('[data-test=page-count]').contains('99');
-
cy.get('@grid1').find('[data-test=item-from]').contains('6');
-
cy.get('@grid1').find('[data-test=item-to]').contains('10');
-
cy.get('@grid1').find('[data-test=total-items]').contains('495');
// 2nd Grid
cy.get('@grid2').find('[data-test=page-count]').contains('105');
-
cy.get('@grid2').find('[data-test=item-from]').contains('1');
-
cy.get('@grid2').find('[data-test=item-to]').contains('5');
-
cy.get('@grid2').find('[data-test=total-items]').contains('525');
});
it('should change Page Number in Grid1 and expect the Pagination to have correct values', () => {
cy.get('#slickGridContainer-grid1').as('grid1');
-
cy.get('@grid1').find('[data-test=page-number-input]').clear().type('52').type('{enter}');
-
cy.get('@grid1').find('[data-test=page-count]').contains('99');
-
cy.get('@grid1').find('[data-test=item-from]').contains('256');
-
cy.get('@grid1').find('[data-test=item-to]').contains('260');
-
cy.get('@grid1').find('[data-test=total-items]').contains('495');
});
it('should change Page Number and Page Size in Grid2 and expect the Pagination to have correct values', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('[data-test=page-number-input]').clear().type('34').type('{enter}');
-
cy.get('@grid2').find('[data-test=page-count]').contains('105');
-
cy.get('@grid2').find('[data-test=item-from]').contains('166');
-
cy.get('@grid2').find('[data-test=item-to]').contains('170');
-
cy.get('@grid2').find('[data-test=total-items]').contains('525');
-
cy.get('@grid2').find('#items-per-page-label').select('75');
-
cy.get('@grid2').find('[data-test=page-count]').contains('7');
-
cy.get('@grid2').find('[data-test=item-from]').contains('1');
-
cy.get('@grid2').find('[data-test=item-to]').contains('75');
});
it('should go back to Grid1 and expect the same value before changing Pagination of Grid2', () => {
cy.get('#slickGridContainer-grid1').as('grid1');
-
cy.get('@grid1').find('[data-test=page-count]').contains('99');
-
cy.get('@grid1').find('[data-test=item-from]').contains('256');
-
cy.get('@grid1').find('[data-test=item-to]').contains('260');
-
cy.get('@grid1').find('[data-test=total-items]').contains('495');
});
it('should display page 0 of 0 with 0 items when applied filter returning an empty dataset', () => {
cy.get('#slickGridContainer-grid1').as('grid1');
-
cy.get('@grid1').find('.filter-title').type('000');
-
cy.get('.slick-empty-data-warning:visible').contains('No data to display.');
-
cy.get('@grid1').find('[data-test=page-count]').contains('0');
-
cy.get('@grid1').find('[data-test=item-from]').should('not.be.visible');
-
cy.get('@grid1').find('[data-test=item-to]').should('not.be.visible');
-
cy.get('@grid1').find('[data-test=total-items]').contains('0');
});
it('should erase part of the filter to have "00" and expect 4 items in total with 1 page', () => {
cy.get('#slickGridContainer-grid1').as('grid1');
-
cy.get('@grid1').find('.filter-title').type('{backspace}');
-
cy.get('.slick-empty-data-warning').contains('No data to display.').should('not.be.visible');
-
cy.get('@grid1').find('[data-test=page-count]').contains('1');
-
cy.get('@grid1').find('[data-test=item-from]').contains('1');
-
cy.get('@grid1').find('[data-test=item-to]').contains('4');
-
cy.get('@grid1').find('[data-test=total-items]').contains('4');
});
it('should also expect Grid2 to be unchanged (after changing Pagination in Grid1 in previous tests)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('[data-test=page-count]').contains('7');
-
cy.get('@grid2').find('[data-test=item-from]').contains('1');
-
cy.get('@grid2').find('[data-test=item-to]').contains('75');
-
cy.get('@grid2').find('[data-test=total-items]').contains('525');
});
@@ -265,35 +222,24 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should go to Page 3 of 2nd Grid and have 2 rows selected in that Page and also have 4 rows selected in the entire grid (Task 3,Task 12,Task 13,Task 522)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('#items-per-page-label').select('5');
-
cy.get('@grid2').find('[data-test=page-number-input]').clear().type('3').type('{enter}');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
});
it('should go to last Page of 2nd Grid and have 1 rows selected in that Page and also have 4 rows selected in the entire grid (Task 3,Task 12,Task 13,Task 522)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('.icon-seek-end').click();
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 1);
});
it(`should go to first Page of 2nd Grid and select another row (Task 1) in that Page, wich will now be (Task1,Task3) and now have 5 rows selected in the entire grid (Task 1,Task 3,Task 12,Task 13,Task 522)`, () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('.icon-seek-first').click().wait(10);
-
cy.get('@grid2').find('.slick-row:nth(1) .slick-cell:nth(0) input[type=checkbox]').click({ force: true });
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
cy.window().then((win) => {
@@ -313,23 +259,16 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should go back to Page 3 of 2nd Grid and have 2 rows selected in that Page and also retain 5 selected rows in the entire grid (Task 1,Task 3,Task 12,Task 13,Task 522)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('#items-per-page-label').select('5');
-
cy.get('@grid2').find('[data-test=page-number-input]').clear().type('3').type('{enter}');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
});
it('should go to last Page of 2nd Grid and still have 1 row selected in that Page and also retain 5 selected rows in the entire grid (Task 1,Task 3,Task 12,Task 13,Task 522)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('.icon-seek-end').click();
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 1);
});
});
@@ -366,27 +305,18 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should go to Page 61 of Grid1 and expect to find "Task 300" still be selected', () => {
cy.get('#slickGridContainer-grid1').as('grid1');
-
cy.get('@grid1').find('[data-test=page-number-input]').clear().type('61').type('{enter}');
-
cy.get('[data-test=grid1-selections]').contains('Task 300');
-
cy.get('.slick-cell.l0.r0.slick-cell-checkboxsel.selected').should('exist');
-
cy.get('[data-test=grid1-selections]').contains('Task 300');
});
it('should go to a different page for next test to confirm that it will then go to page 1', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('[data-test=page-number-input]').clear().type('22').type('{enter}');
-
cy.get('@grid2').find('[data-test=page-count]').contains('105');
-
cy.get('@grid2').find('[data-test=item-from]').contains('106');
-
cy.get('@grid2').find('[data-test=item-to]').contains('110');
-
cy.get('@grid2').find('[data-test=total-items]').contains('525');
});
@@ -453,35 +383,26 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should have 5 rows (Task 1,Task 3,Task 12,Task 13,Task 522) selected in the entire 2nd grid BUT only 2 shown in the DOM in the top portion of the grid (because SlickGrid uses virtual rendering)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
});
it('should scroll to the bottom of 2nd Grid and still have 5 rows (Task 1,Task 3,Task 12,Task 13,Task 522) selected and find 2 row selected because we now have 2 rows that got rendered (first and last)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522');
-
cy.get('@grid2').find('.slick-viewport-top.slick-viewport-left').scrollTo('bottom').wait(10);
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
});
it('should have 2 rows (Task 3,Task 13) selected in 2nd grid after typing in a search filter (3)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('.filter-title').type('3');
-
cy.get('@grid2').find('.slick-viewport-top.slick-viewport-left').scrollTo('top').wait(10);
-
cy.get('@grid2').find('.slick-row').should('not.have.length', 0);
cy.wait(50);
cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 13');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
cy.window().then((win) => {
@@ -514,9 +435,7 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
describe('Re-enable Pagination', () => {
it('should re-enable the Pagination and expect to see it show it again below the grid at Page 1', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('[data-test=toggle-pagination-grid2]').click();
-
cy.get('#slickGridContainer-grid2 .slick-pagination').should('exist');
cy.get('@grid2')
@@ -525,13 +444,9 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
.then((pageNumber) => expect(pageNumber).to.eq('1'));
cy.get('@grid2').find('[data-test=page-number-input]').click();
-
cy.get('@grid2').find('[data-test=page-count]').contains('105');
-
cy.get('@grid2').find('[data-test=item-from]').contains('1');
-
cy.get('@grid2').find('[data-test=item-to]').contains('5');
-
cy.get('@grid2').find('[data-test=total-items]').contains('525');
cy.window().then((win) => {
@@ -549,13 +464,9 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
it('should have 2 rows (Task 3,Task 13) selected in 2nd grid after typing in a search filter (3)', () => {
cy.get('#slickGridContainer-grid2').as('grid2');
-
cy.get('@grid2').find('.filter-title').type('3');
-
cy.get('@grid2').find('.slick-row').should('not.have.length', 0);
-
cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 13');
-
cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2);
cy.window().then((win) => {
@@ -583,11 +494,8 @@ describe('Example 10 - Multiple Grids with Row Selection', () => {
.then((pageNumber) => expect(pageNumber).to.eq('1'));
cy.get('@grid2').find('[data-test=page-count]').contains('3');
-
cy.get('@grid2').find('[data-test=item-from]').contains('1');
-
cy.get('@grid2').find('[data-test=item-to]').contains('5');
-
cy.get('@grid2').find('[data-test=total-items]').contains('179');
});
});
diff --git a/demos/vue/test/cypress/e2e/example14.cy.ts b/demos/vue/test/cypress/e2e/example14.cy.ts
index 402545254b..91f7db34b6 100644
--- a/demos/vue/test/cypress/e2e/example14.cy.ts
+++ b/demos/vue/test/cypress/e2e/example14.cy.ts
@@ -21,16 +21,16 @@ describe('Example 14 - Column Span & Header Grouping', () => {
});
it('should have a frozen grid on page load with 3 columns on the left and 4 columns on the right', () => {
- cy.get('#grid2').find(`[data-row=0]`).should('have.length', 2);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]`).children().should('have.length', 3);
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0]`).children().should('have.length', 4);
+ cy.get('#grid2').find('[data-row=0]').should('have.length', 2);
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 3);
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0]').children().should('have.length', 4);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]> .slick-cell:nth(0)`).should('contain', '0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]> .slick-cell:nth(1)`).should('contain', 'Task 0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]> .slick-cell:nth(2)`).should('contain', '5 days');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(0)').should('contain', '0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(1)').should('contain', 'Task 0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]> .slick-cell:nth(2)').should('contain', '5 days');
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0]> .slick-cell:nth(0)`).should('contain', '01/01/2009');
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0]> .slick-cell:nth(1)`).should('contain', '01/05/2009');
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0]> .slick-cell:nth(0)').should('contain', '01/01/2009');
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0]> .slick-cell:nth(1)').should('contain', '01/05/2009');
});
it('should have exact Column Pre-Header & Column Header Titles in the grid again', () => {
@@ -48,14 +48,14 @@ describe('Example 14 - Column Span & Header Grouping', () => {
it('should click on the "Remove Frozen Columns" button to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => {
cy.get('[data-test="remove-frozen-column-button"]').click();
- cy.get('#grid2').find(`[data-row=0]`).should('have.length', 1);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]`).children().should('have.length', 7);
+ cy.get('#grid2').find('[data-row=0]').should('have.length', 1);
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 7);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)`).should('contain', '0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)`).should('contain', 'Task 0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)`).should('contain', '5 days');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)`).should('contain', '01/01/2009');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)`).should('contain', '01/05/2009');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)').should('contain', '01/01/2009');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)').should('contain', '01/05/2009');
});
it('should have exact Column Pre-Header & Column Header Titles in the grid once again', () => {
@@ -73,16 +73,16 @@ describe('Example 14 - Column Span & Header Grouping', () => {
it('should click on the "Set 3 Frozen Columns" button to switch frozen columns grid and expect 3 frozen columns on the left and 4 columns on the right', () => {
cy.contains('Set 3 Frozen Columns').click({ force: true });
- cy.get('#grid2').find(`[data-row=0]`).should('have.length', 2);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]`).children().should('have.length', 3);
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0]`).children().should('have.length', 4);
+ cy.get('#grid2').find('[data-row=0]').should('have.length', 2);
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 3);
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0]').children().should('have.length', 4);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)`).should('contain', '0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)`).should('contain', 'Task 0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)`).should('contain', '5 days');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days');
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0] > .slick-cell:nth(0)`).should('contain', '01/01/2009');
- cy.get('#grid2').find(`.grid-canvas-right > [data-row=0] > .slick-cell:nth(1)`).should('contain', '01/05/2009');
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0] > .slick-cell:nth(0)').should('contain', '01/01/2009');
+ cy.get('#grid2').find('.grid-canvas-right > [data-row=0] > .slick-cell:nth(1)').should('contain', '01/05/2009');
});
it('should have still exact Column Pre-Header & Column Header Titles in the grid', () => {
@@ -102,14 +102,14 @@ describe('Example 14 - Column Span & Header Grouping', () => {
cy.contains('Unfreeze Columns/Rows').click({ force: true });
- cy.get('#grid2').find(`[data-row=0]`).should('have.length', 1);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0]`).children().should('have.length', 7);
+ cy.get('#grid2').find('[data-row=0]').should('have.length', 1);
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0]').children().should('have.length', 7);
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)`).should('contain', '0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)`).should('contain', 'Task 0');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)`).should('contain', '5 days');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)`).should('contain', '01/01/2009');
- cy.get('#grid2').find(`.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)`).should('contain', '01/05/2009');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(0)').should('contain', '0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(1)').should('contain', 'Task 0');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(2)').should('contain', '5 days');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(3)').should('contain', '01/01/2009');
+ cy.get('#grid2').find('.grid-canvas-left > [data-row=0] > .slick-cell:nth(4)').should('contain', '01/05/2009');
});
it('should reapply 3 frozen columns on 2nd grid', () => {
@@ -209,7 +209,7 @@ describe('Example 14 - Column Span & Header Grouping', () => {
});
describe('Colspan checks on 1st grid', () => {
- it('should hide Finish column and still expect "5 days" to spread accross 3 column', () => {
+ it('should hide Finish column and expect "5 days" spread to drop from 3 to 2 columns (1x hidden column)', () => {
cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0').should('contain', 'Task 1');
cy.get('#grid1').find('[data-row=2] .slick-cell.l0.r5').should('contain', 'Task 2');
cy.get('#grid1').find('[data-row=1] .slick-cell.l1.r3').should('contain', '5 days');
@@ -232,11 +232,64 @@ describe('Example 14 - Column Span & Header Grouping', () => {
.should('contain', 'Hide Column')
.click();
+ // goto right
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0').click();
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0.active').should('contain', 'Task 1').type('{rightArrow}');
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l1.r3.active').should('contain', '5 days').type('{rightArrow}');
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l4.r4.active').contains(/\d+$/).type('{rightArrow}');
+ cy.get('#grid1')
+ .find('[data-row=1] .slick-cell.l5.r5')
+ .contains(/(true|false)+$/);
+
+ // goto left
+ cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5.active')
+ .contains(/(true|false)+$/)
+ .type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4.active').contains(/\d+$/).type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3.active').should('contain', '5 days').type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0.active').should('contain', 'Task 1');
+ });
+
+ it('should reset Column Picker, click on Spread Hidden Coolumn button then hide Finish column and still expect "5 days" to spread accross 3 column', () => {
+ cy.get('#grid1').find('.slick-header-column:nth(1)').trigger('mouseover').trigger('contextmenu').invoke('show');
+
+ cy.get('.slick-column-picker')
+ .find('.slick-column-picker-list')
+ .children('li:nth-child(4)')
+ .children('label')
+ .should('contain', 'Period - Finish')
+ .click();
+
+ cy.get('.slick-column-picker .close').click();
+
+ cy.get('[data-test="spread-colspan-button"]').click();
cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0').should('contain', 'Task 1');
- cy.get('#grid1').find('[data-row=2] .slick-cell.l0.r4').should('contain', 'Task 2');
+ cy.get('#grid1').find('[data-row=2] .slick-cell.l0.r5').should('contain', 'Task 2');
cy.get('#grid1').find('[data-row=1] .slick-cell.l1.r3').should('contain', '5 days');
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l4.r4').contains(/\d+$/);
+ cy.get('#grid1')
+ .find('[data-row=1] .slick-cell.l5.r5')
+ .contains(/(true|false)+$/);
+
+ cy.get('#grid1')
+ .find('.slick-pane-left .slick-header-columns .slick-header-column[role="columnheader"]:nth(3)')
+ .trigger('mouseover')
+ .children('.slick-header-menu-button')
+ .invoke('show')
+ .click();
+
+ cy.get('.slick-header-menu .slick-menu-command-list')
+ .should('be.visible')
+ .children('.slick-menu-item:nth-of-type(3)')
+ .children('.slick-menu-content')
+ .should('contain', 'Hide Column')
+ .click();
+
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l0.r0').should('contain', 'Task 1');
+ cy.get('#grid1').find('[data-row=2] .slick-cell.l0.r5').should('contain', 'Task 2');
+ cy.get('#grid1').find('[data-row=1] .slick-cell.l1.r4').should('contain', '5 days');
cy.get('#grid1')
- .find('[data-row=1] .slick-cell.l4.r4')
+ .find('[data-row=1] .slick-cell.l5.r5')
.contains(/(true|false)+$/);
});
@@ -292,15 +345,13 @@ describe('Example 14 - Column Span & Header Grouping', () => {
describe('First Grid - Key Navigation', () => {
it('should start at Task 1 and expect "Duration" to have colspan of 3 and show "% Complete" and "Effort Driven"', () => {
cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0').should('contain', 'Task 1').click().type('{rightArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').should('have.class', 'active').should('contain', '5 days').type('{rightArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4').should('have.class', 'active').contains(/\d+$/).type('{rightArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5')
- .should('have.class', 'active')
- .contains(/(true|false)+$/);
+ cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3.active').should('contain', '5 days').type('{rightArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4.active').contains(/\d+$/).type('{rightArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5.active').contains(/(true|false)+$/);
cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5.active').type('{leftArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4').should('have.class', 'active').contains(/\d+$/).type('{leftArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').should('have.class', 'active').should('contain', '5 days');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4.active').contains(/\d+$/).type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3.active').should('contain', '5 days');
cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0').should('contain', 'Task 1');
});
@@ -312,16 +363,17 @@ describe('Example 14 - Column Span & Header Grouping', () => {
.children('label')
.should('contain', 'Period - Finish')
.click();
+ cy.get('.slick-column-picker .close').click();
cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0').should('contain', 'Task 1').click().type('{rightArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').should('have.class', 'active').should('contain', '5 days').type('{rightArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4')
+ cy.get('#grid1 [data-row=1] > .slick-cell.l1.r4.active').should('contain', '5 days').type('{rightArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5')
.should('have.class', 'active')
.contains(/(true|false)+$/);
- cy.get('#grid1 [data-row=1] > .slick-cell.l4.r4.active').type('{leftArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l1.r3').should('have.class', 'active').should('contain', '5 days').type('{leftArrow}');
- cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0').should('have.class', 'active');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l5.r5.active').type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l1.r4.active').should('contain', '5 days').type('{leftArrow}');
+ cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0.active');
cy.get('#grid1 [data-row=1] > .slick-cell.l0.r0').should('contain', 'Task 1');
});
});
diff --git a/demos/vue/test/cypress/e2e/example16.cy.ts b/demos/vue/test/cypress/e2e/example16.cy.ts
index a1a2073bb5..78dabd7ae7 100644
--- a/demos/vue/test/cypress/e2e/example16.cy.ts
+++ b/demos/vue/test/cypress/e2e/example16.cy.ts
@@ -408,7 +408,7 @@ describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => {
});
it('should add Edit/Delete columns and expect 2 new columns added at the beginning of the grid', () => {
- const newExpectedColumns = ['', '', ...fullTitles];
+ const newExpectedColumns = ['', '', '', '', 'Title', '% Complete', 'Start', 'Finish', 'Duration', 'Completed'];
cy.get('[data-test="add-crud-columns-btn"]').click();
cy.get('#grid16')
diff --git a/demos/vue/test/cypress/e2e/example18.cy.ts b/demos/vue/test/cypress/e2e/example18.cy.ts
index 55b380a1a1..b9807b0bc8 100644
--- a/demos/vue/test/cypress/e2e/example18.cy.ts
+++ b/demos/vue/test/cypress/e2e/example18.cy.ts
@@ -122,11 +122,10 @@ describe('Example 18 - Draggable Grouping & Aggregators', () => {
it('should expand all rows with "Expand All" from context menu and expect all the Groups to be expanded and the Toogle All icon to be collapsed', () => {
cy.get('#grid18').find('.slick-row .slick-cell:nth(1)').rightclick({ force: true });
- cy.get('.slick-context-menu .slick-menu-command-list')
- .find('.slick-menu-item')
- .find('.slick-menu-content')
- .contains('Expand all Groups')
- .click();
+ cy.get('.slick-context-menu .slick-menu-command-list').find('.slick-menu-content').contains('Export in CSV format');
+ cy.get('.slick-context-menu .slick-menu-command-list').find('.slick-menu-content').contains('Export to Excel');
+ cy.get('.slick-context-menu .slick-menu-command-list').find('.slick-menu-content').contains('Export to PDF');
+ cy.get('.slick-context-menu .slick-menu-command-list').find('.slick-menu-content').contains('Expand all Groups').click();
cy.get('#grid18').find('.slick-group-toggle.collapsed').should('have.length', 0);
diff --git a/demos/vue/test/cypress/e2e/example32.cy.ts b/demos/vue/test/cypress/e2e/example32.cy.ts
index c2c8d5f769..1c8eb7b69e 100644
--- a/demos/vue/test/cypress/e2e/example32.cy.ts
+++ b/demos/vue/test/cypress/e2e/example32.cy.ts
@@ -181,7 +181,12 @@ describe('Example 32 - Columns Resize by Content', () => {
const yesterdayDate = format(addDay(new Date(), -1), 'YYYY-MM-DD');
const todayDate = format(new Date(), 'YYYY-MM-DD');
- cy.get(`[data-vc-date=${yesterdayDate}]`).should('have.attr', 'data-vc-date-disabled');
+ // Check if yesterday's date element exists (may not be visible when 1st day of the month is a Sunday, e.g. 2026-02-01)
+ cy.get(`[data-vc-date=${yesterdayDate}]`).then(($el) => {
+ if ($el.length > 0) {
+ expect($el).to.have.attr('data-vc-date-disabled');
+ }
+ });
cy.get(`[data-vc-date=${todayDate}]`).should('not.have.attr', 'data-vc-date-disabled');
// make grid readonly again
@@ -379,16 +384,21 @@ describe('Example 32 - Columns Resize by Content', () => {
});
it('should be able to edit "Duration" when "autoEditByKeypress" is enabled and by clicking once on second row and expect next row to become editable', () => {
- cy.get('[data-row="2"] .slick-cell.l2.r2').contains(/[0-9]* days/);
- cy.get('[data-row="2"] .slick-cell.l2.r2').click();
- cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 0);
-
- cy.get('[data-row="2"] .slick-cell.l2.r2').type('123');
- cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 1);
- cy.get('[data-row="2"] .slick-cell.l2.r2').type('{enter}');
- cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 0);
-
- cy.get('[data-row="2"] .slick-cell.l2.r2').should('contain', '123 days');
+ // Check if yesterday's date element exists (may not be visible when we run the test on the 1st day of the month and it is a Sunday, e.g. 2026-02-01)
+ cy.get('[data-row="2"] .slick-cell.l2.r2').then(($el) => {
+ if ($el.length > 0) {
+ cy.wrap($el).contains(/[0-9]* days/);
+ cy.wrap($el).click();
+ cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 0);
+
+ cy.get('[data-row="2"] .slick-cell.l2.r2').type('123');
+ cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 1);
+ cy.get('[data-row="2"] .slick-cell.l2.r2').type('{enter}');
+ cy.get('[data-row="2"] .slick-cell.l2.r2.active.editable').should('have.length', 0);
+
+ cy.get('[data-row="2"] .slick-cell.l2.r2').should('contain', '123 days');
+ }
+ });
});
it('should click on "Auto-Edit by keyboard OFF" button', () => {
diff --git a/demos/vue/test/cypress/e2e/example33.cy.ts b/demos/vue/test/cypress/e2e/example33.cy.ts
index fe0cec43d7..d4b06f03f6 100644
--- a/demos/vue/test/cypress/e2e/example33.cy.ts
+++ b/demos/vue/test/cypress/e2e/example33.cy.ts
@@ -5,6 +5,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
'Duration',
'Description',
'Description 2',
+ 'Button Tooltip',
'Cost',
'% Complete',
'Start',
@@ -13,7 +14,6 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
'Prerequisites',
'Action',
];
- const GRID_ROW_HEIGHT = 33;
it('should display Example title', () => {
cy.visit(`${Cypress.config('baseUrl')}/example33`);
@@ -32,7 +32,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over 1st row checkbox column and NOT expect any tooltip to show since it is disabled on that column', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(0)`).as('checkbox0-cell');
+ cy.get('[data-row="0"] > .slick-cell:nth(0)').as('checkbox0-cell');
cy.get('@checkbox0-cell').trigger('mouseover');
cy.get('.slick-custom-tooltip').should('not.exist');
@@ -40,7 +40,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over Task 2 cell and expect async tooltip to show', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(1)`).as('task1-cell');
+ cy.get('[data-row="0"] > .slick-cell:nth(1)').as('task1-cell');
cy.get('@task1-cell').should('contain', 'Task 2');
cy.get('@task1-cell').trigger('mouseover');
cy.get('.slick-custom-tooltip').contains('loading...');
@@ -64,7 +64,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over Task 6 cell and expect async tooltip to show', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(1)`).as('task6-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(1)').as('task6-cell');
cy.get('@task6-cell').should('contain', 'Task 6');
cy.get('@task6-cell').trigger('mouseover');
cy.get('.slick-custom-tooltip').contains('loading...');
@@ -86,7 +86,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
it('should mouse over Task 6 cell on "Start" column and expect a delayed tooltip opening via async process', () => {
cy.get('.slick-custom-tooltip').should('not.exist');
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(7)`).as('start6-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(8)').as('start6-cell');
cy.get('@start6-cell').contains(/\d{4}-\d{2}-\d{2}$/); // use regexp to make sure it's a number
cy.get('@start6-cell').trigger('mouseover');
@@ -110,7 +110,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over 6th row Description and expect full cell content to show in a tooltip because cell has ellipsis and is too long for the cell itself', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(3)`).as('desc6-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(3)').as('desc6-cell');
cy.get('@desc6-cell').should('contain', 'This is a sample task description.');
cy.get('@desc6-cell').trigger('mouseover');
@@ -125,7 +125,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over 6th row Description 2 and expect regular tooltip title + concatenated full cell content when using "useRegularTooltipFromFormatterOnly: true"', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(4)`).as('desc2-5-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(4)').as('desc2-5-cell');
cy.get('@desc2-5-cell').should('contain', 'This is a sample task description.');
cy.get('@desc2-5-cell').trigger('mouseover');
@@ -138,8 +138,24 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
cy.get('@desc2-5-cell').trigger('mouseout');
});
+ it('should mouse over Button Tooltip column and verify button and icon tooltips show correctly', () => {
+ cy.get('[data-row="2"] > .slick-cell:nth(5)').as('button-cell');
+
+ // Hover over the button element and expect its tooltip
+ cy.get('@button-cell').find('button').trigger('mouseover');
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip').should('contain', 'This is the button tooltip');
+ cy.get('@button-cell').find('button').trigger('mouseout');
+
+ // Hover over the icon inside the button and expect its tooltip
+ cy.get('@button-cell').find('i.mdi').trigger('mouseover');
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip').should('contain', 'icon tooltip');
+ cy.get('@button-cell').find('i.mdi').trigger('mouseout');
+ });
+
it('should mouse over 2nd row Duration and expect a custom tooltip shown with 4 label/value pairs displayed', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(2)`).as('duration2-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(2)').as('duration2-cell');
cy.get('@duration2-cell').contains(/\d+\sday[s]?$/);
cy.get('@duration2-cell').trigger('mouseover');
@@ -162,7 +178,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over % Complete cell of Task 6 and expect regular tooltip to show with content "x %" where x is a number', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(6)`).as('percentage-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(7)').as('percentage-cell');
cy.get('@percentage-cell').find('.percent-complete-bar').should('exist');
cy.get('@percentage-cell').trigger('mouseover');
@@ -173,7 +189,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over Prerequisite cell of Task 6 and expect regular tooltip to show with content "Task 6, Task 5"', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 2}px);"] > .slick-cell:nth(10)`).as('prereq-cell');
+ cy.get('[data-row="2"] > .slick-cell:nth(11)').as('prereq-cell');
cy.get('@prereq-cell').should('contain', 'Task 6, Task 5');
cy.get('@prereq-cell').trigger('mouseover');
@@ -205,7 +221,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over header-row (filter) Finish column and NOT expect any tooltip to show since it is disabled on that column', () => {
- cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(8)`).as('finish-filter');
+ cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(9)`).as('finish-filter');
cy.get('@finish-filter').trigger('mouseover');
cy.get('.slick-custom-tooltip').should('not.exist');
@@ -213,7 +229,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should open PreRequisite dropdown and expect it be lazily loaded', () => {
- cy.get('.slick-headerrow-columns .slick-headerrow-column:nth(10)').as('checkbox10-header');
+ cy.get('.slick-headerrow-columns .slick-headerrow-column:nth(11)').as('checkbox10-header');
cy.get('@checkbox10-header').click();
cy.get('[data-test="alert-lazy"]').should('be.visible');
cy.get('[data-name="filter-prerequisites"] .ms-loading span').contains('Loading...');
@@ -224,7 +240,7 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over header-row (filter) Prerequisite column and expect to see tooltip of selected filter options', () => {
- cy.get('.slick-headerrow-columns .slick-headerrow-column:nth(10)').as('checkbox10-header');
+ cy.get('.slick-headerrow-columns .slick-headerrow-column:nth(11)').as('checkbox10-header');
cy.get('@checkbox10-header').trigger('mouseover');
cy.get('.filter-prerequisites .ms-choice span').contains('15 of 1000 selected');
@@ -258,25 +274,57 @@ describe('Example 33 - Regular & Custom Tooltips', () => {
});
it('should mouse over header title on 2nd column with Finish name and NOT expect any tooltip to show since it is disabled on that column', () => {
- cy.get('.slick-header-columns .slick-header-column:nth(8)').as('finish-header');
+ cy.get('.slick-header-columns .slick-header-column:nth(9)').as('finish-header');
cy.get('@finish-header').trigger('mouseover');
cy.get('.slick-custom-tooltip').should('not.exist');
cy.get('@finish-header').trigger('mouseout');
});
+ it('should mouse over "Filters Empty Description" button and expect global tooltip to show with title text', () => {
+ // Test button tooltip
+ cy.get('[data-test="filter-empty-desc"]').trigger('mouseover');
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip .tooltip-body').should('contain', 'Apply filter to show only empty descriptions');
+ cy.get('[data-test="filter-empty-desc"]').trigger('mouseout');
+
+ // Test icon tooltip
+ cy.get('[data-test="filter-empty-desc"] i.mdi').trigger('mouseover');
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip .tooltip-body').should('contain', 'icon tooltip for empty descriptions');
+ cy.get('[data-test="filter-empty-desc"] i.mdi').trigger('mouseout');
+
+ // Verify tooltip is hidden when hovering on another element
+ cy.get('[data-test="server-delay"]').trigger('mouseover');
+ cy.get('.slick-custom-tooltip').should('not.exist');
+ });
+
+ it('should mouse over "Filters Non-Empty Description" button and expect global tooltip to show with title text', () => {
+ // Test button tooltip
+ cy.get('[data-test="filter-non-empty-desc"]').trigger('mouseover');
+ cy.wait(50);
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip .tooltip-body').should('contain', 'Apply filter to show only non-empty descriptions');
+ cy.get('[data-test="filter-non-empty-desc"]').trigger('mouseout');
+
+ // Test icon tooltip
+ cy.get('[data-test="filter-non-empty-desc"] i.mdi').trigger('mouseover');
+ cy.wait(10);
+ cy.get('.slick-custom-tooltip').should('be.visible');
+ cy.get('.slick-custom-tooltip .tooltip-body').should('contain', 'icon tooltip for non-empty descriptions');
+ cy.get('[data-test="filter-non-empty-desc"] i.mdi').trigger('mouseout');
+ cy.wait(10);
+ cy.get('.slick-custom-tooltip').should('not.exist');
+ });
+
it('should click Prerequisite editor of 1st row (Task 2) and expect Task1 & 2 to be selected in the multiple-select drop', () => {
- cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(10)`).as('prereq-cell');
+ cy.get('[data-row="0"] > .slick-cell:nth(11)').as('prereq-cell');
cy.get('@prereq-cell').should('contain', 'Task 2, Task 1').click();
cy.get('div.ms-drop[data-name=editor-prerequisites]').find('li.selected').should('have.length', 2);
-
cy.get('div.ms-drop[data-name=editor-prerequisites]').find('li.selected:nth(0) span').should('contain', 'Task 1');
-
cy.get('div.ms-drop[data-name=editor-prerequisites]').find('li.selected:nth(1) span').should('contain', 'Task 2');
-
cy.get('div.ms-drop[data-name=editor-prerequisites]').find('.ms-ok-button').click();
-
cy.get('div.ms-drop[data-name=editor-prerequisites]').should('not.exist');
});
});
diff --git a/demos/vue/test/cypress/e2e/example44.cy.ts b/demos/vue/test/cypress/e2e/example44.cy.ts
index 6f70275d7a..f399ed0364 100644
--- a/demos/vue/test/cypress/e2e/example44.cy.ts
+++ b/demos/vue/test/cypress/e2e/example44.cy.ts
@@ -463,4 +463,102 @@ describe('Example 44 - Column & Row Span', { retries: 0 }, () => {
cy.get('[data-row=499] > .slick-cell.l5.r5.active').should('have.length', 1);
});
});
+
+ describe('Hide Columns with colspan/rowspan', () => {
+ it('should hide Title column and expect other colspan/rowspan to simply move over and stay attached to same columns', () => {
+ cy.get('[data-row=499] > .slick-cell.l5.r5.active').type('{ctrl}{home}', { release: false });
+ cy.get('[data-row=2] > .slick-cell.l0.r0.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').trigger('contextmenu').invoke('show');
+
+ cy.get('.slick-column-picker')
+ .find('.slick-column-picker-list')
+ .children('li:nth-child(1)')
+ .children('label')
+ .should('contain', 'Title')
+ .click();
+ cy.get('.slick-column-picker .close').click();
+
+ // Task 2 rowspan should be hidden now
+ cy.get('[data-row=2] > .slick-cell.l0.r0.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.not.eq(GRID_ROW_HEIGHT * 3)
+ );
+ });
+
+ it('should start at "Revenue Growth" second cell down, then type "Arrow Right" key 2x times and expect 4th row "Policy Index" green section to still have a rowspan 3x and colspan of 4x', () => {
+ cy.get('[data-row=3] > .slick-cell.l1.r1.rowspan').as('active_cell').click();
+ cy.get('[data-row=3] > .slick-cell.l1.r1.active').should('have.length', 1);
+ cy.get('@active_cell').type('{rightarrow}{rightarrow}');
+
+ cy.get('[data-row=2] > .slick-cell.l3.r5').should('not.have.class', 'rowspan');
+ cy.get('[data-row=3] > .slick-cell.l3.r7.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+ });
+
+ it('should go up by 1x "Arrow Up" and expect blue section colspan of 3x', () => {
+ cy.get('[data-row=3] > .slick-cell.l3.r7.rowspan').type('{uparrow}');
+ cy.get('[data-row=2] > .slick-cell.l3.r5.active').should('not.have.class', 'rowspan');
+ });
+
+ it('should "Revenue Growth" rowspan should now be at first column and "Policy Index" should now be at third column', () => {
+ cy.get(`[data-row=0] > .slick-cell.l0.r0`).should('not.exist');
+ cy.get(`[data-row=1] > .slick-cell.l0.r0`).should('not.exist');
+ cy.get(`[data-row=2] > .slick-cell.l0.r0`).should('not.exist');
+
+ cy.get('[data-row=0] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+ cy.get('[data-row=3] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5)
+ );
+ cy.get('[data-row=8] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 80)
+ );
+
+ cy.get('[data-row=2] > .slick-cell.l3.r5').should('not.have.class', 'rowspan');
+ cy.get('[data-row=3] > .slick-cell.l3.r7.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+ cy.get('[data-row=8] > .slick-cell.l3.r4.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 492)
+ );
+ });
+
+ it('should show again "Title" column and expect "Revenue Growth" and "Policy Index" columns to be moved to the right by 1 column', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').trigger('contextmenu').invoke('show');
+
+ cy.get('.slick-column-picker')
+ .find('.slick-column-picker-list')
+ .children('li:nth-child(1)')
+ .children('label')
+ .should('contain', 'Title')
+ .click();
+ cy.get('.slick-column-picker .close').click();
+
+ cy.get(`[data-row=0] > .slick-cell.l0.r0`).should('contain', 'Task 0');
+ cy.get(`[data-row=1] > .slick-cell.l0.r0`).should('contain', 'Task 1');
+ cy.get(`[data-row=2] > .slick-cell.l0.r0`).should('contain', 'Task 2');
+
+ cy.get('[data-row=0] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+ cy.get('[data-row=3] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5)
+ );
+ cy.get('[data-row=8] > .slick-cell.l1.r1.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 80)
+ );
+
+ cy.get('[data-row=2] > .slick-cell.l3.r5').should('not.have.class', 'rowspan');
+ cy.get('[data-row=3] > .slick-cell.l3.r7.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+ );
+ cy.get('[data-row=8] > .slick-cell.l3.r4.rowspan').should(($el) =>
+ expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 492)
+ );
+ });
+ });
});
diff --git a/demos/vue/test/cypress/e2e/example48.cy.ts b/demos/vue/test/cypress/e2e/example48.cy.ts
index a9df9df6b5..40d990bb34 100644
--- a/demos/vue/test/cypress/e2e/example48.cy.ts
+++ b/demos/vue/test/cypress/e2e/example48.cy.ts
@@ -160,19 +160,17 @@ describe('Example 48 - Hybrid Selection Model', () => {
});
it('should click on row 4 and 5 row checkbox and expect 5 full rows to be selected', () => {
- cy.get('#grid48-2 .slick-row[data-row="4"] .slick-cell.l0.r0').as('task4');
cy.get('#grid48-2 .slick-row[data-row="4"] .slick-cell.l1.r1').should('contain', '4');
- cy.get('@task4').click();
+ cy.get('#grid48-2 .slick-row[data-row="4"] input[type=checkbox]').click({ force: true });
cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top');
cy.get('#grid48-2 .slick-row[data-row="4"] .slick-cell.l0.r0').should('have.class', 'selected');
cy.get('#grid48-2 .slick-cell.selected').should('have.length', 8 * 1);
// select another row
- cy.get('#grid48-2 .slick-row[data-row="5"] .slick-cell.l0.r0').as('task5');
cy.get('#grid48-2 .slick-row[data-row="5"] .slick-cell.l1.r1').should('contain', '5');
- cy.get('@task5').click();
+ cy.get('#grid48-2 .slick-row[data-row="5"] input[type=checkbox]').click({ force: true });
cy.get('#grid48-2 .slick-viewport-top.slick-viewport-left').scrollTo('top');
- cy.get('@task5').should('have.class', 'selected');
+ cy.get('#grid48-2 .slick-row[data-row="5"] .slick-cell.l0.r0').should('have.class', 'selected');
cy.get('#grid48-2 .slick-cell.selected').should('have.length', 8 * 2);
});
});
diff --git a/demos/vue/test/cypress/e2e/example51.cy.ts b/demos/vue/test/cypress/e2e/example51.cy.ts
new file mode 100644
index 0000000000..80e5148c97
--- /dev/null
+++ b/demos/vue/test/cypress/e2e/example51.cy.ts
@@ -0,0 +1,382 @@
+import { format } from '@formkit/tempo';
+
+describe('Example 51 - Menus with Slots', () => {
+ const fullTitles = ['Title', 'Duration', 'Start', 'Finish', 'Cost', '% Complete', 'Action'];
+
+ it('should display Example title', () => {
+ cy.visit(`${Cypress.config('baseUrl')}/example51`);
+ cy.get('h2').should('contain', 'Example 51: Menus with Slots');
+ });
+
+ it('should have exact column titles in the grid', () => {
+ cy.get('#grid51')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(fullTitles[index]));
+ });
+
+ it('should open Context Menu hover "Duration" column and expect built-in and custom items listed in specific order', () => {
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').rightclick({ force: true });
+
+ // 1st item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.edit-cell-icon').contains('โ');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Edit Cell');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('kbd.edit-cell').contains('F2');
+
+ // icon should rotate while hovering
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseover');
+ cy.wait(175); // wait for rotation
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item .edit-cell-icon')
+ .invoke('css', 'transform') // Get the transform property
+ .then((cssTransform) => {
+ const transformValue = cssTransform as unknown as string; // Cast to string
+ cy.getTransformValue(transformValue, true, 'rotate').then((rotationAngle) => {
+ expect(rotationAngle).to.approximately(13, 15); // 15 degrees rotation
+ });
+ });
+
+ // 2nd item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item').find('i.mdi-content-copy').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item').find('span.menu-item-label').contains('Copy');
+
+ // 3rd item - divider
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(2)').should('have.class', 'slick-menu-item-divider');
+
+ // 4th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Clear all Grouping');
+
+ // 5th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item').find('i.mdi-arrow-collapse').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Collapse all Groups');
+
+ // 6th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('i.mdi-arrow-expand').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Expand all Groups');
+
+ // 7th item - divider
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(6)').should('have.class', 'slick-menu-item-divider');
+
+ // 8th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-download').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Export');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7)').find('.sub-item-chevron').should('exist');
+
+ // 9th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(8)').should('have.class', 'slick-menu-item-divider');
+
+ // 10th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(9) .menu-item')
+ .find('i.mdi-delete.text-danger')
+ .should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(9) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Delete Row');
+ });
+
+ it('should open Export->Excel context sub-menu', () => {
+ const subCommands1 = ['Export as Excel', 'Export as CSV', 'Export as PDF'];
+
+ const stub = cy.stub();
+ cy.on('window:alert', stub);
+
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').should('contain', '0');
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').rightclick({ force: true });
+
+ cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains(/^Export$/)
+ .trigger('mouseover');
+
+ cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list')
+ .should('exist')
+ .find('.slick-menu-item .menu-item')
+ .each(($command, index) => expect($command.text()).to.contain(subCommands1[index]));
+
+ // click different sub-menu
+ cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains('Export as Excel')
+ .should('exist')
+ .click()
+ .then(() => expect(stub.getCall(0)).to.be.calledWith('Export to Excel'));
+
+ cy.get('.slick-submenu').should('have.length', 0);
+ });
+
+ it('should open Header Menu from the "Title" column and expect some commands to have keyboard hints on the right side', () => {
+ cy.get('.slick-header-column:nth(0)').trigger('mouseover', { force: true });
+ cy.get('.slick-header-column:nth(0)').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('i.mdi-arrow-expand-horizontal')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Resize by Content');
+
+ // 2nd item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('i.mdi-sort-ascending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Ascending');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('kbd.key-hint').contains('Alt+โ');
+
+ // 4th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-sort-descending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Descending');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('kbd.key-hint').contains('Alt+โ');
+
+ // 5th item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('i.mdi-filter-remove-outline')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Filter');
+
+ // 7th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('i.mdi-sort-variant-off').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Sort');
+
+ // 8th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Hide Column');
+ });
+
+ it('should open Header Menu from the "Duration" column and expect some commands to have tags on the right side', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover', { force: true });
+ cy.get('.slick-header-column:nth(1)').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('i.mdi-arrow-expand-horizontal')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Resize by Content');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('span.key-hint.danger').contains('NEW');
+
+ // 2nd item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('i.mdi-sort-ascending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Ascending');
+
+ // 4th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-sort-descending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Descending');
+
+ // 5th item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('i.mdi-filter-remove-outline')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Filter');
+
+ // 7th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('i.mdi-sort-variant-off').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Sort');
+
+ // 8th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Hide Column');
+ });
+
+ it('should open Header Menu from the "Cost" column and expect first item to have a dynamic tooltip timestamp when hovering', () => {
+ cy.get('#grid51').find('.slick-header-column:nth(4)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.advanced-export-icon').contains('๐');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').contains('Advanced Export');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('kbd.key-hint').contains('Ctrl+E');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').should(
+ 'have.css',
+ 'background-color',
+ 'rgba(0, 0, 0, 0)'
+ );
+
+ // icon should scale up
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseover');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item .advanced-export-icon')
+ .invoke('css', 'transform')
+ .then((cssTransform) => {
+ const transformValue = cssTransform as unknown as string; // Cast to string
+ cy.getTransformValue(transformValue, true, 'scale').then((scaleValue) => {
+ expect(scaleValue).to.be.approximately(1.1, 1.15); // Check the scale value if applied
+ });
+ });
+
+ const today = format(new Date(), 'YYYY-MM-DD');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').should(
+ 'have.css',
+ 'background-color',
+ 'rgb(133, 70, 133)'
+ );
+ cy.get('.slick-custom-tooltip').contains(`๐ Export timestamp: ${today}`);
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseout');
+ cy.get('.slick-custom-tooltip').should('not.exist');
+ cy.get('body').click();
+ });
+
+ it('should open Action Menu from last column "Action" column and expect custom items listed in specific order', () => {
+ cy.get('[data-row="1"] > .slick-cell:nth(6)').click();
+ cy.get('.slick-command-header.with-title.with-close').contains('Cell Actions');
+
+ // 1st item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.mdi-content-copy').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Copy Cell Value');
+
+ // 2nd item - divider
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('.mdi-download').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item span').contains('Export Row');
+
+ // 4th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-download').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('span.menu-item-label').contains('Export');
+
+ // 5th item - divider
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('.edit-cell-icon').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item .edit-cell-icon').contains('โ');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5)').should('have.css', 'background-color', 'rgba(0, 0, 0, 0)');
+
+ // 7th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('.mdi-delete.text-danger').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item span').contains('Delete Row');
+ });
+
+ it('should open Export->Excel cell sub-menu', () => {
+ const subCommands1 = ['Export as Excel', 'Export as CSV', 'Export as PDF'];
+
+ const stub = cy.stub();
+ cy.on('window:alert', stub);
+
+ cy.get('.slick-cell-menu.slick-menu-level-0 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains(/^Export$/)
+ .trigger('mouseover');
+
+ cy.get('.slick-cell-menu.slick-menu-level-1 .slick-menu-command-list')
+ .should('exist')
+ .find('.slick-menu-item .menu-item')
+ .each(($command, index) => expect($command.text()).to.contain(subCommands1[index]));
+
+ // click different sub-menu
+ cy.get('.slick-cell-menu.slick-menu-level-1 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains('Export as Excel')
+ .should('exist')
+ .click()
+ .then(() => expect(stub.getCall(0)).to.be.calledWith('Export row #1 to Excel'));
+
+ cy.get('.slick-submenu').should('have.length', 0);
+ });
+
+ it('should open Grid Menu and expect built-in commands first then custom items listed in specific order', () => {
+ cy.get('.slick-grid-menu-button.mdi-menu').click();
+
+ // 1st item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('.mdi-filter-remove-outline.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Clear all Filters');
+
+ // 2nd item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item')
+ .find('.mdi-sort-variant-off.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item span').contains('Clear all Sorting');
+
+ // 3rd item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('.mdi-flip-vertical.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item span').contains('Toggle Filter Row');
+
+ // 4th item - divider
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(3)').should('have.class', 'slick-menu-item-divider');
+
+ // 5th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item')
+ .find('.mdi-file-excel-outline.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item span').contains('Export to Excel');
+
+ // 6th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('.mdi-download.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item span').contains('Export to CSV');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('span.key-hint.warn').contains('CUSTOM');
+
+ // 7th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('.mdi-refresh.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item span').contains('Refresh Data');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('kbd.key-hint').contains('F5');
+ });
+
+ it('should sort ascending "Duration" even though the header menu item was override without an action callback', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').should('contain', 'Sort Ascending').click();
+
+ cy.get('[data-row=0]').children('.slick-cell:nth(1)').should('contain', '0');
+ cy.get('[data-row=1]').children('.slick-cell:nth(1)').should('contain', '0');
+ cy.get('[data-row=2]').children('.slick-cell:nth(1)').should('contain', '0');
+ });
+
+ it('should sort descending "Duration" even though the header menu item was override without an action callback', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').should('contain', 'Sort Descending').click();
+
+ cy.get('[data-row=0]').children('.slick-cell:nth(1)').should('contain', '100');
+ cy.get('[data-row=1]').children('.slick-cell:nth(1)').should('contain', '100');
+ cy.get('[data-row=2]').children('.slick-cell:nth(1)').should('contain', '100');
+ });
+});
diff --git a/demos/vue/test/cypress/support/commands.ts b/demos/vue/test/cypress/support/commands.ts
index a35be6228b..3be5a3685b 100644
--- a/demos/vue/test/cypress/support/commands.ts
+++ b/demos/vue/test/cypress/support/commands.ts
@@ -47,6 +47,7 @@ declare global {
): Chainable>;
saveLocalStorage: () => void;
restoreLocalStorage: () => void;
+ getTransformValue(cssTransformMatrix: string, absoluteValue: boolean, transformType?: 'rotate' | 'scale'): Chainable;
}
}
}
@@ -86,3 +87,35 @@ Cypress.Commands.add('restoreLocalStorage', () => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
});
});
+
+Cypress.Commands.add(
+ 'getTransformValue',
+ (
+ cssTransformMatrix: string,
+ absoluteValue: boolean,
+ transformType: 'rotate' | 'scale' = 'rotate' // Default to 'rotate'
+ ): Cypress.Chainable => {
+ if (!cssTransformMatrix || cssTransformMatrix === 'none') {
+ throw new Error('Transform matrix is undefined or none');
+ }
+
+ const cssTransformMatrixIndexes = cssTransformMatrix.split('(')[1].split(')')[0].split(',');
+
+ if (transformType === 'rotate') {
+ const cssTransformScale = Math.sqrt(
+ +cssTransformMatrixIndexes[0] * +cssTransformMatrixIndexes[0] + +cssTransformMatrixIndexes[1] * +cssTransformMatrixIndexes[1]
+ );
+
+ const cssTransformSin = +cssTransformMatrixIndexes[1] / cssTransformScale;
+ const cssTransformAngle = Math.round(Math.asin(cssTransformSin) * (180 / Math.PI));
+
+ return cy.wrap(absoluteValue ? Math.abs(cssTransformAngle) : cssTransformAngle);
+ } else if (transformType === 'scale') {
+ // Assuming scale is based on the first value in the matrix.
+ const scaleValue = +cssTransformMatrixIndexes[0]; // First value typically represents scaling in x direction.
+ return cy.wrap(scaleValue); // Directly return the scale value.
+ }
+
+ throw new Error('Unsupported transform type');
+ }
+);
diff --git a/docs/TOC.md b/docs/TOC.md
index 4bfbdd661f..4322fd7f46 100644
--- a/docs/TOC.md
+++ b/docs/TOC.md
@@ -19,7 +19,6 @@
* [Editors](column-functionalities/editors.md)
* [Autocomplete](column-functionalities/editors/autocomplete-editor.md)
* [Input (text/number)](column-functionalities/editors/input-editor.md)
- * [old Date Picker (flatpickr)](column-functionalities/editors/Date-Editor-(flatpickr).md)
* [new Date Picker (vanilla-calendar)](column-functionalities/editors/date-editor-vanilla-calendar.md)
* [LongText (textarea)](column-functionalities/editors/longtext-editor-textarea.md)
* [Select Dropdown Editor (single/multiple)](column-functionalities/editors/select-dropdown-editor.md)
@@ -47,6 +46,7 @@
* [Resize by Cell Content](grid-functionalities/resize-by-cell-content.md)
* [Column Picker](grid-functionalities/column-picker.md)
* [Composite Editor Modal](grid-functionalities/composite-editor-modal.md)
+* [Custom Menu Slots](menu-slots.md)
* [Custom Tooltip](grid-functionalities/custom-tooltip.md)
* [Column & Row Spanning](grid-functionalities/column-row-spanning.md)
* [Context Menu](grid-functionalities/context-menu.md)
@@ -95,4 +95,5 @@
* [Migration Guide to 4.x (2023-12-15)](migrations/migration-to-4.x.md)
* [Migration Guide to 5.x (2024-05-10)](migrations/migration-to-5.x.md)
* Versions 6 to 8 were skipped...
-* [Migration Guide to 9.x (2025-05-10)](migrations/migration-to-9.x.md)
\ No newline at end of file
+* [Migration Guide to 9.x (2025-05-10)](migrations/migration-to-9.x.md)
+* [Migration Guide to 10.x (TBD)](migrations/migration-to-10.x.md)
diff --git a/docs/backend-services/OData.md b/docs/backend-services/OData.md
index a6d9c094e1..233b473b78 100644
--- a/docs/backend-services/OData.md
+++ b/docs/backend-services/OData.md
@@ -258,13 +258,13 @@ For example if the backend responds with `{ value: [{ id: 1, nav1: { field1: 'x'
Column filters may have a `Custom` operator, that acts as a placeholder for you to define your own logic. To do so, the easiest way is to provide the `filterQueryOverride` callback in the OdataOptions. This method will be called with `BackendServiceFilterQueryOverrideArgs` to let you decide dynamically on how the filter should be assembled.
-E.g. you could listen for a specific column and the active OperatorType.custom in order to switch the filter to a matchesPattern SQL LIKE search:
+E.g. you could listen for a specific column and the active 'Custom' in order to switch the filter to a matchesPattern SQL LIKE search:
```ts
backendServiceApi: {
options: {
filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => {
- if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
+ if (columnFilterOperator === 'Custom' && columnDef?.id === 'name') {
let matchesSearch = searchValues[0].replace(/\*/g, '.*');
matchesSearch = matchesSearch.slice(0, 1) + '%5E' + matchesSearch.slice(1);
matchesSearch = matchesSearch.slice(0, -1) + '$\'';
diff --git a/docs/backend-services/graphql/GraphQL-Filtering.md b/docs/backend-services/graphql/GraphQL-Filtering.md
index 46301cdfe7..afcea6d2f8 100644
--- a/docs/backend-services/graphql/GraphQL-Filtering.md
+++ b/docs/backend-services/graphql/GraphQL-Filtering.md
@@ -136,7 +136,7 @@ this.gridOptions = {
Column filters may have a `Custom` Operator, that acts as a placeholder for you to define your own logic. To do so, the easiest way is to provide the `filterQueryOverride` callback in the GraphQL Options. This method will be called with `BackendServiceFilterQueryOverrideArgs` to let you decide dynamically on how the filter should be assembled.
-E.g. you could listen for a specific column and the active `OperatorType.custom` in order to switch the filter to an SQL LIKE search in GraphQL:
+E.g. you could listen for a specific column and the active `'Custom'` in order to switch the filter to an SQL LIKE search in GraphQL:
> **Note** technically speaking GraphQL isn't a database query language like SQL, it's an application query language. Depending on your configuration, your GraphQL Server might already support regex querying (e.g. Hasura [_regex](https://hasura.io/docs/latest/queries/postgres/filters/text-search-operators/#_regex)) or you could add your own implementation (e.g. see this SO: https://stackoverflow.com/a/37981802/1212166). Just make sure that whatever custom operator that you want to use, is already included in your GraphQL Schema.
@@ -144,7 +144,7 @@ E.g. you could listen for a specific column and the active `OperatorType.custom`
backendServiceApi: {
options: {
filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => {
- if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
+ if (columnFilterOperator === 'Custom' && columnDef?.id === 'name') {
// the `operator` is a string, make sure to implement this new operator in your GraphQL Schema
return { field: fieldName, operator: 'Like', value: searchValues[0] };
}
diff --git a/docs/column-functionalities/cell-menu.md b/docs/column-functionalities/cell-menu.md
index 8dbbb14c5c..533230cb14 100644
--- a/docs/column-functionalities/cell-menu.md
+++ b/docs/column-functionalities/cell-menu.md
@@ -94,7 +94,8 @@ So if you decide to use the `action` callback, then your code would look like th
##### with `action` callback
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
commandItems: [
{ command: 'command1', title: 'Command 1', action: (e, args) => console.log(args) },
@@ -111,7 +112,8 @@ The `onCommand` (or `onOptionSelected`) **must** be defined in the Grid Options
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
commandItems: [
{ command: 'command1', title: 'Command 1' },
@@ -140,6 +142,11 @@ this.gridOptions = {
};
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Override Callback Methods
What if you want to dynamically disable or hide a Command/Option or even disable the entire menu in certain circumstances? For these cases, you would use the override callback methods, the method must return a `boolean`. The list of override available are the following
- `menuUsabilityOverride` returning false would make the Cell Menu unavailable to the user
@@ -150,12 +157,10 @@ What if you want to dynamically disable or hide a Command/Option or even disable
For example, say we want the Cell Menu to only be available on the first 20 rows of the grid, we could use the override this way
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
- menuUsabilityOverride: (args) => {
- const dataContext = args?.dataContext;
- return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20
- },
+ menuUsabilityOverride: (args) => args?.dataContext.id < 21, // say we want to display the menu only from Task 0 to 20
}
}
];
@@ -163,29 +168,34 @@ this.columnDefinitions = [
To give another example, with Options this time, we could say that we enable the `n/a` option only when the row is Completed. So we could do it this way
```ts
-this.columnDefinitions = [{
- id: 'action', field: 'action', name: 'Action',
- cellMenu: {
- optionItems: [{
- option: 0, title: 'n/a', textCssClass: 'italic',
- // only enable this option when the task is Not Completed
- itemUsabilityOverride: (args) => {
- const dataContext = args?.dataContext;
- return !dataContext.completed;
- },
- { option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' },
- { option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' },
- { option: 3, iconCssClass: 'mdi mdi-star red', title: 'High' },
- }]
- }
-}];
+this.columnDefinitions = [
+ {
+ id: 'action', field: 'action', name: 'Action',
+ cellMenu: {
+ optionItems: [
+ {
+ option: 0, title: 'n/a', textCssClass: 'italic',
+ // only enable this option when the task is Not Completed
+ itemUsabilityOverride: (args) => {
+ const dataContext = args?.dataContext;
+ return !dataContext.completed;
+ },
+ },
+ { option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' },
+ { option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' },
+ { option: 3, iconCssClass: 'mdi mdi-star red', title: 'High' },
+ ]
+ }
+ },
+];
```
### How to add Translations?
It works exactly like the rest of the library when `enableTranslate` is set, all we have to do is to provide translations with the `Key` suffix, so for example without translations, we would use `title` and that would become `titleKey` with translations, that;'s easy enough. So for example, a list of Options could be defined as follow:
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
optionTitleKey: 'OPTIONS', // optionally pass a title to show over the Options
optionItems: [
diff --git a/docs/column-functionalities/editors.md b/docs/column-functionalities/editors.md
index 27d9c8db27..21f176002b 100644
--- a/docs/column-functionalities/editors.md
+++ b/docs/column-functionalities/editors.md
@@ -270,7 +270,7 @@ So if we take all of these informations and we want to create our own Custom Edi
const myCustomTitleValidator: EditorValidator = (value: any, args: EditorArgs) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
const grid = args?.grid;
- const gridOptions = (grid && grid.getOptions) ? grid.getOptions() : {};
+ const gridOptions = grid.getOptions() : {};
const i18n = gridOptions.i18n;
if (value == null || value === undefined || !value.length) {
diff --git a/docs/column-functionalities/editors/Date-Editor-(flatpickr).md b/docs/column-functionalities/editors/Date-Editor-(flatpickr).md
deleted file mode 100644
index ca32e9aaac..0000000000
--- a/docs/column-functionalities/editors/Date-Editor-(flatpickr).md
+++ /dev/null
@@ -1,71 +0,0 @@
-##### index
-- [Editor Options](#editor-options)
-- [Custom Validator](#custom-validator)
-- See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...)
-
-### Information
-The Date Editor is provided through an external library named [Flatpickr](https://flatpickr.js.org/examples/) and all options from that library can be added to your `editorOptions` (see below), so in order to add things like minimum date, disabling dates, ... just review all the [Flatpickr Examples](https://flatpickr.js.org/examples/) and then add them into `editorOptions`. Also just so you know, `editorOptions` is use by all other editors as well to expose external library like Flatpickr, Multiple-Select.js, etc...
-
-### Demo
-[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example12) | [Demo Component](https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vanilla/src/examples/example12.ts)
-
-### Editor Options
-You can use any of the Flatpickr [options](https://flatpickr.js.org/options/) by adding them to `editorOptions` as shown below.
-
-#### [FlatpickrOption](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/flatpickrOption.interface.ts) Interface.
-
-```ts
-initializeGrid() {
- this.columnDefinitions = [
- {
- id: 'title', name: 'Title', field: 'title',
- editor: {
- model: Editors.date,
- editorOptions: {
- minDate: 'today',
- disable: [(date: Date) => this.isWeekendDay(date)], // disable weekend days (Sat, Sunday)
- } as FlatpickrOption,
- },
- },
- ];
-}
-
-/** Returns true when it's a weekend day (Saturday, Sunday) */
-isWeekendDay(date: Date): boolean {
- return (date.getDay() === 0 || date.getDay() === 6);
-}
-```
-
-#### Grid Option `defaultEditorOptions
-You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultEditorOptions` Grid Option. Note that they are set via the editor type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `editorOptions` (also note that each key is already typed with the correct editor option interface), for example
-
-```ts
-this.gridOptions = {
- defaultEditorOptions: {
- date: { displayDateMin: 'today' }, // typed as FlatpickrOption
- }
-}
-```
-
-### Custom Validator
-You can add a Custom Validator from an external function or inline (inline is shown below and comes from [Example 12](https://ghiscoding.github.io/slickgrid-universal/#/example12))
-```ts
-initializeGrid() {
- this.columnDefinitions = [
- {
- id: 'title', name: 'Title', field: 'title',
- editor: {
- model: Editors.date,
- required: true,
- validator: (value, args) => {
- const dataContext = args?.item;
- if (dataContext && (dataContext.completed && !value)) {
- return { valid: false, msg: 'You must provide a "Finish" date when "Completed" is checked.' };
- }
- return { valid: true, msg: '' };
- }
- },
- },
- ];
-}
-```
\ No newline at end of file
diff --git a/docs/column-functionalities/editors/select-dropdown-editor.md b/docs/column-functionalities/editors/select-dropdown-editor.md
index defd751b7a..eef11e25c5 100644
--- a/docs/column-functionalities/editors/select-dropdown-editor.md
+++ b/docs/column-functionalities/editors/select-dropdown-editor.md
@@ -6,11 +6,11 @@
- [Collection Label Prefix/Suffix](#collection-label-prefixsuffix)
- [Collection Label Render HTML](#collection-label-render-html)
- [Collection Change Watch](#collection-watch)
- - [`multiple-select.js` Options](#multiple-selectjs-options)
+ - [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- See the [Editors - Docs](../Editors.md) for more general info about Editors (validators, event handlers, ...)
## Select Editors
-The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select](https://github.com/ghiscoding/multiple-select-adapted/blob/master/src/multiple-select.js) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
+The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
@@ -199,8 +199,8 @@ this.columnDefinitions = [
];
```
-### `multiple-select.js` Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### `multiple-select-vanilla` Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/lib` folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -226,7 +226,7 @@ this.columnDefinitions = [
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Editors.singleSelect,
elementOptions: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
}
diff --git a/docs/column-functionalities/filters/compound-filters.md b/docs/column-functionalities/filters/compound-filters.md
index 5ff9558727..0cdfd996d0 100644
--- a/docs/column-functionalities/filters/compound-filters.md
+++ b/docs/column-functionalities/filters/compound-filters.md
@@ -105,7 +105,7 @@ this.gridOptions = {
```
#### Date and Time
-The date picker will automatically detect if the `type` or `outputType` has time inside, if it does then it will add a time picker at the bottom of the date picker and also note that the `'='` will also filter the value including that time (and it also includes seconds even if it isn't displayed in the picker). But what if you would like to show a date+time in the grid but filter with only a date? In that case you can show your date with a formatter that includes the time (e.g. `Formatters.dateUsAmPm`) and then make sure to add the output type that the picker will use in the UI but also in its filtering comparison (e.g. `outputType: 'dateUs'`)
+The date picker will automatically detect if the `type` or `outputType` has time inside, if it does then it will add a time picker at the bottom of the date picker and also note that the `'='` will also filter the value including that time (and it also includes seconds even if it isn't displayed in the picker). But what if you would like to show a date+time in the grid but filter with only a date? In that case you can show your date with a formatter that includes the time (e.g. `Formatters.dateUsAmPm`) and then make sure to add the output type that the picker will use in the UI but also in its filtering comparison (e.g. `outputType: 'dateUs'`)
For example, if we have an input date in UTC format and we want to display a Date ISO format with time to the screen (UI) and the date picker.
@@ -156,7 +156,7 @@ this.gridOptions = {
```
### Compound Operator List (custom list)
-Each Compound Filter will try to define the best possible Operator List depending on what Field Type you may have (for example we can have StartsWith Operator on a string but not on a number). If you want to provide your own custom Operator List to a Compound Filter, you can do that via the `compoundOperatorList` property (also note that your Operator must be a valid OperatorType/OperatorString).
+Each Compound Filter will try to define the best possible Operator List depending on what Field Type you may have (for example we can have StartsWith Operator on a string but not on a number). If you want to provide your own custom Operator List to a Compound Filter, you can do that via the `compoundOperatorList` property (also note that your Operator must be a valid OperatorType).
```ts
this.columnDefinitions = [
@@ -198,7 +198,7 @@ The texts are separated into 2 groups (`numeric` or `text`) so that the alternat
```ts
this.gridOptions = {
compoundOperatorAltTexts: {
- // where '=' is any of the `OperatorString` type shown above
+ // where '=' is any of the `OperatorType` type shown above
numeric: { '=': { operatorAlt: 'eq', descAlt: 'alternate numeric equal description' } },
text: { '=': { operatorAlt: 'eq', descAlt: 'alternate text equal description' } }
},
diff --git a/docs/column-functionalities/filters/range-filters.md b/docs/column-functionalities/filters/range-filters.md
index 804e1933f5..e6e6ad2b41 100644
--- a/docs/column-functionalities/filters/range-filters.md
+++ b/docs/column-functionalities/filters/range-filters.md
@@ -23,7 +23,7 @@ this.columnDefinitions = [
filterable: true,
filter: {
model: Filters.input,
- operator: OperatorType.rangeInclusive // defaults to exclusive
+ operator: 'RangeInclusive' // defaults to exclusive
// or use the string (case sensitive)
operator: 'RangeInclusive', // defaults to exclusive
@@ -37,7 +37,7 @@ You can use a regular input filter with the 2 dots (..) notation to represent a
##### Component
```ts
-import { Filters, Formatters, GridOption, OperatorType } from '@slickgrid-universal/common';
+import { Filters, Formatters, GridOption } from '@slickgrid-universal/common';
export class GridBasicComponent {
columnDefinitions: Column[];
@@ -55,7 +55,7 @@ export class GridBasicComponent {
// input filter is the default, so you can skip this unless you want to specify the `operator`
filter: {
model: 'input',
- operator: OperatorType.rangeInclusive // defaults to exclusive
+ operator: 'RangeInclusive' // defaults to exclusive
}
},
];
@@ -72,7 +72,7 @@ The slider range filter is very useful if you can just want to use the mouse to
##### Component
```ts
-import { Filters, Formatters, GridOption, SliderRangeOption, OperatorType } from '@slickgrid-universal/commomn';
+import { Filters, Formatters, GridOption, SliderRangeOption } from '@slickgrid-universal/commomn';
export class GridBasicComponent {
columnDefinitions: Column[];
@@ -91,7 +91,7 @@ export class GridBasicComponent {
filter: {
model: Filters.sliderRange,
maxValue: 100, // or you can use the options as well
- operator: OperatorType.rangeInclusive, // optional, defaults to exclusive
+ operator: 'RangeInclusive', // optional, defaults to exclusive
params: { hideSliderNumbers: false }, // you can hide/show the slider numbers on both side
// you can also optionally pass any option of the Slider filter
@@ -140,7 +140,7 @@ The date range filter allows you to search data between 2 dates, it uses the [Va
> **Note** we use [Tempo](https://tempo.formkit.com/) to parse and format Dates to the chosen format via the `type` option when provided in your column definition.
##### Component
-import { Filters, Formatters, GridOption, OperatorType, VanillaCalendarOption } from '@slickgrid-universal/common';
+import { Filters, Formatters, GridOption, VanillaCalendarOption } from '@slickgrid-universal/common';
```typescript
export class GridBasicComponent {
diff --git a/docs/column-functionalities/filters/select-filter.md b/docs/column-functionalities/filters/select-filter.md
index 7942013524..f0cecfd56a 100644
--- a/docs/column-functionalities/filters/select-filter.md
+++ b/docs/column-functionalities/filters/select-filter.md
@@ -16,7 +16,7 @@
- [Collection Async Load](#collection-async-load)
- [Collection Lazy Load](#collection-lazy-load)
- [Collection Watch](#collection-watch)
-- [`multiple-select.js` Options](#multiple-selectjs-options)
+- [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- [Filter Options (`MultipleSelectOption` interface)](#filter-options-multipleselectoption-interface)
- [Display shorter selected label text](#display-shorter-selected-label-text)
- [Query against a different field](#query-against-another-field-property)
@@ -33,7 +33,7 @@ Multiple Select (dropdown) filter is useful when we want to filter the grid 1 or
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
#### Note
-For this filter to work you will need to add [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) to your project. This is a customized version of the original (thought all the original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
+For this filter to work you will need to add [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) to your project. This is a customized version of the original (thought all the original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
- `okButtonText` was also added for locale (i18n)
- `offsetLeft` option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen.
@@ -207,7 +207,7 @@ Note: the defaults for single & multiple select filters are different
```ts
// define you columns, in this demo Effort Driven will use a Select Filter
this.columnDefinitions = [
- {
+ {
id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven',
formatter: Formatters.checkmark,
type: 'boolean',
@@ -246,7 +246,7 @@ this.columnDefinitions = [
],
collectionFilterBy: {
property: 'effortDriven',
- operator: OperatorType.notEqual,
+ operator: '!=',
value: undefined
},
collectionSortBy: {
@@ -278,11 +278,11 @@ this.columnDefinitions = [
collection: multiSelectFilterArray,
collectionFilterBy: [{
property: 'value',
- operator: OperatorType.notEqual, // remove day 1
+ operator: '!=', // remove day 1
value: 1
}, {
property: 'value',
- operator: OperatorType.notEqual, // remove day 365
+ operator: '!=', // remove day 365
value: 365
}],
model: Filters.multipleSelect
@@ -358,7 +358,7 @@ this.columnDefinitions = [
],
collectionFilterBy: {
property: 'effortDriven',
- operator: OperatorType.equal, // defaults to equal when not provided
+ operator: '=', // defaults to equal when not provided
value: undefined
},
collectionSortBy: {
@@ -608,8 +608,8 @@ this.gridOptions = {
}
```
-### Multiple-select.js Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### multiple-select-vanilla Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why we are using a fork [ghiscoding/multiple-select-modified](https://github.com/ghiscoding/multiple-select-modified) folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -641,7 +641,7 @@ prepareGrid() {
model: Filters.singleSelect,
// previously known as `filterOptions` for < 9.0
options: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
} as MultipleSelectOption
diff --git a/docs/column-functionalities/filters/single-search-filter.md b/docs/column-functionalities/filters/single-search-filter.md
index 2f66733c84..16111a42f3 100644
--- a/docs/column-functionalities/filters/single-search-filter.md
+++ b/docs/column-functionalities/filters/single-search-filter.md
@@ -55,7 +55,7 @@ export class MyExample {
columnDefinitions: Column[];
gridOptions: GridOption;
dataset: any[];
- operatorList: OperatorString[] = ['=', '<', '<=', '>', '>=', '<>'];
+ operatorList: OperatorType[] = ['=', '<', '<=', '>', '>=', '<>'];
//
// -- if any of the Search form input changes, we'll call the updateFilter() method
@@ -78,7 +78,7 @@ export class MyExample {
const filter = {};
const filterArg: FilterCallbackArg = {
columnDef: this.selectedColumn,
- operator: this.selectedOperator as OperatorString, // or fix one yourself like '='
+ operator: this.selectedOperator as OperatorType, // or fix one yourself like '='
searchTerms: [this.searchValue || '']
};
diff --git a/docs/column-functionalities/filters/slider-filter.md b/docs/column-functionalities/filters/slider-filter.md
index a023a87525..98a9304cd2 100644
--- a/docs/column-functionalities/filters/slider-filter.md
+++ b/docs/column-functionalities/filters/slider-filter.md
@@ -112,7 +112,7 @@ Example with range slider:
model: Filters.sliderRange,
minValue: 0, // minimum value on the slider
maxValue: 100, // maximum value on the slider
- operator: OperatorType.rangeInclusive, // defaults to inclusive
+ operator: 'RangeInclusive', // defaults to inclusive
options: {
hideSliderNumbers: false, // show/hide the numbers on both sides
sliderStartValue: 0, // left handle starting position
diff --git a/docs/getting-started/installation-salesforce.md b/docs/getting-started/installation-salesforce.md
index a921177cbe..8eaab368d5 100644
--- a/docs/getting-started/installation-salesforce.md
+++ b/docs/getting-started/installation-salesforce.md
@@ -20,7 +20,7 @@ Click on the `zip` link and then the `Download` button on the top right to downl
### Step 2. load Slickgrid
-> Please note that the project has TypeScript enums which Salesforce doesn't support by default, so to make them work, we moved them into a separate namespace under `Slicker.Enums` (or simply `Enums` if you follow 2nd approach below). Also please note that field `type` can be simplified by defining them as string, for example `type: 'number'` (see [`FieldType`](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/enums/fieldType.enum.ts) enum for all available types)
+> Please note that the project has TypeScript enums which Salesforce doesn't support by default, so to make them work, we moved them into a separate namespace under `Slicker.Enums` (or simply `Enums` if you follow 2nd approach below). Also please note that field `type` can be simplified by defining them as string, for example `type: 'number'` (see [`FieldType`](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/enums/field.type.ts) enum for all available types)
#### 2.1 First Approach
Create all the Static Resources that are required by Slickgrid-Universal as shown below (they could have different names in your org).
diff --git a/docs/grid-functionalities/composite-editor-modal.md b/docs/grid-functionalities/composite-editor-modal.md
index 3a508ef41d..dcf33b1f8b 100644
--- a/docs/grid-functionalities/composite-editor-modal.md
+++ b/docs/grid-functionalities/composite-editor-modal.md
@@ -365,7 +365,7 @@ When adding a backend API to the `onSave` you can (and should) wrap your code in
## How to Skip a Mass Change
### Mass Change (Mass-Update / Mass-Selection) - Skipping according to certain condition(s)
-The use case would be to skip a change, in silent without any errors shown, if another column or property has value(s) that do not match our condition expectaation. A possible use case could be found under [Example 12](https://github.com/ghiscoding/slickgrid-universal/blob/eb1d5069e10b8b2cb2f14ac964f2c6e2b8f006a9/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts#L949-L956), the use case that we could do is the following: "Do not apply a mass change on the 'Duration' column that is below 5 days if its 'Complexity' column is set to 'Complex' or 'Very Complex'", the code do this use case is shown below. Also note that the 3rd argument of `onSave` (in our case `dataContextOrUpdatedDatasetPreview`) will have the updated dataset but without the change(s) that got skipped
+The use case would be to skip a change, in silent without any errors shown, if another column or property has value(s) that do not match our condition expectaation. A possible use case could be found under [Example 12](https://github.com/ghiscoding/slickgrid-universal/blob/44d9a5230b9cb57f5fecbf6e7b12b8ef9f3ab69d/demos/vanilla/src/examples/example12.ts#L1093-L1101), the use case that we could do is the following: "Do not apply a mass change on the 'Duration' column that is below 5 days if its 'Complexity' column is set to 'Complex' or 'Very Complex'", the code do this use case is shown below. Also note that the 3rd argument of `onSave` (in our case `dataContextOrUpdatedDatasetPreview`) will have the updated dataset but without the change(s) that got skipped
```ts
this.compositeEditorInstance.openDetails({
@@ -535,7 +535,7 @@ export class GridExample {
// you can also change some editor options
// not all Editors supports this functionality, so far only these Editors are supported: AutoComplete, Date, Single/Multiple Select
if (columnDef.id === 'completed') {
- this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select.js, show filter in dropdown
+ this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select-vanilla, show filter in dropdown
this.compositeEditorInstance.changeFormEditorOption('product', 'minLength', 3); // autocomplete, change minLength char to type
this.compositeEditorInstance.changeFormEditorOption('finish', 'displayDateMin', 'today'); // vanilla calendar date picker, change minDate to today
}
diff --git a/docs/grid-functionalities/context-menu.md b/docs/grid-functionalities/context-menu.md
index 4b9ecd1b42..fc797f97de 100644
--- a/docs/grid-functionalities/context-menu.md
+++ b/docs/grid-functionalities/context-menu.md
@@ -21,7 +21,7 @@ This extensions is wrapped around the new SlickGrid Plugin **SlickContextMenu**
### Default Usage
Technically, the Context Menu is enabled by default (copy, export) and so you don't have anything to do to enjoy it (you could disable it at any time). However, if you want to customize the content of the Context Menu, then continue reading. You can customize the menu with 2 different lists, Commands and/or Options, they can be used separately or at the same time. Also note that even though the code shown below makes a separation between the Commands and Options, you can mix them in the same Context Menu.
-#### with Commands
+#### with Commands (Static)
```ts
this.gridOptions = {
enableFiltering: true,
@@ -60,6 +60,98 @@ this.gridOptions = {
};
```
+#### with Commands (Dynamic Builder)
+For advanced scenarios where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list.
+
+```ts
+this.gridOptions = {
+ enableContextMenu: true,
+ contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // Example: Add custom commands after built-in ones
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'delete-row',
+ title: 'Delete Row',
+ iconCssClass: 'mdi mdi-delete text-danger',
+ action: (e, args) => {
+ if (confirm(`Delete row ${args.dataContext.id}?`)) {
+ this.gridService.deleteItem(args.dataContext);
+ }
+ }
+ },
+ {
+ command: 'duplicate-row',
+ title: 'Duplicate Row',
+ iconCssClass: 'mdi mdi-content-duplicate',
+ action: (e, args) => {
+ const newItem = { ...args.dataContext, id: this.generateNewId() };
+ this.gridService.addItem(newItem);
+ }
+ }
+ ];
+ },
+ onCommand: (e, args) => {
+ // Handle commands here if not using action callbacks
+ console.log('Command:', args.command);
+ }
+ }
+};
+```
+
+**Example: Filter commands based on row data**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // You can't access row data here, but you can filter/modify built-in items
+ // Use itemUsabilityOverride or itemVisibilityOverride for row-specific logic
+
+ // Only show export commands
+ return builtInItems.filter(item =>
+ item === 'divider' || item.command?.includes('export')
+ );
+ }
+}
+```
+
+**Example: Sort and reorganize commands**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ const customFirst = [
+ {
+ command: 'edit',
+ title: 'Edit Row',
+ iconCssClass: 'mdi mdi-pencil',
+ positionOrder: 0
+ }
+ ];
+
+ // Sort built-in commands by positionOrder
+ const sorted = [...builtInItems].sort((a, b) => {
+ if (a === 'divider' || b === 'divider') return 0;
+ return (a.positionOrder || 50) - (b.positionOrder || 50);
+ });
+
+ return [...customFirst, 'divider', ...sorted];
+ }
+}
+```
+
+**When to use `commandListBuilder` vs `commandItems`:**
+- Use `commandItems` for static command lists
+- Use `commandListBuilder` when you need to:
+ - Append/prepend to built-in commands
+ - Filter or modify commands dynamically
+ - Sort or reorder the final command list
+ - Have full control over what gets rendered
+
+**Note:** Typically use `commandListBuilder` **instead of** `commandItems`, not both together.
+
+See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples.
+
#### with Options
That is when you want to define a list of Options (only 1 list) that the user can choose from and once is selected we would do something (for example change the value of a cell in the grid).
```ts
@@ -122,6 +214,11 @@ contextMenu: {
}
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Override Callback Methods
What if you want to dynamically disable or hide a Command/Option or even disable the entire menu in certain circumstances? For these cases, you would use the override callback methods, the method must return a `boolean`. The list of override available are the following
- `menuUsabilityOverride` returning false would make the Context Menu unavailable to the user
@@ -141,17 +238,19 @@ contextMenu: {
To give another example, with Options this time, we could say that we enable the `n/a` option only when the row is Completed. So we could do it this way
```ts
contextMenu: {
- optionItems: [{
- option: 0, title: 'n/a', textCssClass: 'italic',
- // only enable this option when the task is Not Completed
- itemUsabilityOverride: (args) => {
- const dataContext = args?.dataContext;
- return !dataContext.completed;
+ optionItems: [
+ {
+ option: 0, title: 'n/a', textCssClass: 'italic',
+ // only enable this option when the task is Not Completed
+ itemUsabilityOverride: (args) => {
+ const dataContext = args?.dataContext;
+ return !dataContext.completed;
+ },
},
{ option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' },
{ option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' },
{ option: 3, iconCssClass: 'mdi mdi-star red', title: 'High' },
- }]
+ ]
}
```
@@ -160,11 +259,11 @@ It works exactly like the rest of the library when `enableTranslate` is set, all
```ts
contextMenu: {
optionTitleKey: 'OPTIONS', // optionally pass a title to show over the Options
- optionItems: [{
+ optionItems: [
{ option: 1, titleKey: 'LOW', iconCssClass: 'mdi mdi-star-outline yellow' },
{ option: 2, titleKey: 'MEDIUM', iconCssClass: 'mdi mdi-star orange' },
{ option: 3, titleKey: 'HIGH', iconCssClass: 'mdi mdi-star red' },
- }]
+ ]
}
```
@@ -184,25 +283,25 @@ Another set of possible Commands would be related to Grouping, so if you are usi
All of these internal commands, you can choose to hide them and/or change their icons, the default global options are the following and you can change any of them.
```ts
contextMenu: {
- autoAdjustDrop: true,
- autoAlignSide: true,
- hideCloseButton: true,
- hideClearAllGrouping: false,
- hideCollapseAllGroups: false,
- hideCommandSection: false,
- hideCopyCellValueCommand: false,
- hideExpandAllGroups: false,
- hideExportCsvCommand: false,
- hideExportExcelCommand: false,
- hideExportTextDelimitedCommand: true,
- hideMenuOnScroll: true,
- hideOptionSection: false,
- iconCopyCellValueCommand: 'mdi mdi-content-copy',
- iconExportCsvCommand: 'mdi mdi-download',
- iconExportExcelCommand: 'mdi mdi-file-excel-outline text-success',
- iconExportTextDelimitedCommand: 'mdi mdi-download',
- width: 200,
- },
+ autoAdjustDrop: true,
+ autoAlignSide: true,
+ hideCloseButton: true,
+ hideClearAllGrouping: false,
+ hideCollapseAllGroups: false,
+ hideCommandSection: false,
+ hideCopyCellValueCommand: false,
+ hideExpandAllGroups: false,
+ hideExportCsvCommand: false,
+ hideExportExcelCommand: false,
+ hideExportTextDelimitedCommand: true,
+ hideMenuOnScroll: true,
+ hideOptionSection: false,
+ iconCopyCellValueCommand: 'mdi mdi-content-copy',
+ iconExportCsvCommand: 'mdi mdi-download',
+ iconExportExcelCommand: 'mdi mdi-file-excel-outline text-success',
+ iconExportTextDelimitedCommand: 'mdi mdi-download',
+ width: 200,
+},
```
### How to Disable the Context Menu?
diff --git a/docs/grid-functionalities/custom-tooltip.md b/docs/grid-functionalities/custom-tooltip.md
index f21ab42a70..dab0e61f95 100644
--- a/docs/grid-functionalities/custom-tooltip.md
+++ b/docs/grid-functionalities/custom-tooltip.md
@@ -8,6 +8,7 @@
- [on Column Header (title)](#column-header-custom-tooltip-with-headerformatter) with `headerFormatter`
- [on Column Header row (filter)](#column-header-custom-tooltip-with-headerrowformatter) with `headerRowFormatter`
- [with regular `[title]` attribute](#regular-tooltip-with-a-title-attribute)
+ - [Nested/Inner Tooltips](#nestedinner-tooltips) (parent and child element tooltips)
- [tooltip text length](#regular-tooltip-max-length)
- [How to delay the opening of a tooltip?](#how-to-delay-the-opening-of-a-tooltip)
- [delay a tooltip with Formatter](#delay-a-tooltip-with-formatter)
@@ -179,6 +180,21 @@ customTooltip: {
},
```
+### Nested/Inner Tooltips
+You can have tooltips on both parent and child elements (for example, a button with an icon inside where both have different tooltips). When hovering:
+- **Parent element** (button): shows the parent tooltip
+- **Child element** (icon): shows the child tooltip
+
+Both tooltips can be styled independently using the CSS `data-target-id` attribute. This is useful when you want different tooltip styling for nested elements.
+
+#### HTML Example
+```html
+
+
+ Action
+
+```
+
#### Regular tooltip max length
By default the custom tooltip text will be limited, and potentially truncated, to 650 characters in order to keep the tooltip with a size that is not too large. You could change the grid option setting with this
diff --git a/docs/grid-functionalities/export-to-excel.md b/docs/grid-functionalities/export-to-excel.md
index 8086d24643..45310e3987 100644
--- a/docs/grid-functionalities/export-to-excel.md
+++ b/docs/grid-functionalities/export-to-excel.md
@@ -1,6 +1,7 @@
#### index
- [Grid Options](#grid-options)
- [Column Definition & Options](#column-definition-and-options)
+- [Custom Column Width](#custom-column-width)
- [Custom Cell Styling](#custom-cell-styling)
- [Cell Value Parser](#cell-value-parser)
- [Cell Format Auto-Detect Disable](#cell-format-auto-detect-disable)
@@ -130,6 +131,10 @@ this.gridOptions = {
};
```
+### Custom Column Width
+
+See [Custom Cell Styling](#custom-cell-styling) to define cell width.
+
### Styling the Header Titles
By default the header titles (first row) will be styled as Bold text, however you can choose to style them differently with custom styles as shown below. To find out what styling you can use, you can take a look at Excel Builder-Vanilla [Documentation](https://ghiscoding.gitbook.io/excel-builder-vanilla/cookbook/fonts-and-colors) website. The code shown below is used in [Aurelia-Slickgrid - Example 24](https://ghiscoding.github.io/aurelia-slickgrid-demos/#/slickgrid/example24) if you wish to see the result.
@@ -317,7 +322,6 @@ Below is a preview of the previous customizations shown above

### Cell Format Auto-Detect Disable
-##### requires `v3.2.0` or higher
The system will auto-detect the Excel format to use for Date and Number field types, if for some reason you wish to disable it then you provide the excel export options below
```ts
diff --git a/docs/grid-functionalities/export-to-text-file.md b/docs/grid-functionalities/export-to-text-file.md
index 8c47ef6c87..4831e81071 100644
--- a/docs/grid-functionalities/export-to-text-file.md
+++ b/docs/grid-functionalities/export-to-text-file.md
@@ -126,7 +126,7 @@ export class MySample {
exportToFile(type = 'csv') {
this.textExportService.exportToFile({
- delimiter: (type === 'csv') ? DelimiterType.comma : DelimiterType.tab,
+ delimiter: (type === 'csv') ? ',' : '\t',
filename: 'myExport',
format: (type === 'csv') ? 'csv' : 'txt'
});
diff --git a/docs/grid-functionalities/grid-menu.md b/docs/grid-functionalities/grid-menu.md
index 9fce5112ef..2e1befef10 100644
--- a/docs/grid-functionalities/grid-menu.md
+++ b/docs/grid-functionalities/grid-menu.md
@@ -21,6 +21,8 @@ The Grid Menu also comes, by default, with a list of built-in custom commands (a
- _Refresh Dataset_, only shown when using Backend Service API (you can hide it with `hideRefreshDatasetCommand: true`)
This section is called Custom Commands because you can also customize this section with your own commands. To do that, you need to fill in 2 properties (an array of `commandItems` and define `onGridMenuCommand` callback) in your Grid Options. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
+
+#### Using Static Command Items
```ts
this.gridOptions = {
enableAutoResize: true,
@@ -72,6 +74,90 @@ this.gridOptions = {
}
};
```
+
+#### Advanced: Dynamic Command List Builder
+For more advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list. This function is executed **after** `commandItems` is processed and is the **last call before rendering** the menu in the DOM.
+
+**When to use `commandListBuilder`:**
+- You want to append/prepend items to the built-in commands
+- You need to filter commands based on runtime conditions
+- You want to sort or reorder commands dynamically
+- You need access to both built-in and custom commands to manipulate the final list
+
+**Note:** You would typically use `commandListBuilder` **instead of** `commandItems` (not both), since the builder gives you full control over the final command list.
+
+```ts
+gridOptions: {
+ gridMenu: {
+ // Build the command list dynamically
+ commandListBuilder: (builtInItems) => {
+ // Example 1: Append custom commands to built-in ones
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'help',
+ title: 'Help',
+ iconCssClass: 'mdi mdi-help-circle',
+ positionOrder: 99,
+ action: () => window.open('https://example.com/help', '_blank')
+ },
+ ];
+ },
+ onCommand: (e, args) => {
+ if (args.command === 'help') {
+ // command handled via action callback above
+ }
+ }
+ }
+}
+```
+
+**Example: Filter commands based on user permissions**
+```ts
+gridOptions: {
+ gridMenu: {
+ commandListBuilder: (builtInItems) => {
+ // Remove export commands if user doesn't have export permission
+ if (!this.userHasExportPermission) {
+ return builtInItems.filter(item =>
+ item !== 'divider' &&
+ !item.command?.includes('export')
+ );
+ }
+ return builtInItems;
+ }
+ }
+}
+```
+
+**Example: Reorder and customize the command list**
+```ts
+gridOptions: {
+ gridMenu: {
+ commandListBuilder: (builtInItems) => {
+ // Add custom commands at the beginning
+ const customCommands = [
+ {
+ command: 'refresh-cache',
+ title: 'Refresh Cache',
+ iconCssClass: 'mdi mdi-cached',
+ action: () => this.refreshCache()
+ },
+ 'divider'
+ ];
+
+ // Sort built-in items by title
+ const sortedBuiltIn = builtInItems
+ .filter(item => item !== 'divider')
+ .sort((a, b) => (a.title || '').localeCompare(b.title || ''));
+
+ return [...customCommands, ...sortedBuiltIn];
+ }
+ }
+}
+```
+
#### Events
There are multiple events/callback hooks which are accessible from the Grid Options
- `onBeforeMenuShow`
@@ -104,6 +190,11 @@ gridMenu: {
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/extensions/slickGridMenu.ts) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change an icon of all default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
@@ -120,6 +211,21 @@ this.gridOptions = {
};
```
+### When using Pre-Header
+
+By default the Grid Menu icon will be showing on the right after the column headers, if however you wish to move the button icon to show in the pre-header instead, you could simply use the `iconButtonContainer` grid option
+
+```ts
+gridOptions = {
+ createPreHeaderPanel: true,
+ showPreHeaderPanel: true,
+ preHeaderPanelHeight: 26,
+ gridMenu: {
+ iconButtonContainer: 'preheader', // we can display the grid menu icon in either the preheader or in the column header (default)
+ },
+}
+```
+
### How to Disable the Grid Menu?
You can disable the Grid Menu, by calling `enableGridMenu: false` from the Grid Options.
```ts
diff --git a/docs/grid-functionalities/grid-state-preset.md b/docs/grid-functionalities/grid-state-preset.md
index db7b02965a..f041a2e0f7 100644
--- a/docs/grid-functionalities/grid-state-preset.md
+++ b/docs/grid-functionalities/grid-state-preset.md
@@ -54,6 +54,8 @@ export class GridExample {
}
```
+> **Note** since v10 you can now pass `true` as the argument to `gridStateService.getCurrentGridState(true)` which will return all columns, not just the visible columns but also include the hidden columns and their "hidden" properties.
+
### Using Grid Presets & Filter SearchTerm(s)
What happens when we use the grid `presets` and a [Filter Default SearchTerms](../column-functionalities/filters/Select-Filter.md#default-search-terms)? In this case, the `presets` will win over filter `searchTerms`. The cascading order of priorities is the following
1. Do we have any `presets`? Yes use them, else go to step 2
@@ -72,12 +74,12 @@ export interface CurrentColumn {
}
export interface CurrentFilter {
columnId: string;
- operator?: OperatorType | OperatorString;
+ operator?: OperatorType;
searchTerms?: SearchTerm[];
}
export interface CurrentSorter {
columnId: string;
- direction: SortDirection | SortDirectionString;
+ direction: SortDirection;
}
export interface GridState {
columns?: CurrentColumn[] | null;
@@ -194,6 +196,10 @@ You can show/hide or even change a column position via the `presets`, yes `prese
So let say that we want to hide the last Column on page load, we can just find the column by it's `id` that you want to hide and pass the new column definition to the `presets` (again make sure to follow the correct preset structure).
+#### Option 1
+
+Pass the Grid Presets with an array that has less `presets.columns`, whichever column(s) are missing will be considered hidden columns
+
```ts
this.columnDefinitions = [
// initial column definitions
@@ -204,7 +210,7 @@ this.columnDefinitions = [
const mappedColumnDefinitions = this.columnDefinitions.map(col => ({ columnId: col.id, width: col.width }));
mappedColumnDefinitions.pop();
-// then pass it to the presets
+// then pass it to the grid presets (an array of columns minus the last column)
this.gridOptions = {
presets: {
columns: mappedColumnDefinitions
@@ -213,6 +219,11 @@ this.gridOptions = {
```
This would be the easiest way to do it.
+#### Option 2
+
+Since v10, the second alternative is to pass all the columns to `presets.columns` with some of them having the `hidden` properties. Both approaches are valid in v10, just choose whichever option you prefer.
+
+###### Summary
As pointed out earlier, the `presets` requires a specific structure where the `columns` is the list of columns to show/hide with their possible widths. Also worth mentioning again that the position in the array is very important as it defines the position shown in the UI.
##### ViewModel
@@ -231,4 +242,4 @@ this.gridOptions = {
}
};
```
-You could technically redefine by hand the complete list of `columns` that the `presets` requires. I would personally do it via the Column Definitions looping with `map()`, but go manual is also perfectly fine. You would just re-declare the `columns` again with the `id` and `width` and that would work as well.
\ No newline at end of file
+You could technically redefine by hand the complete list of `columns` that the `presets` requires. I would personally do it via the Column Definitions looping with `map()`, but loading them manually is also perfectly fine. You would just re-declare the `columns` again with the `id` and `width` (maybe include the `hidden` prop as well) and that would work as well.
\ No newline at end of file
diff --git a/docs/grid-functionalities/header-menu-header-buttons.md b/docs/grid-functionalities/header-menu-header-buttons.md
index 555e7ca51c..861f69b24e 100644
--- a/docs/grid-functionalities/header-menu-header-buttons.md
+++ b/docs/grid-functionalities/header-menu-header-buttons.md
@@ -20,7 +20,10 @@ The Header Menu also comes, by default, with a list of built-in custom commands
- Sort Descending (you can hide it with `hideSortCommands: true`)
- Hide Column (you can hide it with `hideColumnHideCommand: true`)
-This section is called Custom Commands because you can also customize this section with your own commands. To do that, you need to fill in 2 properties (an array of `headerMenuItems` that will go under each column definition and define `onCommand` callbacks) in your Grid Options. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
+This section is called Custom Commands because you can also customize this section with your own commands. You can do this in two ways: using static command items or using a dynamic command list builder.
+
+#### Static Command Items
+To add static commands, fill in an array of items in your column definition's `header.menu.commandItems`. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
```ts
this.gridOptions = {
enableAutoResize: true,
@@ -51,6 +54,99 @@ this.gridOptions = {
}
};
```
+#### Dynamic Command List Builder
+For advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list. This is executed **after** `commandItems` and is the **last call before rendering**.
+
+```ts
+this.columnDefinitions = [
+ {
+ id: 'title',
+ name: 'Title',
+ field: 'title',
+ header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Append custom commands to the built-in sort/hide commands
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'freeze-column',
+ title: 'Freeze Column',
+ iconCssClass: 'mdi mdi-pin',
+ action: (e, args) => {
+ // Implement column freezing
+ console.log('Freeze column:', args.column.name);
+ }
+ }
+ ];
+ }
+ }
+ }
+ }
+];
+```
+
+**Example: Conditional commands based on column type**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ const column = this; // column context
+
+ // Add filtering option only for filterable columns
+ const extraCommands = [];
+ if (column.filterable !== false) {
+ extraCommands.push({
+ command: 'clear-filter',
+ title: 'Clear Filter',
+ iconCssClass: 'mdi mdi-filter-remove',
+ action: (e, args) => {
+ this.filterService.clearFilterByColumnId(args.column.id);
+ }
+ });
+ }
+
+ return [...builtInItems, ...extraCommands];
+ }
+ }
+}
+```
+
+**Example: Remove sort commands, keep only custom ones**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Filter out sort commands
+ const filtered = builtInItems.filter(item =>
+ item !== 'divider' &&
+ !item.command?.includes('sort')
+ );
+
+ // Add custom commands
+ return [
+ ...filtered,
+ {
+ command: 'custom-action',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ action: (e, args) => alert('Custom: ' + args.column.name)
+ }
+ ];
+ }
+ }
+}
+```
+
+**When to use `commandListBuilder`:**
+- You want to append/prepend items to built-in commands
+- You need to filter or modify commands based on column properties
+- You want to customize the command list per column dynamically
+- You need full control over the final command list
+
+**Note:** Use `commandListBuilder` **instead of** `commandItems`, not both together.
+
#### Callback Hooks
There are 2 callback hooks which are accessible in the Grid Options
- `onBeforeMenuShow`
@@ -58,6 +154,11 @@ There are 2 callback hooks which are accessible in the Grid Options
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/extensions/slickHeaderButtons.ts) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change icon(s) of the default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
diff --git a/docs/grid-functionalities/row-based-edit.md b/docs/grid-functionalities/row-based-edit.md
index 8935cad3dd..9eb226c940 100644
--- a/docs/grid-functionalities/row-based-edit.md
+++ b/docs/grid-functionalities/row-based-edit.md
@@ -7,7 +7,7 @@
- [Disable External Button when having Empty Selection](#disable-external-button-when-having-empty-selection)
- [Change Row Selections](#change-row-selections)
- Troubleshooting
- - [Adding a Column dynamically is removing the Row Selection, why is that?](#adding-a-column-dynamically-is-removing-the-row-selection-why-is-that)
+ - [Adding a Column dynamically is removing the Row Selection column, why is that?](#adding-a-column-dynamically-is-removing-the-row-selection-column-why-is-that)
### Description
The Row based editing plugin makes it possible to keep the grid readonly except for rows which the user explicitely toggles into edit mode.
diff --git a/docs/grid-functionalities/row-detail.md b/docs/grid-functionalities/row-detail.md
index f9914a4a91..545e77d647 100644
--- a/docs/grid-functionalities/row-detail.md
+++ b/docs/grid-functionalities/row-detail.md
@@ -6,7 +6,7 @@
- [Row Detail - View Component](#row-detail---view-component)
- [Access Parent Component (grid) from the Child Component (row detail)](#access-parent-component-grid-from-the-child-component-row-detail)
- Troubleshooting
- - [Adding a Column dynamically is removing the Row Selection, why is that?](#adding-a-column-dynamically-is-removing-the-row-selection-why-is-that)
+ - [Adding a Column dynamically is removing the Row Selection column, why is that?](#adding-a-column-dynamically-is-removing-the-row-selection-column-why-is-that)
### Demo
[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example21) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vanilla/src/examples/example21.ts)
@@ -61,14 +61,14 @@ export default class Example21 {
this.gridOptions = {
enableRowDetailView: true,
// `rowSelectionOptions` in <=9.x OR `selectionOptions` in >=10.x
- rowSelectionOptions: {
+ selectionOptions: {
selectActiveRow: true
},
preRegisterExternalExtensions: (pubSubService) => {
// Row Detail View is a special case because of its requirement to create extra column definition dynamically
// so it must be pre-registered before SlickGrid is instantiated, we can do so via this option
this.rowDetail = new SlickRowDetailView(pubSubService);
- return [{ name: ExtensionName.rowDetailView, instance: this.rowDetail }];
+ return [{ name: 'rowDetailView', instance: this.rowDetail }];
},
rowDetailView: {
// We can load the "process" asynchronously via Fetch, Promise, ...
@@ -181,7 +181,7 @@ This requires a bit more work, you can call the method `collapseDetailView(item)
```ts
closeRowDetail(gridRowIndex: number) {
if (this.sgb) {
- const rowDetailInstance = this.sgb.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView);
+ const rowDetailInstance = this.sgb.extensionService.getExtensionInstanceByName('rowDetailView');
const item = this.sgb.gridService.getDataItemByRowIndex(gridRowIndex);
rowDetailInstance.collapseDetailView(item);
}
@@ -234,7 +234,7 @@ export class Example {
// Row Detail View is a special case because of its requirement to create extra column definition dynamically
// so it must be pre-registered before SlickGrid is instantiated, we can do so via this option
const rowDetail = new SlickRowDetailView(pubSubService as EventPubSubService);
- return [{ name: ExtensionName.rowDetailView, instance: rowDetail }];
+ return [{ name: 'rowDetailView', instance: rowDetail }];
},
rowDetailView: {
// We can load the "process" asynchronously via Fetch, Promise, ...
diff --git a/docs/grid-functionalities/row-selection.md b/docs/grid-functionalities/row-selection.md
index 054c8287aa..392adda787 100644
--- a/docs/grid-functionalities/row-selection.md
+++ b/docs/grid-functionalities/row-selection.md
@@ -7,7 +7,7 @@
- [Disable External Button when having Empty Selection](#disable-external-button-when-having-empty-selection)
- [Change Row Selections](#change-row-selections)
- Troubleshooting
- - [Adding a Column dynamically is removing the Row Selection, why is that?](#adding-a-column-dynamically-is-removing-the-row-selection-why-is-that)
+ - [Adding a Column dynamically is removing the Row Selection column, why is that?](#adding-a-column-dynamically-is-removing-the-row-selection-column-why-is-that)
- [Hybrid Selection Model (cell+row selection)](#hybrid-selection-model-and-drag-fill)
### Description
@@ -108,7 +108,7 @@ export class Example1 {
enableCheckboxSelector: true,
enableRowSelection: true,
// `rowSelectionOptions` in <=9.x OR `selectionOptions` in >=10.x
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false
},
@@ -134,7 +134,7 @@ export class Example1 {
enableCheckboxSelector: true,
enableRowSelection: true,
// `rowSelectionOptions` in <=9.x OR `selectionOptions` in >=10.x
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false
},
@@ -198,7 +198,7 @@ export class Example1 implements OnInit {
},
multiSelect: false,
// `rowSelectionOptions` in <=9.x OR `selectionOptions` in >=10.x
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: true,
},
@@ -280,7 +280,7 @@ this.gridOptions = {
// enable new hybrid selection model (rows & cells)
enableHybridSelection: true,
// `rowSelectionOptions` in <=9.x OR `selectionOptions` in >=10.x
- rowSelectionOptions: {
+ selectionOptions: {
selectActiveRow: true,
rowSelectColumnIds: ['selector'],
},
@@ -340,7 +340,7 @@ export class Example1 {
```
## Troubleshooting
-### Adding a Column dynamically is removing the Row Selection, why is that?
+### Adding a Column dynamically is removing the Row Selection column, why is that?
The reason is because the Row Selection (checkbox) plugin is a special column and Slickgrid-Universal is adding an extra column dynamically for the Row Selection checkbox and that is **not** reflected in your local copy of `columnDefinitions`. To address this issue, you need to get the Slickgrid-Universal internal copy of all columns (including the extra columns), you can get it via `getAllColumnDefinitions()` from the Grid Service and then you can use to that array and that will work.
```ts
diff --git a/docs/localization/localization-i18n.md b/docs/localization/localization-i18n.md
index 4558eada05..69e1474232 100644
--- a/docs/localization/localization-i18n.md
+++ b/docs/localization/localization-i18n.md
@@ -115,9 +115,8 @@ The final step is of course the actual translations. There's multiple ways to co
1. Manually copy the translation keys/values
2. Manually copy the JSON files to your `assets` folder
3. Or modify your `package.json` and add a script to copy the JSON files to your `assets` folder
- - install NPM package `copyfiles` (`npm install copy-files`)
+ - install NPM package `native-copyfiles` (`npm install native-copyfiles`)
- add a new script in your `package.json`
- run the script **once** with `npm run copy:i18n` and you should now have the JSON files in your `src/assets` folder
- - visit [copyfiles](https://www.npmjs.com/package/copyfiles) site on how to use it in your npm script
If you want to manually re-create the translation in your own files, the list of translations that you will need are displayed in the asset i18n translation folder (from that file, you need all translations shown before the 'BILLING', the next few ones are for the demo page only).
diff --git a/docs/menu-slots.md b/docs/menu-slots.md
new file mode 100644
index 0000000000..14b9f9bb47
--- /dev/null
+++ b/docs/menu-slots.md
@@ -0,0 +1,524 @@
+## Custom Menu Slots - Rendering
+
+All menu plugins (Header Menu, Cell Menu, Context Menu, Grid Menu) support **cross-framework compatible slot rendering** for custom content injection in menu items. This is achieved through the `slotRenderer` callback at the item level combined with an optional `defaultMenuItemRenderer` at the menu level.
+
+> **Note:** This documentation covers **how menu items are rendered** (visual presentation). If you need to **dynamically modify which commands appear** in the menu (filtering, sorting, adding/removing items), see the `commandListBuilder` callback documented in [Grid Menu](grid-functionalities/grid-menu.md), [Context Menu](grid-functionalities/context-menu.md), or [Header Menu](grid-functionalities/header-menu-header-buttons.md).
+
+### TypeScript Tip: Type Inference with commandListBuilder
+
+When using `commandListBuilder` to add custom menu items with slotRenderer callbacks, **cast the return value to the appropriate type** to enable proper type parameters in callbacks:
+
+```typescript
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ return [
+ ...builtInItems,
+ {
+ command: 'custom-action',
+ title: 'My Action',
+ slotRenderer: (cmdItem, args) => `
${cmdItem.title}
`,
+ }
+ ] as Array;
+ }
+}
+```
+
+Alternatively, if you only have built-in items and dividers, you can use the simpler cast:
+
+```typescript
+return [...] as Array;
+```
+
+### Core Concept
+
+Each menu item can define a `slotRenderer` callback function that receives the item and args, and returns either an HTML string or an HTMLElement. This single API works uniformly across all menu plugins.
+
+### Slot Renderer Callback
+
+```typescript
+slotRenderer?: (cmdItem: MenuItem, args: MenuCallbackArgs, event?: Event) => string | HTMLElement
+```
+
+- **cmdItem** - The menu cmdItem object containing command, title, iconCssClass, etc.
+- **args** - The callback args providing access to grid, column, dataContext, and other context
+- **event** - Optional DOM event passed during click handling (allows `stopPropagation()`)
+
+### Basic Example - HTML String Rendering
+
+```typescript
+const menuItem = {
+ command: 'custom-command',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ // Return custom HTML string for the entire menu item
+ slotRenderer: () => `
+
+
+ Custom Action
+ NEW
+
+ `
+};
+```
+
+### Advanced Example - HTMLElement Objects
+
+```typescript
+// Create custom element with full DOM control
+const menuItem = {
+ command: 'notifications',
+ title: 'Notifications',
+ // Return HTMLElement for more control and event listeners
+ slotRenderer: (cmdItem, args) => {
+ const container = document.createElement('div');
+ container.style.display = 'flex';
+ container.style.alignItems = 'center';
+
+ const icon = document.createElement('i');
+ icon.className = 'mdi mdi-bell';
+ icon.style.marginRight = '8px';
+
+ const text = document.createElement('span');
+ text.textContent = cmdItem.title;
+
+ const badge = document.createElement('span');
+ badge.className = 'badge';
+ badge.textContent = '5';
+ badge.style.marginLeft = 'auto';
+
+ container.appendChild(icon);
+ container.appendChild(text);
+ container.appendChild(badge);
+
+ return container;
+ }
+};
+```
+
+### Default Menu-Level Renderer
+
+Set a `defaultMenuItemRenderer` at the menu option level to apply to all items (unless overridden by individual `slotRenderer`):
+
+```typescript
+const menuOption = {
+ // Apply this renderer to all menu items (can be overridden per item)
+ defaultMenuItemRenderer: (cmdItem, args) => {
+ return `
+
';
+ }
+ }
+}
+```
diff --git a/docs/migrations/migration-to-10.x.md b/docs/migrations/migration-to-10.x.md
new file mode 100644
index 0000000000..0497ce0e33
--- /dev/null
+++ b/docs/migrations/migration-to-10.x.md
@@ -0,0 +1,237 @@
+## Simplification and Modernization โก
+
+One of the biggest change of this release is to hide columns by using the `hidden` column property (now used by Column Picker, Grid Menu, etc...). Previously we were removing columns from the original columns array and we then called `setColumns()` to update the grid, but this meant that we had to keep references for all visible and non-visible columns. With this new release we now keep the full columns array at all time and instead we just change column(s) visibility via their `hidden` column properties by using `grid.updateColumnById('id', { hidden: true })` and finally we update the grid via `grid.updateColumns()`. What I'm trying to emphasis is that you should really stop using `grid.setColumns()` in v10+, and if you want to hide some columns when declaring the columns, then just update their `hidden` properties, see more details below...
+
+#### Major Changes - Quick Summary
+- [`hidden` columns](#hidden-columns)
+- [What's next?](#whats-next-version-11)
+
+> **Note:** if you come from an earlier version, please make sure to follow each migrations in their respective order (review previous migration guides)
+
+### Column Definitions
+
+#### Hidden Columns
+
+_if you're not dynamically hiding columns and you're not using `colspan` or `rowspan` then you won't be impacted by this change._
+
+For years, I had to keep some references in a Shared Service via `shared.allColumns` and `shared.visibleColumns`, for translating locales and that is also being used by Column Picker and Grid Menu to keep track of which columns to hide/show and in which order they were; then later we called `grid.setColumns()` to update the columns in the grid... but that had side effects since SlickGrid never kept the entire column definitions list (until now). However with v10, we simply start using `hidden` property on the column(s) to hide/show some of them, then we are now able to keep the full columns reference at all time. We can translate them more easily and we no longer need to use `grid.setColumns()`, what we'll do instead is to start using `grid.updateColumnById('colId', { hidden: true })`. If you want to get visible columns, you can now simply call `grid.getVisibleColumns()` which behind the scene is simply doing a `columns.filter(c => !c.hidden)`. This new approach does also have side effects for colspan/rowspan, because previously if we were to hide a column then the next column to the right was previously taking over the spannings, but with the new approach if we hide a column then its spannings will now disappear with it (so I had to make code changes to handle that too)... If you want more details, you can see full explanations of the complete change in the [PR #2281](https://github.com/ghiscoding/slickgrid-universal/pull/2281)
+
+#### New Approach with column `hidden` property
+
+| Before | After |
+| ------- | ------ |
+| `grid.setColumns(visibleCols)` | `grid.updateColumnById('colId', { hidden: true });` and `grid.updateColumns();` |
+| `sharedService.allColumns` | `grid.getColumns()` _... is now including all columns_ |
+| `sharedService.visibleColumns` or `grid.getColumns()`| `grid.getVisibleColumns()` |
+
+## Grid Functionalities
+
+_following changes should be transparent to most users, I'm just listing them in case of side effects._
+
+1. Reimplementing `SlickCompositeEditorComponent` modal and migrating from a `
{
- this.selectedLanguage = nextLanguage;
+ this.selectedLanguage.set(nextLanguage);
})
);
}
toggleGridMenu(e: MouseEvent) {
if (this.angularGrid && this.angularGrid.extensionService) {
- const gridMenuInstance = this.angularGrid.extensionService.getExtensionInstanceByName(ExtensionName.gridMenu);
+ const gridMenuInstance = this.angularGrid.extensionService.getExtensionInstanceByName('gridMenu');
// open the external button Grid Menu, you can also optionally pass Grid Menu options as 2nd argument
// for example we want to align our external button on the right without affecting the menu within the grid which will stay aligned on the left
gridMenuInstance.showGridMenu(e, { dropSide: 'right' });
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example10.component.html b/frameworks/angular-slickgrid/src/demos/examples/example10.component.html
index 4d4af6e170..031380e5d4 100644
--- a/frameworks/angular-slickgrid/src/demos/examples/example10.component.html
+++ b/frameworks/angular-slickgrid/src/demos/examples/example10.component.html
@@ -50,7 +50,7 @@
+ }
value.toF
styleUrls: ['example44.component.scss'],
templateUrl: './example44.component.html',
encapsulation: ViewEncapsulation.None,
- imports: [AngularSlickgridModule, FormsModule],
+ imports: [AngularSlickgridComponent, FormsModule],
})
export class Example44Component implements OnInit {
columnDefinitions: Column[] = [];
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example45-detail.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example45-detail.component.ts
index 0b90da82fe..10dc61101e 100644
--- a/frameworks/angular-slickgrid/src/demos/examples/example45-detail.component.ts
+++ b/frameworks/angular-slickgrid/src/demos/examples/example45-detail.component.ts
@@ -1,5 +1,5 @@
import { Component, ViewEncapsulation, type OnDestroy, type OnInit } from '@angular/core';
-import { AngularSlickgridModule, type AngularGridInstance, type Column, type GridOption, type GridState } from '../../library';
+import { AngularSlickgridComponent, type AngularGridInstance, type Column, type GridOption, type GridState } from '../../library';
export interface Distributor {
id: number;
@@ -24,7 +24,7 @@ export interface OrderData {
styles: ['.innergrid { --slick-header-menu-display: inline-block; }'],
templateUrl: './example45-detail.component.html',
encapsulation: ViewEncapsulation.None,
- imports: [AngularSlickgridModule],
+ imports: [AngularSlickgridComponent],
})
export class Example45DetailComponent implements OnDestroy, OnInit {
model!: Distributor;
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example45.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example45.component.ts
index 465fce3ac9..38ef8e7058 100644
--- a/frameworks/angular-slickgrid/src/demos/examples/example45.component.ts
+++ b/frameworks/angular-slickgrid/src/demos/examples/example45.component.ts
@@ -1,7 +1,8 @@
import { Component, ViewEncapsulation, type OnDestroy, type OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { faker } from '@faker-js/faker';
-import { AngularSlickgridModule, type AngularGridInstance, type Column, type GridOption, type SlickRowDetailView } from '../../library';
+import { AngularSlickRowDetailView } from '@slickgrid-universal/angular-row-detail-plugin';
+import { AngularSlickgridComponent, type AngularGridInstance, type Column, type GridOption } from '../../library';
import { Example45DetailComponent, type Distributor, type OrderData } from './example45-detail.component';
import { RowDetailPreloadComponent } from './rowdetail-preload.component';
@@ -12,7 +13,7 @@ const NB_ITEMS = 995;
styleUrls: ['example45.component.scss'],
templateUrl: './example45.component.html',
encapsulation: ViewEncapsulation.None,
- imports: [AngularSlickgridModule, FormsModule],
+ imports: [AngularSlickgridComponent, FormsModule],
})
export class Example45Component implements OnDestroy, OnInit {
private _darkMode = false;
@@ -26,7 +27,7 @@ export class Example45Component implements OnDestroy, OnInit {
isUsingAutoHeight = false;
serverWaitDelay = FAKE_SERVER_DELAY;
- get rowDetailInstance(): SlickRowDetailView {
+ get rowDetailInstance(): AngularSlickRowDetailView {
return this.angularGrid.extensions.rowDetailView?.instance || {};
}
@@ -111,6 +112,7 @@ export class Example45Component implements OnDestroy, OnInit {
rowTopOffsetRenderType: 'top', // RowDetail and/or RowSpan don't render well with "transform", you should use "top"
darkMode: this._darkMode,
rowHeight: 33,
+ externalResources: [AngularSlickRowDetailView],
rowDetailView: {
process: (item: any) => this.simulateServerAsyncCall(item),
loadOnce: false, // you can't use loadOnce with inner grid because only HTML template are re-rendered, not JS events
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example46.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example46.component.ts
index 5854b5184e..2bad7b4463 100644
--- a/frameworks/angular-slickgrid/src/demos/examples/example46.component.ts
+++ b/frameworks/angular-slickgrid/src/demos/examples/example46.component.ts
@@ -3,7 +3,7 @@ import { FormsModule } from '@angular/forms';
import { ExcelExportService } from '@slickgrid-universal/excel-export';
import { TextExportService } from '@slickgrid-universal/text-export';
import {
- AngularSlickgridModule,
+ AngularSlickgridComponent,
Filters,
Formatters,
type AngularGridInstance,
@@ -41,7 +41,7 @@ const coloredTextFormatter: Formatter = (_row: number, _cell: number, val: any,
templateUrl: './example46.component.html',
styleUrls: ['example46.component.scss'],
encapsulation: ViewEncapsulation.None,
- imports: [AngularSlickgridModule, FormsModule],
+ imports: [AngularSlickgridComponent, FormsModule],
})
export class Example46Component implements OnInit {
angularGrid!: AngularGridInstance;
@@ -138,7 +138,7 @@ export class Example46Component implements OnInit {
sanitizeDataExport: true,
},
enableCheckboxSelector: true,
- enableRowSelection: true,
+ enableSelection: true,
multiSelect: false,
checkboxSelector: {
// columnIndexPosition: 1,
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example47.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example47.component.ts
index ee42287e7d..eedf624689 100644
--- a/frameworks/angular-slickgrid/src/demos/examples/example47.component.ts
+++ b/frameworks/angular-slickgrid/src/demos/examples/example47.component.ts
@@ -1,8 +1,9 @@
import { Component, type OnDestroy, type OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
+import { AngularSlickRowDetailView } from '@slickgrid-universal/angular-row-detail-plugin';
import {
Aggregators,
- AngularSlickgridModule,
+ AngularSlickgridComponent,
Editors,
Filters,
Formatters,
@@ -13,7 +14,6 @@ import {
type Column,
type GridOption,
type Grouping,
- type SlickRowDetailView,
} from '../../library';
import { Example47RowDetailComponent } from './example47-rowdetail.component';
import { RowDetailPreloadComponent } from './rowdetail-preload.component';
@@ -34,7 +34,7 @@ export interface Item {
@Component({
templateUrl: './example47.component.html',
- imports: [AngularSlickgridModule, FormsModule],
+ imports: [AngularSlickgridComponent, FormsModule],
})
export class Example47Component implements OnDestroy, OnInit {
private _darkMode = false;
@@ -57,7 +57,7 @@ export class Example47Component implements OnDestroy, OnInit {
this.groupByDuration(); // group by duration on page load
}
- get rowDetailInstance(): SlickRowDetailView {
+ get rowDetailInstance(): AngularSlickRowDetailView {
// you can get the SlickGrid RowDetail plugin (addon) instance via 2 ways
// option 1
@@ -178,6 +178,7 @@ export class Example47Component implements OnDestroy, OnInit {
enableRowDetailView: true,
rowTopOffsetRenderType: 'top', // RowDetail and/or RowSpan don't render well with "transform", you should use "top"
darkMode: this._darkMode,
+ externalResources: [AngularSlickRowDetailView],
rowDetailView: {
process: (item) => this.simulateServerAsyncCall(item),
loadOnce: true,
@@ -191,7 +192,7 @@ export class Example47Component implements OnDestroy, OnInit {
// View Component to load when row detail data is ready
viewComponent: Example47RowDetailComponent,
},
- rowSelectionOptions: {
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: true,
},
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example48.component.html b/frameworks/angular-slickgrid/src/demos/examples/example48.component.html
index ac9233c736..1071059c7d 100644
--- a/frameworks/angular-slickgrid/src/demos/examples/example48.component.html
+++ b/frameworks/angular-slickgrid/src/demos/examples/example48.component.html
@@ -17,7 +17,7 @@
SlickHybridSelectionModel This Selection Model is an hybrid approach that uses a combination of the row or cell selections
- depending on certain conditions. Use enableHybridSelection grid option to enable the new Hybrid Selection Model.
+ depending on certain conditions.
1. clicking on the first column (id) will use RowSelectionModel because of our configuration of
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example48.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example48.component.ts
index aa6eee34f4..b12280aa58 100644
--- a/frameworks/angular-slickgrid/src/demos/examples/example48.component.ts
+++ b/frameworks/angular-slickgrid/src/demos/examples/example48.component.ts
@@ -1,7 +1,7 @@
import { Component, type OnInit } from '@angular/core';
import { ExcelExportService } from '@slickgrid-universal/excel-export';
import {
- AngularSlickgridModule,
+ AngularSlickgridComponent,
Formatters,
SlickEventHandler,
type AngularGridInstance,
@@ -14,7 +14,7 @@ const NB_ITEMS = 995;
@Component({
templateUrl: './example48.component.html',
// styles: ['#grid48-1 { .slick-row .slick-cell:first-child { border-right: 1px solid #d4d4d4; } }'],
- imports: [AngularSlickgridModule],
+ imports: [AngularSlickgridComponent],
})
export class Example48Component implements OnInit {
protected _eventHandler = new SlickEventHandler();
@@ -124,9 +124,10 @@ export class Example48Component implements OnInit {
externalResources: [new ExcelExportService()],
// enable new hybrid selection model (rows & cells)
- enableHybridSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
rowSelectColumnIds: ['id'],
+ selectionType: 'mixed',
},
// when using the ExcelCopyBuffer, you can see what the selection range is
@@ -141,8 +142,8 @@ export class Example48Component implements OnInit {
...this.gridOptions1,
// you can also enable checkbox selection & row selection, make sure to use `rowSelectColumnIds: ['id', '_checkbox_selector']`
enableCheckboxSelector: true,
- enableRowSelection: true,
- rowSelectionOptions: {
+ enableSelection: true,
+ selectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false,
diff --git a/frameworks/angular-slickgrid/src/demos/examples/example49.component.html b/frameworks/angular-slickgrid/src/demos/examples/example49.component.html
index 8e8c7cab33..bbd6605a3f 100644
--- a/frameworks/angular-slickgrid/src/demos/examples/example49.component.html
+++ b/frameworks/angular-slickgrid/src/demos/examples/example49.component.html
@@ -22,7 +22,8 @@
Spreadsheet with drag-fill, hybrid selection model. Type a few values in the grid and then select those cells and use the bottom right
drag handle spread the selection and auto-fill the values to other cells. Use onDragReplaceCells event to customize the
- drag-fill behavior. Use enableHybridSelection grid option to enable the new Hybrid Selection Model.
+ drag-fill behavior. Use { enableSelection: true, selectionOptions: { selectionType: 'mixed' }}
+ grid option to enable the new Hybrid Selection Model.
+ Example 51: Menus with Slots
+
+
+ code
+
+
+
+
+
+
+
+
+
+
+ Menu Slots Demo with Custom Renderer
+
+
+ Click on the menu buttons to see the new single slot functionality working across all menu types (Header Menu, Cell
+ Menu, Context Menu, Grid Menu):
+
+
+ Note: The demo focuses on the custom rendering capability via slotRenderer and
+ defaultMenuItemRenderer, which work across all menu plugins (SlickHeaderMenu, SlickCellMenu, SlickContextMenu,
+ SlickGridMenu). Also note that the keyboard shortcuts displayed in the menus (e.g., Alt+โ, F5) are for demo
+ purposes only and do not actually trigger any actions.
+
+
+
+
+
+
+
+ Clear grouping
+
+
+
+ Collapse all groups
+
+
+
+ Expand all groups
+
+
+ Group by Duration
+
+