From 432f878999b74d790a94845acd793aefd47c895e Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Sat, 14 Feb 2026 14:04:37 -0500 Subject: [PATCH 1/2] chore: merge next to master branch --- .github/copilot-instructions.md | 76 + .github/renovate.json5 | 14 - .github/workflows/pkg-pr-new.yml | 2 +- .github/workflows/release.yml | 49 +- .github/workflows/test-angular.yml | 5 +- .github/workflows/test-aurelia.yml | 5 +- .github/workflows/test-react.yml | 5 +- .github/workflows/test-vanilla.yml | 2 +- .github/workflows/test-vue.yml | 5 +- .prettierignore | 4 +- .vscode/settings.json | 4 +- CHANGELOG.md | 52 + demos/aurelia/CHANGELOG.md | 32 + demos/aurelia/package.json | 6 +- .../custom-aureliaViewModelEditor.ts | 2 +- .../custom-aureliaViewModelFilter.ts | 5 +- .../examples/slickgrid/custom-inputFilter.ts | 5 +- .../src/examples/slickgrid/example10.ts | 10 +- .../src/examples/slickgrid/example11.html | 2 +- .../src/examples/slickgrid/example11.ts | 2 +- .../src/examples/slickgrid/example12.ts | 5 +- .../src/examples/slickgrid/example14.html | 21 +- .../src/examples/slickgrid/example14.ts | 14 + .../src/examples/slickgrid/example16.ts | 16 +- .../src/examples/slickgrid/example18.ts | 7 +- .../slickgrid/example19-detail-view.ts | 4 +- .../src/examples/slickgrid/example19.ts | 8 +- .../src/examples/slickgrid/example2.ts | 2 +- .../src/examples/slickgrid/example21.ts | 8 +- .../src/examples/slickgrid/example23.ts | 13 +- .../src/examples/slickgrid/example24.ts | 15 +- .../src/examples/slickgrid/example25.ts | 7 +- .../src/examples/slickgrid/example26.ts | 3 +- .../src/examples/slickgrid/example3.ts | 7 +- .../src/examples/slickgrid/example30.ts | 4 +- .../src/examples/slickgrid/example31.ts | 9 +- .../src/examples/slickgrid/example32.ts | 6 +- .../src/examples/slickgrid/example33.html | 18 + .../src/examples/slickgrid/example33.ts | 55 +- .../src/examples/slickgrid/example38.ts | 2 +- .../src/examples/slickgrid/example4.ts | 9 +- .../src/examples/slickgrid/example41.ts | 5 +- .../src/examples/slickgrid/example42.ts | 5 +- .../src/examples/slickgrid/example43.ts | 19 +- .../src/examples/slickgrid/example45.ts | 6 +- .../src/examples/slickgrid/example46.ts | 2 +- .../slickgrid/example47-detail-view.ts | 4 +- .../src/examples/slickgrid/example47.ts | 5 +- .../src/examples/slickgrid/example48.html | 3 +- .../src/examples/slickgrid/example48.ts | 9 +- .../src/examples/slickgrid/example49.html | 3 +- .../src/examples/slickgrid/example49.ts | 7 +- .../src/examples/slickgrid/example5.ts | 11 +- .../src/examples/slickgrid/example50.ts | 4 +- .../src/examples/slickgrid/example51.html | 75 + .../src/examples/slickgrid/example51.scss | 144 + .../src/examples/slickgrid/example51.ts | 611 + .../src/examples/slickgrid/example6.ts | 34 +- .../src/examples/slickgrid/example9.ts | 3 +- .../src/examples/slickgrid/utilities.ts | 7 - demos/aurelia/src/my-app.ts | 1 + .../aurelia/test/cypress/e2e/example10.cy.ts | 92 - .../aurelia/test/cypress/e2e/example14.cy.ts | 142 +- .../aurelia/test/cypress/e2e/example16.cy.ts | 2 +- .../aurelia/test/cypress/e2e/example18.cy.ts | 9 +- .../aurelia/test/cypress/e2e/example32.cy.ts | 32 +- .../aurelia/test/cypress/e2e/example33.cy.ts | 86 +- .../aurelia/test/cypress/e2e/example44.cy.ts | 98 + .../aurelia/test/cypress/e2e/example48.cy.ts | 8 +- .../aurelia/test/cypress/e2e/example51.cy.ts | 382 + .../aurelia/test/cypress/support/commands.ts | 33 + demos/react/CHANGELOG.md | 33 + demos/react/package.json | 13 +- demos/react/src/examples/slickgrid/App.tsx | 3 + .../src/examples/slickgrid/Example10.tsx | 10 +- .../src/examples/slickgrid/Example11.tsx | 4 +- .../src/examples/slickgrid/Example12.tsx | 5 +- .../src/examples/slickgrid/Example14.tsx | 34 +- .../src/examples/slickgrid/Example15.tsx | 2 - .../src/examples/slickgrid/Example16.tsx | 4 +- .../src/examples/slickgrid/Example18.tsx | 7 +- .../src/examples/slickgrid/Example19.tsx | 21 +- .../src/examples/slickgrid/Example21.tsx | 15 +- .../src/examples/slickgrid/Example23.tsx | 9 +- .../src/examples/slickgrid/Example24.tsx | 17 +- .../src/examples/slickgrid/Example25.tsx | 16 +- .../react/src/examples/slickgrid/Example3.tsx | 7 +- .../src/examples/slickgrid/Example30.tsx | 4 +- .../src/examples/slickgrid/Example31.tsx | 7 +- .../src/examples/slickgrid/Example32.tsx | 6 +- .../src/examples/slickgrid/Example33.tsx | 73 +- .../src/examples/slickgrid/Example38.tsx | 2 +- .../react/src/examples/slickgrid/Example4.tsx | 5 +- .../src/examples/slickgrid/Example41.tsx | 5 +- .../src/examples/slickgrid/Example42.tsx | 5 +- .../src/examples/slickgrid/Example43.tsx | 19 +- .../src/examples/slickgrid/Example45.tsx | 16 +- .../src/examples/slickgrid/Example46.tsx | 2 +- .../src/examples/slickgrid/Example47.tsx | 10 +- .../src/examples/slickgrid/Example48.tsx | 12 +- .../src/examples/slickgrid/Example49.tsx | 9 +- .../react/src/examples/slickgrid/Example5.tsx | 11 +- .../src/examples/slickgrid/Example50.tsx | 4 +- .../src/examples/slickgrid/Example51.tsx | 694 + .../react/src/examples/slickgrid/Example6.tsx | 28 +- .../react/src/examples/slickgrid/Example9.tsx | 3 +- .../examples/slickgrid/custom-inputFilter.tsx | 5 +- .../src/examples/slickgrid/example51.scss | 144 + .../react/src/examples/slickgrid/utilities.ts | 7 - demos/react/test/cypress/e2e/example10.cy.ts | 94 +- demos/react/test/cypress/e2e/example14.cy.ts | 142 +- demos/react/test/cypress/e2e/example16.cy.ts | 2 +- demos/react/test/cypress/e2e/example18.cy.ts | 9 +- demos/react/test/cypress/e2e/example32.cy.ts | 32 +- demos/react/test/cypress/e2e/example33.cy.ts | 86 +- demos/react/test/cypress/e2e/example44.cy.ts | 100 +- demos/react/test/cypress/e2e/example48.cy.ts | 8 +- demos/react/test/cypress/e2e/example51.cy.ts | 382 + demos/react/test/cypress/support/commands.ts | 33 + demos/react/tsconfig.json | 1 - demos/vanilla/CHANGELOG.md | 30 + demos/vanilla/package.json | 4 +- demos/vanilla/src/app-routing.ts | 2 + demos/vanilla/src/app.html | 1 + demos/vanilla/src/examples/example01.ts | 4 +- demos/vanilla/src/examples/example03.ts | 13 +- demos/vanilla/src/examples/example04.ts | 7 +- demos/vanilla/src/examples/example05.ts | 2 +- demos/vanilla/src/examples/example07.ts | 8 +- demos/vanilla/src/examples/example08.html | 12 +- demos/vanilla/src/examples/example08.ts | 19 +- demos/vanilla/src/examples/example09.ts | 13 +- demos/vanilla/src/examples/example10.ts | 29 +- demos/vanilla/src/examples/example11.ts | 35 +- demos/vanilla/src/examples/example12.ts | 8 +- demos/vanilla/src/examples/example14.ts | 9 +- demos/vanilla/src/examples/example15.ts | 18 +- demos/vanilla/src/examples/example16.html | 29 +- demos/vanilla/src/examples/example16.ts | 36 +- demos/vanilla/src/examples/example17.html | 8 +- demos/vanilla/src/examples/example17.ts | 10 +- demos/vanilla/src/examples/example19.html | 2 +- demos/vanilla/src/examples/example20.ts | 7 +- demos/vanilla/src/examples/example21.ts | 4 +- demos/vanilla/src/examples/example25.ts | 9 +- demos/vanilla/src/examples/example26.ts | 5 +- demos/vanilla/src/examples/example29.ts | 5 +- demos/vanilla/src/examples/example30.ts | 5 +- demos/vanilla/src/examples/example32.ts | 19 +- demos/vanilla/src/examples/example34.ts | 4 +- demos/vanilla/src/examples/example35.ts | 2 +- demos/vanilla/src/examples/example36.ts | 7 +- demos/vanilla/src/examples/example37.html | 5 +- demos/vanilla/src/examples/example37.ts | 9 +- demos/vanilla/src/examples/example38.html | 3 +- demos/vanilla/src/examples/example38.ts | 7 +- demos/vanilla/src/examples/example39.ts | 4 +- demos/vanilla/src/examples/example40.html | 57 + demos/vanilla/src/examples/example40.scss | 140 + demos/vanilla/src/examples/example40.ts | 620 + demos/vanilla/src/examples/utilities.ts | 7 - demos/vanilla/vite.config.mts | 7 + demos/vue/CHANGELOG.md | 34 + demos/vue/package.json | 7 +- demos/vue/src/components/Example01.vue | 4 +- demos/vue/src/components/Example02.vue | 2 +- demos/vue/src/components/Example03.vue | 9 +- demos/vue/src/components/Example04.vue | 11 +- demos/vue/src/components/Example05.vue | 13 +- demos/vue/src/components/Example06.vue | 36 +- demos/vue/src/components/Example07.vue | 4 +- demos/vue/src/components/Example08.vue | 2 +- demos/vue/src/components/Example09.vue | 5 +- demos/vue/src/components/Example10.vue | 12 +- demos/vue/src/components/Example11.vue | 6 +- demos/vue/src/components/Example12.vue | 7 +- demos/vue/src/components/Example13.vue | 2 +- demos/vue/src/components/Example14.vue | 25 +- demos/vue/src/components/Example15.vue | 2 +- demos/vue/src/components/Example16.vue | 8 +- demos/vue/src/components/Example17.vue | 2 +- demos/vue/src/components/Example18.vue | 9 +- demos/vue/src/components/Example19.vue | 21 +- demos/vue/src/components/Example20.vue | 2 +- demos/vue/src/components/Example21.vue | 10 +- demos/vue/src/components/Example22.vue | 4 +- demos/vue/src/components/Example23.vue | 11 +- demos/vue/src/components/Example24.vue | 17 +- demos/vue/src/components/Example25.vue | 7 +- demos/vue/src/components/Example26.vue | 5 +- demos/vue/src/components/Example27.vue | 2 +- demos/vue/src/components/Example29.vue | 2 +- demos/vue/src/components/Example30.vue | 6 +- demos/vue/src/components/Example31.vue | 11 +- demos/vue/src/components/Example32.vue | 8 +- demos/vue/src/components/Example33.vue | 78 +- demos/vue/src/components/Example34.vue | 2 +- demos/vue/src/components/Example35.vue | 2 +- demos/vue/src/components/Example36.vue | 2 +- demos/vue/src/components/Example37.vue | 2 +- demos/vue/src/components/Example38.vue | 4 +- demos/vue/src/components/Example39.vue | 2 +- demos/vue/src/components/Example40.vue | 2 +- demos/vue/src/components/Example41.vue | 7 +- demos/vue/src/components/Example42.vue | 7 +- demos/vue/src/components/Example43.vue | 21 +- demos/vue/src/components/Example44.vue | 2 +- demos/vue/src/components/Example45.vue | 17 +- demos/vue/src/components/Example45Detail.vue | 2 +- demos/vue/src/components/Example46.vue | 2 +- demos/vue/src/components/Example47.vue | 6 +- demos/vue/src/components/Example48.vue | 15 +- demos/vue/src/components/Example49.vue | 12 +- demos/vue/src/components/Example50.vue | 8 +- demos/vue/src/components/Example51.vue | 818 + .../vue/src/components/custom-inputFilter.ts | 5 +- .../src/components/custom-viewModelFilter.ts | 5 +- demos/vue/src/components/utilities.ts | 7 - demos/vue/src/router/index.ts | 6 +- demos/vue/test/cypress/e2e/example10.cy.ts | 92 - demos/vue/test/cypress/e2e/example14.cy.ts | 142 +- demos/vue/test/cypress/e2e/example16.cy.ts | 2 +- demos/vue/test/cypress/e2e/example18.cy.ts | 9 +- demos/vue/test/cypress/e2e/example32.cy.ts | 32 +- demos/vue/test/cypress/e2e/example33.cy.ts | 86 +- demos/vue/test/cypress/e2e/example44.cy.ts | 98 + demos/vue/test/cypress/e2e/example48.cy.ts | 8 +- demos/vue/test/cypress/e2e/example51.cy.ts | 382 + demos/vue/test/cypress/support/commands.ts | 33 + docs/TOC.md | 5 +- docs/backend-services/OData.md | 4 +- .../graphql/GraphQL-Filtering.md | 4 +- docs/column-functionalities/cell-menu.md | 58 +- docs/column-functionalities/editors.md | 2 +- .../editors/Date-Editor-(flatpickr).md | 71 - .../editors/select-dropdown-editor.md | 10 +- .../filters/compound-filters.md | 6 +- .../filters/range-filters.md | 12 +- .../filters/select-filter.md | 20 +- .../filters/single-search-filter.md | 4 +- .../filters/slider-filter.md | 2 +- .../installation-salesforce.md | 2 +- .../composite-editor-modal.md | 4 +- docs/grid-functionalities/context-menu.md | 157 +- docs/grid-functionalities/custom-tooltip.md | 16 + docs/grid-functionalities/export-to-excel.md | 6 +- .../export-to-text-file.md | 2 +- docs/grid-functionalities/grid-menu.md | 106 + .../grid-functionalities/grid-state-preset.md | 19 +- .../header-menu-header-buttons.md | 103 +- docs/grid-functionalities/row-based-edit.md | 2 +- docs/grid-functionalities/row-detail.md | 10 +- docs/grid-functionalities/row-selection.md | 12 +- docs/localization/localization-i18n.md | 3 +- docs/menu-slots.md | 524 + docs/migrations/migration-to-10.x.md | 237 + docs/migrations/migration-to-9.x.md | 20 +- .../examples/example-header-menu-slots.html | 227 + .../angular-row-detail-plugin/.oxlintrc.json | 61 + .../angular-row-detail-plugin/CHANGELOG.md | 23 + .../angular-row-detail-plugin/README.md | 19 + .../angular-row-detail-plugin/package.json | 56 + .../src/angularSlickRowDetailView.ts | 51 +- .../angular-row-detail-plugin/src/index.ts | 1 + .../src/interfaces.ts | 16 + .../angular-row-detail-plugin/tsconfig.json | 39 + .../aurelia-row-detail-plugin/CHANGELOG.md | 19 + .../aurelia-row-detail-plugin/README.md | 19 + .../aurelia-row-detail-plugin/package.json | 51 + .../src/aureliaSlickRowDetailView.ts | 27 +- .../aurelia-row-detail-plugin/src/index.ts | 1 + .../src/interfaces.ts | 16 + .../aurelia-row-detail-plugin/tsconfig.json | 28 + .../react-row-detail-plugin/CHANGELOG.md | 19 + .../react-row-detail-plugin/README.md | 19 + .../react-row-detail-plugin/package.json | 54 + .../react-row-detail-plugin/src/index.ts | 1 + .../react-row-detail-plugin/src/interfaces.ts | 15 + .../src/reactSlickRowDetailView.ts | 20 +- .../react-row-detail-plugin/tsconfig.json | 28 + .../vue-row-detail-plugin/CHANGELOG.md | 19 + .../vue-row-detail-plugin/README.md | 19 + .../vue-row-detail-plugin/package.json | 49 + .../vue-row-detail-plugin/src/index.ts | 1 + .../vue-row-detail-plugin/src/interfaces.ts | 16 + .../src/vueSlickRowDetailView.ts | 18 +- .../vue-row-detail-plugin/tsconfig.json | 28 + frameworks/angular-slickgrid/.oxlintrc.json | 3 +- frameworks/angular-slickgrid/CHANGELOG.md | 43 + frameworks/angular-slickgrid/README.md | 1 + frameworks/angular-slickgrid/angular.json | 33 +- frameworks/angular-slickgrid/docs/TOC.md | 18 +- .../docs/backend-services/OData.md | 4 +- .../graphql/GraphQL-Filtering.md | 4 +- .../docs/column-functionalities/cell-menu.md | 38 +- .../docs/column-functionalities/editors.md | 8 +- .../editors/date-editor-flatpickr.md | 71 - .../editors/select-dropdown-editor.md | 12 +- .../filters/autocomplete-filter.md | 1 - .../filters/compound-filters.md | 6 +- .../filters/range-filters.md | 12 +- .../filters/select-filter.md | 20 +- .../filters/single-search-filter.md | 14 +- .../filters/slider-filter.md | 2 +- .../docs/getting-started/quick-start.md | 81 +- .../Export-to-Text-File.md | 2 +- .../Header-Menu-&-Header-Buttons.md | 104 +- .../grid-functionalities/Row-based-edit.md | 2 +- ...tor-Modal.md => composite-editor-modal.md} | 4 +- .../{Context-Menu.md => context-menu.md} | 105 +- .../{Custom-Footer.md => custom-footer.md} | 0 ...-Tooltip-(plugin).md => custom-tooltip.md} | 16 + .../grid-functionalities/export-to-excel.md | 4 +- .../{Global-Options.md => global-options.md} | 31 +- .../{Grid-Menu.md => grid-menu.md} | 25 + .../grid-functionalities/grid-state-preset.md | 19 +- .../docs/grid-functionalities/row-detail.md | 31 +- .../grid-functionalities/row-selection.md | 29 +- ...le.md => localization-component-sample.md} | 2 +- ...md => localization-with-custom-locales.md} | 52 +- ....md => localization-with-ngx-translate.md} | 136 +- .../angular-slickgrid/docs/menu-slots.md | 524 + .../docs/migrations/migration-to-10.x.md | 344 + .../docs/migrations/migration-to-9.x.md | 18 +- .../angular-slickgrid/docs/styling/styling.md | 15 + frameworks/angular-slickgrid/package.json | 58 +- .../src/demos/app-routing.module.ts | 167 +- .../src/demos/app.component.html | 5 +- .../{app.module.ts => app.initializer.ts} | 11 +- .../examples/custom-angularComponentEditor.ts | 2 +- .../examples/custom-angularComponentFilter.ts | 5 +- .../src/demos/examples/custom-inputFilter.ts | 21 +- .../examples/editor-ng-select.component.ts | 2 +- .../demos/examples/example01.component.html | 10 + .../src/demos/examples/example01.component.ts | 5 +- .../src/demos/examples/example02.component.ts | 13 +- .../demos/examples/example03.component.html | 8 +- .../src/demos/examples/example03.component.ts | 13 +- .../demos/examples/example04.component.html | 10 +- .../src/demos/examples/example04.component.ts | 15 +- .../demos/examples/example05.component.html | 26 +- .../src/demos/examples/example05.component.ts | 56 +- .../demos/examples/example06.component.html | 18 +- .../src/demos/examples/example06.component.ts | 67 +- .../src/demos/examples/example07.component.ts | 4 +- .../demos/examples/example08.component.html | 2 +- .../src/demos/examples/example08.component.ts | 20 +- .../demos/examples/example09.component.html | 2 +- .../src/demos/examples/example09.component.ts | 17 +- .../demos/examples/example10.component.html | 26 +- .../src/demos/examples/example10.component.ts | 38 +- .../demos/examples/example11.component.html | 2 +- .../src/demos/examples/example11.component.ts | 6 +- .../demos/examples/example12.component.html | 2 +- .../src/demos/examples/example12.component.ts | 21 +- .../src/demos/examples/example13.component.ts | 7 +- .../demos/examples/example14.component.html | 22 +- .../src/demos/examples/example14.component.ts | 18 +- .../demos/examples/example15.component.html | 2 +- .../src/demos/examples/example15.component.ts | 16 +- .../demos/examples/example16.component.html | 2 +- .../src/demos/examples/example16.component.ts | 38 +- .../demos/examples/example17.component.html | 19 +- .../src/demos/examples/example17.component.ts | 11 +- .../demos/examples/example18.component.html | 29 +- .../src/demos/examples/example18.component.ts | 83 +- .../demos/examples/example19.component.html | 8 +- .../src/demos/examples/example19.component.ts | 16 +- .../src/demos/examples/example20.component.ts | 4 +- .../demos/examples/example21.component.html | 8 +- .../src/demos/examples/example21.component.ts | 13 +- .../src/demos/examples/example22.component.ts | 4 +- .../demos/examples/example23.component.html | 16 +- .../src/demos/examples/example23.component.ts | 32 +- .../demos/examples/example24.component.html | 2 +- .../src/demos/examples/example24.component.ts | 32 +- .../demos/examples/example25.component.html | 6 +- .../src/demos/examples/example25.component.ts | 34 +- .../demos/examples/example26.component.html | 8 +- .../src/demos/examples/example26.component.ts | 6 +- .../src/demos/examples/example27.component.ts | 10 +- .../src/demos/examples/example28.component.ts | 4 +- .../src/demos/examples/example29.component.ts | 11 +- .../src/demos/examples/example30.component.ts | 8 +- .../src/demos/examples/example32.component.ts | 10 +- .../demos/examples/example33.component.html | 20 +- .../src/demos/examples/example33.component.ts | 68 +- .../src/demos/examples/example34.component.ts | 4 +- .../demos/examples/example35.component.html | 6 +- .../src/demos/examples/example35.component.ts | 34 +- .../src/demos/examples/example36.component.ts | 4 +- .../src/demos/examples/example37.component.ts | 4 +- .../demos/examples/example38.component.html | 40 +- .../src/demos/examples/example38.component.ts | 69 +- .../demos/examples/example39.component.html | 32 +- .../src/demos/examples/example39.component.ts | 63 +- .../demos/examples/example40.component.html | 18 +- .../src/demos/examples/example40.component.ts | 6 +- .../src/demos/examples/example41.component.ts | 11 +- .../src/demos/examples/example42.component.ts | 4 +- .../src/demos/examples/example43.component.ts | 30 +- .../src/demos/examples/example44.component.ts | 4 +- .../examples/example45-detail.component.ts | 4 +- .../src/demos/examples/example45.component.ts | 8 +- .../src/demos/examples/example46.component.ts | 6 +- .../src/demos/examples/example47.component.ts | 11 +- .../demos/examples/example48.component.html | 2 +- .../src/demos/examples/example48.component.ts | 13 +- .../demos/examples/example49.component.html | 3 +- .../src/demos/examples/example49.component.ts | 11 +- .../demos/examples/example50.component.html | 2 +- .../src/demos/examples/example50.component.ts | 16 +- .../demos/examples/example51.component.html | 62 + .../demos/examples/example51.component.scss | 144 + .../src/demos/examples/example51.component.ts | 614 + .../examples/filter-ng-select.component.ts | 2 +- .../swt-common-grid-pagination.component.ts | 60 +- .../swt-common-grid-test.component.html | 2 +- .../swt-common-grid-test.component.ts | 48 +- .../examples/swt-common-grid.component.ts | 9 +- .../src/demos/examples/utilities.ts | 7 - .../angular-slickgrid.component.spec.ts | 153 +- .../angular-slickgrid.component.html | 5 - .../components/angular-slickgrid.component.ts | 183 +- .../__tests__/slickRowDetailView.spec.ts | 788 - .../src/library/extensions/index.ts | 1 - .../src/library/global-grid-options.ts | 282 +- .../angular-slickgrid/src/library/index.ts | 2 - .../modules/angular-slickgrid.module.spec.ts | 24 - .../modules/angular-slickgrid.module.ts | 26 - .../__tests__/angularUtilService.spec.ts | 5 +- .../services/__tests__/utilities.spec.ts | 9 +- .../library/services/angularUtil.service.ts | 11 +- .../src/library/services/container.service.ts | 4 +- .../library/services/translater.service.ts | 2 +- .../src/library/services/utilities.ts | 5 +- frameworks/angular-slickgrid/src/main.ts | 59 +- .../test/cypress/e2e/example10.cy.ts | 152 +- .../test/cypress/e2e/example14.cy.ts | 142 +- .../test/cypress/e2e/example16.cy.ts | 2 +- .../test/cypress/e2e/example18.cy.ts | 9 +- .../test/cypress/e2e/example23.cy.ts | 5 - .../test/cypress/e2e/example32.cy.ts | 32 +- .../test/cypress/e2e/example33.cy.ts | 86 +- .../test/cypress/e2e/example44.cy.ts | 98 + .../test/cypress/e2e/example48.cy.ts | 8 +- .../test/cypress/e2e/example51.cy.ts | 382 + .../test/cypress/support/commands.ts | 33 + .../test/rxjsResourceStub.ts | 2 +- .../angular-slickgrid/test/test-setup.ts | 19 +- frameworks/angular-slickgrid/tsconfig.json | 15 +- frameworks/angular-slickgrid/vite.config.mts | 6 +- frameworks/aurelia-slickgrid/CHANGELOG.md | 37 + frameworks/aurelia-slickgrid/README.md | 1 + frameworks/aurelia-slickgrid/docs/TOC.md | 2 +- .../docs/backend-services/OData.md | 4 +- .../graphql/GraphQL-Filtering.md | 4 +- .../docs/column-functionalities/cell-menu.md | 23 +- .../docs/column-functionalities/editors.md | 12 +- .../editors/date-editor-flatpickr.md | 72 - .../editors/select-dropdown-editor.md | 10 +- .../filters/autocomplete-filter.md | 1 - .../filters/compound-filters.md | 6 +- .../filters/custom-filter.md | 4 +- .../filters/range-filters.md | 12 +- .../filters/select-filter.md | 20 +- .../filters/single-search-filter.md | 4 +- .../filters/slider-filter.md | 2 +- .../docs/getting-started/quick-start.md | 4 +- .../grid-functionalities/Row-based-edit.md | 2 +- .../composite-editor-modal.md | 4 +- .../docs/grid-functionalities/context-menu.md | 105 +- .../grid-functionalities/custom-tooltip.md | 16 + .../grid-functionalities/export-to-excel.md | 13 +- .../export-to-text-file.md | 2 +- .../docs/grid-functionalities/grid-menu.md | 25 + .../grid-functionalities/grid-state-preset.md | 19 +- .../header-menu-header-buttons.md | 104 +- .../docs/grid-functionalities/row-detail.md | 30 +- .../grid-functionalities/row-selection.md | 45 +- .../localization-with-custom-locales.md | 6 +- .../docs/localization/localization.md | 10 +- .../aurelia-slickgrid/docs/menu-slots.md | 524 + .../docs/migrations/migration-to-10.x.md | 257 + .../docs/migrations/migration-to-9.x.md | 18 +- frameworks/aurelia-slickgrid/package.json | 4 +- .../src/custom-elements/aurelia-slickgrid.ts | 147 +- .../aurelia-slickgrid/src/extensions/index.ts | 1 - .../src/global-grid-options.ts | 293 +- frameworks/aurelia-slickgrid/src/index.ts | 3 - .../models/aureliaGridInstance.interface.ts | 4 + frameworks/aurelia-slickgrid/tsconfig.json | 1 - frameworks/slickgrid-react/CHANGELOG.md | 37 + frameworks/slickgrid-react/README.md | 5 +- frameworks/slickgrid-react/docs/TOC.md | 1 + .../docs/backend-services/OData.md | 4 +- .../graphql/GraphQL-Filtering.md | 4 +- .../docs/column-functionalities/cell-menu.md | 23 +- .../docs/column-functionalities/editors.md | 14 +- .../editors/select-dropdown-editor.md | 10 +- .../filters/compound-filters.md | 6 +- .../filters/custom-filter.md | 4 +- .../filters/range-filters.md | 12 +- .../filters/select-filter.md | 20 +- .../filters/single-search-filter.md | 4 +- .../filters/slider-filter.md | 2 +- .../grid-functionalities/Row-based-edit.md | 2 +- .../composite-editor-modal.md | 4 +- .../docs/grid-functionalities/context-menu.md | 105 +- .../grid-functionalities/custom-tooltip.md | 16 + .../grid-functionalities/export-to-excel.md | 26 +- .../export-to-text-file.md | 2 +- .../docs/grid-functionalities/grid-menu.md | 25 + .../grid-functionalities/grid-state-preset.md | 19 +- .../header-menu-header-buttons.md | 104 +- .../docs/grid-functionalities/row-detail.md | 67 +- .../grid-functionalities/row-selection.md | 45 +- .../docs/localization/localization.md | 8 +- frameworks/slickgrid-react/docs/menu-slots.md | 524 + .../docs/migrations/migration-to-10.x.md | 258 + .../docs/migrations/migration-to-9.x.md | 20 +- frameworks/slickgrid-react/package.json | 16 +- .../src/components/slickgrid-react.tsx | 107 +- .../src/global-grid-options.ts | 292 +- frameworks/slickgrid-react/src/index.ts | 14 +- .../slickgrid-react/src/services/index.ts | 1 + .../slickgrid-react/src/tsconfig.build.json | 2 +- frameworks/slickgrid-react/tsconfig.json | 1 - frameworks/slickgrid-vue/CHANGELOG.md | 37 + frameworks/slickgrid-vue/README.md | 12 +- frameworks/slickgrid-vue/docs/TOC.md | 1 + .../docs/backend-services/GraphQL.md | 4 +- .../docs/backend-services/OData.md | 4 +- .../custom-backend-service.md | 2 +- .../graphql/GraphQL-Filtering.md | 4 +- .../docs/column-functionalities/cell-menu.md | 27 +- .../docs/column-functionalities/editors.md | 20 +- .../editors/autocomplete-editor.md | 10 +- .../editors/select-dropdown-editor.md | 10 +- .../filters/compound-filters.md | 6 +- .../filters/custom-filter.md | 4 +- .../filters/input-filter.md | 10 +- .../filters/range-filters.md | 14 +- .../filters/select-filter.md | 20 +- .../filters/single-search-filter.md | 10 +- .../filters/slider-filter.md | 2 +- .../docs/column-functionalities/formatters.md | 2 +- .../docs/column-functionalities/sorting.md | 11 +- .../docs/developer-guides/csp-compliance.md | 2 +- .../docs/events/available-events.md | 8 +- .../docs/events/grid-dataview-events.md | 12 +- .../docs/getting-started/quick-start.md | 2 +- .../grid-functionalities/Row-based-edit.md | 2 +- .../add-update-highlight.md | 14 +- .../column-row-spanning.md | 2 +- .../composite-editor-modal.md | 24 +- .../docs/grid-functionalities/context-menu.md | 105 +- .../grid-functionalities/custom-tooltip.md | 17 + .../dynamic-item-metadata.md | 6 +- .../grid-functionalities/export-to-excel.md | 32 +- .../grid-functionalities/export-to-pdf.md | 2 +- .../export-to-text-file.md | 10 +- .../frozen-columns-rows.md | 10 +- .../grid-functionalities/grid-auto-resize.md | 17 +- .../docs/grid-functionalities/grid-menu.md | 25 + .../grid-functionalities/grid-state-preset.md | 29 +- .../grouping-aggregators.md | 6 +- .../header-footer-slots.md | 7 +- .../header-menu-header-buttons.md | 102 +- .../grid-functionalities/infinite-scroll.md | 12 +- .../docs/grid-functionalities/row-detail.md | 60 +- .../grid-functionalities/row-selection.md | 89 +- .../grid-functionalities/tree-data-grid.md | 4 +- .../docs/localization/localization.md | 10 +- frameworks/slickgrid-vue/docs/menu-slots.md | 524 + .../docs/migrations/migration-to-10.x.md | 272 + .../docs/migrations/migration-to-9.x.md | 18 +- .../slickgrid-dataview-objects.md | 14 +- frameworks/slickgrid-vue/package.json | 6 +- .../src/components/SlickgridVue.vue | 141 +- .../slickgrid-vue/src/global-grid-options.ts | 293 +- frameworks/slickgrid-vue/src/index.ts | 2 - lerna.json | 8 +- package.json | 45 +- packages/binding/CHANGELOG.md | 4 + packages/binding/package.json | 2 +- packages/common/CHANGELOG.md | 43 +- packages/common/build-watch.mjs | 3 +- packages/common/ensure-sass-dir.mjs | 8 + packages/common/package.json | 12 +- .../commonEditorFilterUtils.ts | 5 +- .../src/core/__tests__/slickDataView.spec.ts | 10 +- .../src/core/__tests__/slickGrid.spec.ts | 506 +- packages/common/src/core/slickGrid.ts | 246 +- .../__tests__/autocompleterEditor.spec.ts | 96 +- .../src/editors/__tests__/dateEditor.spec.ts | 63 +- .../editors/__tests__/longTextEditor.spec.ts | 17 - .../editors/__tests__/selectEditor.spec.ts | 25 +- .../editors/__tests__/sliderEditor.spec.ts | 66 +- .../common/src/editors/autocompleterEditor.ts | 11 +- packages/common/src/editors/dateEditor.ts | 18 +- packages/common/src/editors/editors.index.ts | 4 +- packages/common/src/editors/longTextEditor.ts | 2 +- packages/common/src/editors/selectEditor.ts | 11 +- packages/common/src/editors/sliderEditor.ts | 5 +- .../enums/{caseType.type.ts => case.type.ts} | 0 ...e.type.ts => compositeEditorModal.type.ts} | 0 packages/common/src/enums/delimiter.type.ts | 1 + .../common/src/enums/delimiterType.enum.ts | 13 - .../{emitterType.type.ts => emitter.type.ts} | 0 .../common/src/enums/extensionName.enum.ts | 22 +- .../{fieldType.enum.ts => field.type.ts} | 76 +- packages/common/src/enums/file.type.ts | 1 + packages/common/src/enums/fileType.enum.ts | 9 - ...ype.type.ts => filterMultiplePass.type.ts} | 0 ...ridStateType.enum.ts => gridState.type.ts} | 13 +- packages/common/src/enums/index.ts | 24 +- ...peratorString.type.ts => operator.type.ts} | 2 +- .../common/src/enums/operatorType.enum.ts | 77 - .../common/src/enums/slickPluginList.enum.ts | 14 +- .../common/src/enums/sortDirection.enum.ts | 7 - .../common/src/enums/sortDirection.type.ts | 1 + .../src/enums/sortDirectionString.type.ts | 1 - ...hangeType.ts => toggleStateChange.type.ts} | 0 .../__tests__/extensionUtility.spec.ts | 58 +- .../slickCellExcelCopyManager.spec.ts | 9 +- .../slickCellExternalCopyManager.spec.ts | 20 +- .../__tests__/slickCellMenu.plugin.spec.ts | 167 +- .../__tests__/slickCellSelectionModel.spec.ts | 732 - .../slickCheckboxSelectColumn.spec.ts | 22 +- .../__tests__/slickColumnPicker.spec.ts | 134 +- .../__tests__/slickContextMenu.spec.ts | 177 +- .../__tests__/slickGridMenu.spec.ts | 428 +- .../__tests__/slickHeaderButtons.spec.ts | 1 + .../__tests__/slickHeaderMenu.spec.ts | 453 +- .../slickHybridSelectionModel.spec.ts | 8 + .../__tests__/slickRowSelectionModel.spec.ts | 530 - .../src/extensions/extensionCommonUtils.ts | 58 +- .../common/src/extensions/extensionUtility.ts | 25 +- packages/common/src/extensions/index.ts | 3 +- .../common/src/extensions/menuBaseClass.ts | 181 +- .../common/src/extensions/slickAutoTooltip.ts | 2 +- .../extensions/slickCellExcelCopyManager.ts | 12 +- .../slickCellExternalCopyManager.ts | 6 +- .../common/src/extensions/slickCellMenu.ts | 7 +- .../src/extensions/slickCellRangeDecorator.ts | 2 +- .../src/extensions/slickCellRangeSelector.ts | 5 +- .../src/extensions/slickCellSelectionModel.ts | 329 - .../extensions/slickCheckboxSelectColumn.ts | 9 +- .../src/extensions/slickColumnPicker.ts | 6 +- .../common/src/extensions/slickContextMenu.ts | 159 +- .../src/extensions/slickDraggableGrouping.ts | 10 +- .../common/src/extensions/slickGridMenu.ts | 434 +- .../slickGroupItemMetadataProvider.ts | 2 +- .../src/extensions/slickHeaderButtons.ts | 4 +- .../common/src/extensions/slickHeaderMenu.ts | 281 +- .../extensions/slickHybridSelectionModel.ts | 6 +- .../src/extensions/slickRowBasedEdit.ts | 2 +- .../src/extensions/slickRowMoveManager.ts | 2 +- .../src/extensions/slickRowSelectionModel.ts | 275 - .../__tests__/booleanFilterCondition.spec.ts | 16 +- .../collectionSearchFilterCondition.spec.ts | 56 +- .../__tests__/dateEuroFilterCondition.spec.ts | 60 +- .../dateEuroShortFilterCondition.spec.ts | 80 +- .../__tests__/dateFilterCondition.spec.ts | 60 +- .../__tests__/dateIsoFilterCondition.spec.ts | 150 +- .../__tests__/dateUsFilterCondition.spec.ts | 64 +- .../dateUsShortFilterCondition.spec.ts | 64 +- .../__tests__/dateUtcFilterCondition.spec.ts | 60 +- .../__tests__/numberFilterCondition.spec.ts | 56 +- .../__tests__/objectFilterCondition.spec.ts | 32 +- .../__tests__/stringFilterCondition.spec.ts | 67 +- .../collectionSearchFilterCondition.ts | 2 +- .../filter-conditions/dateFilterCondition.ts | 15 +- .../filterConditionProcesses.ts | 24 +- .../src/filter-conditions/filterUtilities.ts | 6 +- .../numberFilterCondition.ts | 6 +- .../stringFilterCondition.ts | 23 +- .../__tests__/autocompleterFilter.spec.ts | 52 +- .../__tests__/compoundDateFilter.spec.ts | 44 +- .../__tests__/compoundInputFilter.spec.ts | 39 +- .../__tests__/compoundSliderFilter.spec.ts | 22 +- .../filters/__tests__/dateRangeFilter.spec.ts | 6 +- .../filters/__tests__/selectFilter.spec.ts | 58 +- .../__tests__/singleSelectFilter.spec.ts | 2 +- .../__tests__/singleSliderFilter.spec.ts | 65 +- .../__tests__/sliderRangeFilter.spec.ts | 123 +- .../common/src/filters/autocompleterFilter.ts | 16 +- packages/common/src/filters/dateFilter.ts | 30 +- packages/common/src/filters/filters.index.ts | 4 +- packages/common/src/filters/inputFilter.ts | 26 +- packages/common/src/filters/selectFilter.ts | 22 +- packages/common/src/filters/sliderFilter.ts | 47 +- .../__tests__/formatterUtilities.spec.ts | 18 +- .../src/formatters/formatterUtilities.ts | 8 +- .../common/src/formatters/formatters.index.ts | 8 +- packages/common/src/global-grid-options.ts | 16 +- packages/common/src/index.ts | 2 +- .../backendServiceOption.interface.ts | 2 +- .../interfaces/cellMenuOption.interface.ts | 10 +- .../collectionFilterBy.interface.ts | 15 +- .../interfaces/collectionOption.interface.ts | 2 +- .../interfaces/collectionSortBy.interface.ts | 2 +- .../common/src/interfaces/column.interface.ts | 12 +- .../src/interfaces/columnEditor.interface.ts | 8 +- .../src/interfaces/columnFilter.interface.ts | 14 +- ...mpositeEditorOpenDetailOption.interface.ts | 7 +- .../compositeEditorOption.interface.ts | 5 +- .../src/interfaces/contextMenu.interface.ts | 14 + .../interfaces/contextMenuOption.interface.ts | 26 +- .../src/interfaces/currentColumn.interface.ts | 3 + .../src/interfaces/currentFilter.interface.ts | 4 +- .../src/interfaces/currentSorter.interface.ts | 4 +- .../customTooltipOption.interface.ts | 14 + .../interfaces/excelExportOption.interface.ts | 4 +- .../interfaces/extensionModel.interface.ts | 4 +- .../interfaces/externalResource.interface.ts | 16 +- .../common/src/interfaces/filter.interface.ts | 8 +- .../interfaces/filterArguments.interface.ts | 4 +- .../interfaces/filterCallback.interface.ts | 4 +- .../interfaces/filterChangedArgs.interface.ts | 4 +- .../filterConditionOption.interface.ts | 10 +- .../src/interfaces/gridMenu.interface.ts | 16 + .../interfaces/gridMenuOption.interface.ts | 29 +- .../src/interfaces/gridOption.interface.ts | 48 +- .../interfaces/gridStateChange.interface.ts | 4 +- .../src/interfaces/headerMenu.interface.ts | 16 + .../interfaces/headerMenuOption.interface.ts | 34 +- packages/common/src/interfaces/index.ts | 1 + .../src/interfaces/interactions.interface.ts | 1 + .../menuFromCellCallbackArgs.interface.ts | 7 +- .../src/interfaces/menuItem.interface.ts | 29 +- .../src/interfaces/menuOption.interface.ts | 63 + .../interfaces/operatorDetail.interface.ts | 4 +- .../searchColumnFilter.interface.ts | 6 +- .../selectionModelOption.interface.ts | 18 +- .../slickRowDetailView.interface.ts | 2 +- .../interfaces/textExportOption.interface.ts | 10 +- .../interfaces/treeDataOption.interface.ts | 5 +- .../treeToggleStateChange.interface.ts | 2 +- .../__tests__/collection.service.spec.ts | 33 +- .../src/services/__tests__/dateUtils.spec.ts | 72 +- .../__tests__/extension.service.spec.ts | 219 +- .../services/__tests__/filter.service.spec.ts | 181 +- .../services/__tests__/grid.service.spec.ts | 156 +- .../__tests__/gridState.service.spec.ts | 282 +- .../__tests__/headerGrouping.service.spec.ts | 9 +- .../__tests__/resizer.service.spec.ts | 20 +- .../src/services/__tests__/rxjsFacade.spec.ts | 4 + .../services/__tests__/shared.service.spec.ts | 21 - .../services/__tests__/sort.service.spec.ts | 29 +- .../__tests__/treeData.service.spec.ts | 2 +- .../src/services/__tests__/utilities.spec.ts | 317 +- .../src/services/backendUtility.service.ts | 2 +- .../common/src/services/collection.service.ts | 22 +- packages/common/src/services/dateUtils.ts | 66 +- .../src/services/excelExport.service.ts | 2 +- .../common/src/services/extension.service.ts | 141 +- .../common/src/services/filter.service.ts | 51 +- packages/common/src/services/grid.service.ts | 81 +- .../common/src/services/gridState.service.ts | 132 +- .../src/services/headerGrouping.service.ts | 15 +- .../common/src/services/pdfExport.service.ts | 2 +- .../common/src/services/resizer.service.ts | 5 +- packages/common/src/services/rxjsFacade.ts | 2 + .../common/src/services/shared.service.ts | 10 - packages/common/src/services/sort.service.ts | 25 +- .../common/src/services/textExport.service.ts | 2 +- .../common/src/services/treeData.service.ts | 3 +- packages/common/src/services/utilities.ts | 204 +- .../dateEuroShortSortComparer.spec.ts | 8 +- .../__tests__/dateEuroSortComparer.spec.ts | 8 +- .../__tests__/dateIsoSortComparer.spec.ts | 12 +- .../__tests__/dateSortComparer.spec.ts | 8 +- .../__tests__/dateUsShortSortComparer.spec.ts | 8 +- .../__tests__/dateUsSortComparer.spec.ts | 8 +- .../__tests__/sortUtilities.spec.ts | 16 +- .../common/src/sortComparers/dateUtilities.ts | 6 +- .../src/sortComparers/sortComparers.index.ts | 6 +- .../common/src/sortComparers/sortUtilities.ts | 22 +- packages/common/src/styles/_variables.scss | 7 +- packages/common/src/styles/colors.scss | 159 +- packages/common/src/styles/slick-editors.scss | 3 + packages/common/src/styles/slick-grid.scss | 6 +- packages/common/src/styles/slick-plugins.scss | 10 +- .../src/styles/slickgrid-icons-svg-utils.scss | 2 - .../composite-editor-component/CHANGELOG.md | 14 + .../composite-editor-component/package.json | 2 +- .../slick-composite-editor.component.spec.ts | 168 +- .../src/slick-composite-editor.component.ts | 9 +- packages/custom-footer-component/CHANGELOG.md | 10 + packages/custom-footer-component/package.json | 2 +- .../src/slick-footer.component.ts | 3 +- packages/custom-tooltip-plugin/CHANGELOG.md | 11 + packages/custom-tooltip-plugin/package.json | 2 +- .../src/__tests__/slickCustomTooltip.spec.ts | 569 + .../src/slickCustomTooltip.ts | 263 +- packages/empty-warning-component/CHANGELOG.md | 6 + packages/empty-warning-component/package.json | 2 +- .../src/slick-empty-warning.component.ts | 1 + packages/event-pub-sub/CHANGELOG.md | 10 + packages/event-pub-sub/package.json | 2 +- .../src/eventPubSub.service.spec.ts | 17 +- .../event-pub-sub/src/eventPubSub.service.ts | 18 +- packages/event-pub-sub/src/index.ts | 2 +- ...Style.enum.ts => eventNamingStyle.type.ts} | 13 +- packages/excel-export/CHANGELOG.md | 16 + packages/excel-export/package.json | 4 +- .../src/excelExport.service.spec.ts | 30 +- .../excel-export/src/excelExport.service.ts | 17 +- packages/excel-export/src/excelUtils.spec.ts | 108 +- packages/excel-export/src/excelUtils.ts | 5 +- packages/graphql/CHANGELOG.md | 19 + packages/graphql/package.json | 2 +- .../graphqlFilteringOption.interface.ts | 4 +- .../graphqlSortingOption.interface.ts | 4 +- .../__tests__/graphql.service.spec.ts | 181 +- .../graphql/src/services/graphql.service.ts | 69 +- packages/odata/CHANGELOG.md | 19 + packages/odata/package.json | 2 +- .../odataSortingOption.interface.ts | 4 +- .../__tests__/grid-odata.service.spec.ts | 294 +- .../odata/src/services/grid-odata.service.ts | 76 +- packages/pagination-component/CHANGELOG.md | 4 + packages/pagination-component/package.json | 2 +- packages/pdf-export/CHANGELOG.md | 6 + packages/pdf-export/package.json | 4 +- packages/pdf-export/src/pdfExport.service.ts | 4 +- packages/row-detail-view-plugin/CHANGELOG.md | 10 + packages/row-detail-view-plugin/package.json | 2 +- .../src/slickRowDetailView.ts | 2 +- packages/rxjs-observable/CHANGELOG.md | 6 + packages/rxjs-observable/package.json | 2 +- .../rxjs-observable/src/rxjs-resource.spec.ts | 2 +- packages/rxjs-observable/src/rxjs.resource.ts | 2 +- packages/text-export/CHANGELOG.md | 16 + packages/text-export/package.json | 2 +- .../src/textExport.service.spec.ts | 19 +- .../text-export/src/textExport.service.ts | 15 +- packages/utils/CHANGELOG.md | 4 + packages/utils/package.json | 2 +- packages/vanilla-bundle/CHANGELOG.md | 22 + packages/vanilla-bundle/package.json | 3 +- .../__tests__/slick-vanilla-grid.spec.ts | 110 +- .../components/slick-vanilla-grid-bundle.ts | 112 +- packages/vanilla-force-bundle/CHANGELOG.md | 16 + packages/vanilla-force-bundle/compress.mjs | 5 +- packages/vanilla-force-bundle/package.json | 5 +- .../src/salesforce-global-grid-options.ts | 4 +- .../src/vanilla-force-bundle.ts | 4 +- pnpm-lock.yaml | 23068 +++++----------- pnpm-workspace.yaml | 53 +- scripts/checkBuild.mjs | 72 + test/cypress/e2e/example03.cy.ts | 9 +- test/cypress/e2e/example04.cy.ts | 18 +- test/cypress/e2e/example07.cy.ts | 6 +- test/cypress/e2e/example08.cy.ts | 259 +- test/cypress/e2e/example14.cy.ts | 32 +- test/cypress/e2e/example16.cy.ts | 106 +- test/cypress/e2e/example33.cy.ts | 98 + test/cypress/e2e/example37.cy.ts | 8 +- test/cypress/e2e/example40.cy.ts | 382 + test/cypress/support/commands.ts | 33 + test/rxjsResourceStub.ts | 2 +- test/vitest.config.mts | 1 + tsconfig.base.json | 3 +- 866 files changed, 31066 insertions(+), 28868 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 demos/aurelia/src/examples/slickgrid/example51.html create mode 100644 demos/aurelia/src/examples/slickgrid/example51.scss create mode 100644 demos/aurelia/src/examples/slickgrid/example51.ts create mode 100644 demos/aurelia/test/cypress/e2e/example51.cy.ts create mode 100644 demos/react/src/examples/slickgrid/Example51.tsx create mode 100644 demos/react/src/examples/slickgrid/example51.scss create mode 100644 demos/react/test/cypress/e2e/example51.cy.ts create mode 100644 demos/vanilla/src/examples/example40.html create mode 100644 demos/vanilla/src/examples/example40.scss create mode 100644 demos/vanilla/src/examples/example40.ts create mode 100644 demos/vue/src/components/Example51.vue create mode 100644 demos/vue/test/cypress/e2e/example51.cy.ts delete mode 100644 docs/column-functionalities/editors/Date-Editor-(flatpickr).md create mode 100644 docs/menu-slots.md create mode 100644 docs/migrations/migration-to-10.x.md create mode 100644 examples/vite-demo-vanilla-bundle/src/examples/example-header-menu-slots.html create mode 100644 frameworks-plugins/angular-row-detail-plugin/.oxlintrc.json create mode 100644 frameworks-plugins/angular-row-detail-plugin/CHANGELOG.md create mode 100644 frameworks-plugins/angular-row-detail-plugin/README.md create mode 100644 frameworks-plugins/angular-row-detail-plugin/package.json rename frameworks/angular-slickgrid/src/library/extensions/slickRowDetailView.ts => frameworks-plugins/angular-row-detail-plugin/src/angularSlickRowDetailView.ts (90%) create mode 100644 frameworks-plugins/angular-row-detail-plugin/src/index.ts create mode 100644 frameworks-plugins/angular-row-detail-plugin/src/interfaces.ts create mode 100644 frameworks-plugins/angular-row-detail-plugin/tsconfig.json create mode 100644 frameworks-plugins/aurelia-row-detail-plugin/CHANGELOG.md create mode 100644 frameworks-plugins/aurelia-row-detail-plugin/README.md create mode 100644 frameworks-plugins/aurelia-row-detail-plugin/package.json rename frameworks/aurelia-slickgrid/src/extensions/slickRowDetailView.ts => frameworks-plugins/aurelia-row-detail-plugin/src/aureliaSlickRowDetailView.ts (93%) create mode 100644 frameworks-plugins/aurelia-row-detail-plugin/src/index.ts create mode 100644 frameworks-plugins/aurelia-row-detail-plugin/src/interfaces.ts create mode 100644 frameworks-plugins/aurelia-row-detail-plugin/tsconfig.json create mode 100644 frameworks-plugins/react-row-detail-plugin/CHANGELOG.md create mode 100644 frameworks-plugins/react-row-detail-plugin/README.md create mode 100644 frameworks-plugins/react-row-detail-plugin/package.json create mode 100644 frameworks-plugins/react-row-detail-plugin/src/index.ts create mode 100644 frameworks-plugins/react-row-detail-plugin/src/interfaces.ts rename frameworks/slickgrid-react/src/extensions/slickRowDetailView.ts => frameworks-plugins/react-row-detail-plugin/src/reactSlickRowDetailView.ts (94%) create mode 100644 frameworks-plugins/react-row-detail-plugin/tsconfig.json create mode 100644 frameworks-plugins/vue-row-detail-plugin/CHANGELOG.md create mode 100644 frameworks-plugins/vue-row-detail-plugin/README.md create mode 100644 frameworks-plugins/vue-row-detail-plugin/package.json create mode 100644 frameworks-plugins/vue-row-detail-plugin/src/index.ts create mode 100644 frameworks-plugins/vue-row-detail-plugin/src/interfaces.ts rename frameworks/slickgrid-vue/src/extensions/slickRowDetailView.ts => frameworks-plugins/vue-row-detail-plugin/src/vueSlickRowDetailView.ts (95%) create mode 100644 frameworks-plugins/vue-row-detail-plugin/tsconfig.json delete mode 100644 frameworks/angular-slickgrid/docs/column-functionalities/editors/date-editor-flatpickr.md rename frameworks/angular-slickgrid/docs/grid-functionalities/{Composite-Editor-Modal.md => composite-editor-modal.md} (98%) rename frameworks/angular-slickgrid/docs/grid-functionalities/{Context-Menu.md => context-menu.md} (77%) rename frameworks/angular-slickgrid/docs/grid-functionalities/{Custom-Footer.md => custom-footer.md} (100%) rename frameworks/angular-slickgrid/docs/grid-functionalities/{Custom-Tooltip-(plugin).md => custom-tooltip.md} (95%) rename frameworks/angular-slickgrid/docs/grid-functionalities/{Global-Options.md => global-options.md} (54%) rename frameworks/angular-slickgrid/docs/grid-functionalities/{Grid-Menu.md => grid-menu.md} (82%) rename frameworks/angular-slickgrid/docs/localization/{Localization---Component-Sample.md => localization-component-sample.md} (98%) rename frameworks/angular-slickgrid/docs/localization/{Localization-with-Custom-Locales.md => localization-with-custom-locales.md} (70%) rename frameworks/angular-slickgrid/docs/localization/{Localization-with-ngx-translate.md => localization-with-ngx-translate.md} (56%) create mode 100644 frameworks/angular-slickgrid/docs/menu-slots.md create mode 100644 frameworks/angular-slickgrid/docs/migrations/migration-to-10.x.md rename frameworks/angular-slickgrid/src/demos/{app.module.ts => app.initializer.ts} (72%) create mode 100644 frameworks/angular-slickgrid/src/demos/examples/example51.component.html create mode 100644 frameworks/angular-slickgrid/src/demos/examples/example51.component.scss create mode 100644 frameworks/angular-slickgrid/src/demos/examples/example51.component.ts delete mode 100644 frameworks/angular-slickgrid/src/library/components/angular-slickgrid.component.html delete mode 100644 frameworks/angular-slickgrid/src/library/extensions/__tests__/slickRowDetailView.spec.ts delete mode 100644 frameworks/angular-slickgrid/src/library/extensions/index.ts delete mode 100644 frameworks/angular-slickgrid/src/library/modules/angular-slickgrid.module.spec.ts delete mode 100644 frameworks/angular-slickgrid/src/library/modules/angular-slickgrid.module.ts create mode 100644 frameworks/angular-slickgrid/test/cypress/e2e/example51.cy.ts delete mode 100644 frameworks/aurelia-slickgrid/docs/column-functionalities/editors/date-editor-flatpickr.md create mode 100644 frameworks/aurelia-slickgrid/docs/menu-slots.md create mode 100644 frameworks/aurelia-slickgrid/docs/migrations/migration-to-10.x.md delete mode 100644 frameworks/aurelia-slickgrid/src/extensions/index.ts create mode 100644 frameworks/slickgrid-react/docs/menu-slots.md create mode 100644 frameworks/slickgrid-react/docs/migrations/migration-to-10.x.md create mode 100644 frameworks/slickgrid-vue/docs/menu-slots.md create mode 100644 frameworks/slickgrid-vue/docs/migrations/migration-to-10.x.md create mode 100644 packages/common/ensure-sass-dir.mjs rename packages/common/src/enums/{caseType.type.ts => case.type.ts} (100%) rename packages/common/src/enums/{compositeEditorModalType.type.ts => compositeEditorModal.type.ts} (100%) create mode 100644 packages/common/src/enums/delimiter.type.ts delete mode 100644 packages/common/src/enums/delimiterType.enum.ts rename packages/common/src/enums/{emitterType.type.ts => emitter.type.ts} (100%) rename packages/common/src/enums/{fieldType.enum.ts => field.type.ts} (59%) create mode 100644 packages/common/src/enums/file.type.ts delete mode 100644 packages/common/src/enums/fileType.enum.ts rename packages/common/src/enums/{filterMultiplePassType.type.ts => filterMultiplePass.type.ts} (100%) rename packages/common/src/enums/{gridStateType.enum.ts => gridState.type.ts} (73%) rename packages/common/src/enums/{operatorString.type.ts => operator.type.ts} (99%) delete mode 100644 packages/common/src/enums/operatorType.enum.ts delete mode 100644 packages/common/src/enums/sortDirection.enum.ts create mode 100644 packages/common/src/enums/sortDirection.type.ts delete mode 100644 packages/common/src/enums/sortDirectionString.type.ts rename packages/common/src/enums/{toggleStateChangeType.ts => toggleStateChange.type.ts} (100%) delete mode 100644 packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts delete mode 100644 packages/common/src/extensions/__tests__/slickRowSelectionModel.spec.ts delete mode 100644 packages/common/src/extensions/slickCellSelectionModel.ts delete mode 100644 packages/common/src/extensions/slickRowSelectionModel.ts create mode 100644 packages/common/src/interfaces/menuOption.interface.ts rename packages/event-pub-sub/src/types/{eventNamingStyle.enum.ts => eventNamingStyle.type.ts} (73%) create mode 100644 scripts/checkBuild.mjs create mode 100644 test/cypress/e2e/example40.cy.ts 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:
  • 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. + +

      +
      + +
      +
      + + + + +
      +
      + +
      + + +
      +
      diff --git a/demos/aurelia/src/examples/slickgrid/example51.scss b/demos/aurelia/src/examples/slickgrid/example51.scss new file mode 100644 index 0000000000..539ad2ecd2 --- /dev/null +++ b/demos/aurelia/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/aurelia/src/examples/slickgrid/example51.ts b/demos/aurelia/src/examples/slickgrid/example51.ts new file mode 100644 index 0000000000..32e54a15de --- /dev/null +++ b/demos/aurelia/src/examples/slickgrid/example51.ts @@ -0,0 +1,611 @@ +import { format as tempoFormat } from '@formkit/tempo'; +import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; +import { + Aggregators, + createDomElement, + Filters, + Formatters, + SortComparers, + SortDirectionNumber, + type AureliaGridInstance, + type Column, + type GridOption, + type Grouping, + type MenuCommandItem, +} from 'aurelia-slickgrid'; +import './example51.scss'; + +const NB_ITEMS = 2000; + +interface ReportItem { + id: number; + title: string; + duration: number; + cost: number; + percentComplete: number; + start: Date; + finish: Date; + action?: string; +} + +export class Example51 { + aureliaGrid!: AureliaGridInstance; + columnDefinitions: Column[] = []; + gridOptions!: GridOption; + dataset!: ReportItem[]; + hideSubTitle = false; + + constructor() { + // define the grid options & columns and then create the grid itself + this.defineGrid(); + } + + attached() { + // mock some data (different in each dataset) + this.dataset = this.loadData(NB_ITEMS); + } + + aureliaGridReady(aureliaGrid: AureliaGridInstance) { + this.aureliaGrid = aureliaGrid; + } + + defineGrid() { + this.columnDefinitions = [ + { + 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) => ` + + `, + }, + { + command: 'sort-desc', + title: 'Sort Descending', + positionOrder: 51, + // Slot renderer using native DOM elements + slotRenderer: () => { + const menuItemElm = createDomElement('div', { className: 'menu-item' }); + const iconElm = createDomElement('i', { className: 'mdi mdi-sort-descending menu-item-icon' }); + const menuItemLabelElm = createDomElement('span', { className: 'menu-item-label', textContent: 'Sort Descending' }); + const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'Alt+↓' }); + menuItemElm.appendChild(iconElm); + menuItemElm.appendChild(menuItemLabelElm); + menuItemElm.appendChild(kbdElm); + return menuItemElm; + }, + }, + ], + }, + }, + }, + { + id: 'duration', + name: 'Duration', + field: 'duration', + sortable: true, + filterable: true, + minWidth: 100, + // Demo: Header Menu with Slot - showing badge and status dot + header: { + menu: { + commandItems: [ + { + command: 'column-resize-by-content', + title: 'Resize by Content', + positionOrder: 47, + // Slot renderer with badge + slotRenderer: () => ` + + `, + }, + { divider: true, command: '', positionOrder: 48 }, + { + command: 'sort-asc', + title: 'Sort Ascending', + iconCssClass: 'mdi mdi-sort-ascending', + positionOrder: 50, + }, + { + command: 'sort-desc', + title: 'Sort Descending', + iconCssClass: 'mdi mdi-sort-descending', + positionOrder: 51, + }, + { divider: true, command: '', positionOrder: 52 }, + { + command: 'clear-sort', + title: 'Remove Sort', + positionOrder: 58, + // Slot renderer with status indicator + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'start', + name: 'Start', + field: 'start', + sortable: true, + formatter: Formatters.dateIso, + filterable: true, + filter: { model: Filters.compoundDate }, + minWidth: 100, + }, + { + id: 'finish', + name: 'Finish', + field: 'finish', + sortable: true, + formatter: Formatters.dateIso, + filterable: true, + filter: { model: Filters.dateRange }, + minWidth: 100, + }, + { + id: 'cost', + name: 'Cost', + field: 'cost', + width: 90, + sortable: true, + filterable: true, + formatter: Formatters.dollar, + // Demo: Header Menu with Slot - showing slotRenderer with callback (item, args) + header: { + menu: { + commandItems: [ + { + command: 'custom-action', + title: 'Advanced Export', + // Demo: Native HTMLElement with event listeners using slotRenderer (full DOM control) + slotRenderer: (cmdItem) => { + // you can use `createDomElement()` from Slickgrid for easier DOM element creation + const containerDiv = createDomElement('div', { className: 'menu-item' }); + const iconDiv = createDomElement('div', { className: 'advanced-export-icon', textContent: '📊' }); + const textSpan = createDomElement('span', { textContent: cmdItem.title || '', style: { flex: '1' } }); + const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'Ctrl+E' }); + containerDiv.appendChild(iconDiv); + containerDiv.appendChild(textSpan); + containerDiv.appendChild(kbdElm); + + // Add native event listeners for hover effects + containerDiv.addEventListener('mouseover', () => { + iconDiv.style.transform = 'scale(1.15)'; + iconDiv.style.background = 'linear-gradient(135deg, #d8dcef 0%, #ffffff 100%)'; + containerDiv.parentElement!.style.backgroundColor = '#854685'; + containerDiv.parentElement!.title = `📈 Export timestamp: ${tempoFormat(new Date(), 'YYYY-MM-DD hh:mm:ss a')}`; + containerDiv.style.color = 'white'; + containerDiv.querySelector('.key-hint')!.style.color = 'black'; + }); + containerDiv.addEventListener('mouseout', () => { + iconDiv.style.transform = 'scale(1)'; + iconDiv.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; + containerDiv.parentElement!.style.backgroundColor = 'white'; + containerDiv.style.color = 'black'; + document.querySelector('.export-timestamp')?.remove(); + }); + + return containerDiv; + }, + action: () => { + alert('Custom export action triggered!'); + }, + }, + { divider: true, command: '' }, + { + command: 'filter-column', + title: 'Filter Column', + // Slot renderer with status indicator and beta badge + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'percentComplete', + name: '% Complete', + field: 'percentComplete', + sortable: true, + filterable: true, + type: 'number', + filter: { model: Filters.slider, operator: '>=' }, + // Demo: Header Menu with Slot - showing interactive element (checkbox) + header: { + menu: { + commandItems: [ + { + command: 'recalc', + title: 'Recalculate', + iconCssClass: 'mdi mdi-refresh', + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'action', + name: 'Action', + field: 'action', + width: 70, + minWidth: 70, + maxWidth: 70, + cssClass: 'justify-center flex', + formatter: () => + `
      `, + excludeFromExport: true, + // Demo: Cell Menu with slot examples (demonstrating defaultMenuItemRenderer at menu level) + cellMenu: { + hideCloseButton: false, + commandTitle: 'Cell Actions', + // Demo: Menu-level default renderer that applies to all items unless overridden + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + commandItems: [ + { + command: 'copy-cell', + title: 'Copy Cell Value', + iconCssClass: 'mdi mdi-content-copy', + action: (_e, args) => { + console.log('Copy cell value:', args.dataContext[args.column.field]); + alert(`Copied: ${args.dataContext[args.column.field]}`); + }, + }, + 'divider', + { + command: 'export-row', + title: 'Export Row', + iconCssClass: 'mdi mdi-download', + action: (_e, args) => { + console.log('Export row:', args.dataContext); + alert(`Export row #${args.dataContext.id}`); + }, + }, + { + command: 'export', + title: 'Export', + iconCssClass: 'mdi mdi-download', + commandItems: [ + { + command: 'export-excel', + title: 'Export as Excel', + iconCssClass: 'mdi mdi-file-excel-outline text-success', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to Excel`); + }, + }, + { + command: 'export-csv', + title: 'Export as CSV', + iconCssClass: 'mdi mdi-file-document-outline', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to CSV`); + }, + }, + { + command: 'export-pdf', + title: 'Export as PDF', + iconCssClass: 'mdi mdi-file-pdf-outline text-red', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to PDF`); + }, + }, + ], + }, + { divider: true, command: '' }, + { + command: 'edit-row', + title: 'Edit Row', + // Individual slotRenderer overrides the defaultMenuItemRenderer + slotRenderer: (_item, args) => ` + + `, + action: (_e, args) => { + console.log('Edit row:', args.dataContext); + alert(`Edit row #${args.dataContext.id}`); + }, + }, + { + command: 'delete-row', + title: 'Delete Row', + iconCssClass: 'mdi mdi-delete text-danger', + action: (_event, args) => { + const dataContext = args.dataContext; + if (confirm(`Do you really want to delete row (${args.row! + 1}) with "${dataContext.title}"`)) { + this.aureliaGrid?.gridService.deleteItemById(dataContext.id); + } + }, + }, + ], + }, + }, + ]; + + this.gridOptions = { + autoResize: { + container: '#demo-container', + }, + enableAutoResize: true, + enableCellNavigation: true, + enableFiltering: true, + enableSorting: true, + enableGrouping: true, + + // Header Menu with slots (already configured in columns above) + enableHeaderMenu: true, + headerMenu: { + // hideCommands: ['column-resize-by-content', 'clear-sort'], + + // Demo: Menu-level default renderer for all header menu items + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + }, + + // Cell Menu with slots (configured in the Action column above) + enableCellMenu: true, + + // Context Menu with slot examples + enableContextMenu: true, + contextMenu: { + // hideCommands: ['clear-grouping', 'copy'], + + // build your command items list + // spread built-in commands and optionally filter/sort them however you want + commandListBuilder: (builtInItems) => { + // commandItems.sort((a, b) => (a === 'divider' || b === 'divider' ? 0 : a.title! > b.title! ? -1 : 1)); + return [ + // filter commands if you want + // ...builtInItems.filter((x) => x !== 'divider' && x.command !== 'copy' && x.command !== 'clear-grouping'), + { + command: 'edit-cell', + title: 'Edit Cell', + // Demo: Individual slotRenderer overrides the menu's defaultMenuItemRenderer + slotRenderer: (cmdItem) => { + // you can use `createDomElement()` from Slickgrid for easier DOM element creation + const containerDiv = createDomElement('div', { className: 'menu-item' }); + const iconDiv = createDomElement('div', { className: 'edit-cell-icon', textContent: '✎' }); + const textSpan = createDomElement('span', { textContent: cmdItem.title || '', style: { flex: '1' } }); + const kbdElm = createDomElement('kbd', { className: 'edit-cell', textContent: 'F2' }); + containerDiv.appendChild(iconDiv); + containerDiv.appendChild(textSpan); + containerDiv.appendChild(kbdElm); + + // Native event listeners for interactive effects + containerDiv.addEventListener('mouseover', () => { + iconDiv.style.transform = 'rotate(15deg) scale(1.1)'; + iconDiv.style.boxShadow = '0 2px 8px rgba(0,200,83,0.4)'; + }); + containerDiv.addEventListener('mouseout', () => { + iconDiv.style.transform = 'rotate(0deg) scale(1)'; + iconDiv.style.boxShadow = 'none'; + }); + + return containerDiv; + }, + action: () => alert('Edit cell'), + }, + ...builtInItems, + { divider: true, command: '' }, + { + command: 'export', + title: 'Export', + iconCssClass: 'mdi mdi-download', + commandItems: [ + { + command: 'export-excel', + title: 'Export as Excel', + iconCssClass: 'mdi mdi-file-excel-outline text-success', + action: () => alert('Export to Excel'), + }, + { + command: 'export-csv', + title: 'Export as CSV', + iconCssClass: 'mdi mdi-file-document-outline', + action: () => alert('Export to CSV'), + }, + { + command: 'export-pdf', + title: 'Export as PDF', + iconCssClass: 'mdi mdi-file-pdf-outline text-danger', + action: () => alert('Export to PDF'), + }, + ], + }, + { divider: true, command: '' }, + { + command: 'delete-row', + title: 'Delete Row', + iconCssClass: 'mdi mdi-delete text-danger', + action: () => alert('Delete row'), + }, + ] as Array; + }, + // Demo: Menu-level default renderer for context menu items + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + }, + + // Grid Menu with slot examples (demonstrating defaultMenuItemRenderer at menu level) + enableGridMenu: true, + gridMenu: { + // hideCommands: ['toggle-preheader', 'toggle-filter'], + + // Demo: Menu-level default renderer that applies to all items (can be overridden per item with slotRenderer) + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + commandListBuilder: (builtInItems) => { + return [ + ...builtInItems, + { divider: true, command: '' }, + { + command: 'export-excel', + title: 'Export to Excel', + iconCssClass: 'mdi mdi-file-excel-outline', + action: () => alert('Export to Excel'), + }, + { + command: 'export-csv', + title: 'Export to CSV', + iconCssClass: 'mdi mdi-download', + // Individual slotRenderer overrides the defaultMenuItemRenderer for this item + slotRenderer: (cmdItem) => ` + + `, + 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.aureliaGrid?.dataView?.setGrouping([]); + } + + collapseAllGroups() { + this.aureliaGrid?.dataView?.collapseAllGroups(); + } + + expandAllGroups() { + this.aureliaGrid?.dataView?.expandAllGroups(); + } + + groupByDuration() { + // you need to manually add the sort icon(s) in UI + this.aureliaGrid?.slickGrid?.setSortColumns([{ columnId: 'duration', sortAsc: true }]); + this.aureliaGrid?.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.aureliaGrid?.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.hideSubTitle = !this.hideSubTitle; + const action = this.hideSubTitle ? 'add' : 'remove'; + document.querySelector('.subtitle')?.classList[action]('hidden'); + this.aureliaGrid.resizerService.resizeGrid(0); + } +} diff --git a/demos/aurelia/src/examples/slickgrid/example6.ts b/demos/aurelia/src/examples/slickgrid/example6.ts index 84d339945a..1214ebfec7 100644 --- a/demos/aurelia/src/examples/slickgrid/example6.ts +++ b/demos/aurelia/src/examples/slickgrid/example6.ts @@ -10,8 +10,6 @@ import { resolve } from 'aurelia'; import { Filters, Formatters, - OperatorType, - SortDirection, type AureliaGridInstance, type Column, type CursorPageInfo, @@ -182,7 +180,7 @@ export class Example6 { gridHeight: 200, gridWidth: 900, 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' } }, }, gridMenu: { @@ -205,18 +203,18 @@ export class Example6 { ], filters: [ // 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' }, ], sorters: [ // 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' }, ], pagination: { pageNumber: this.isWithCursor ? 1 : 2, pageSize: 20 }, // if cursor based, start at page 1 }, @@ -233,7 +231,7 @@ export class Example6 { }, ], filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => { - 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. @@ -360,11 +358,11 @@ export class Example6 { // we can Set Filters Dynamically (or different filters) afterward through the FilterService this.aureliaGrid.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' }, ]); } @@ -383,18 +381,18 @@ export class Example6 { this.aureliaGrid.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' }, ]); this.aureliaGrid.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(() => { this.aureliaGrid.paginationService?.changeItemPerPage(20); diff --git a/demos/aurelia/src/examples/slickgrid/example9.ts b/demos/aurelia/src/examples/slickgrid/example9.ts index 6532536593..e2f495c0f4 100644 --- a/demos/aurelia/src/examples/slickgrid/example9.ts +++ b/demos/aurelia/src/examples/slickgrid/example9.ts @@ -1,7 +1,6 @@ import { I18N } from '@aurelia/i18n'; import { resolve } from 'aurelia'; import { - ExtensionName, Filters, Formatters, type AureliaGridInstance, @@ -297,7 +296,7 @@ export class Example9 { toggleGridMenu(e: MouseEvent) { if (this.aureliaGrid?.extensionService) { - const gridMenuInstance = this.aureliaGrid.extensionService.getExtensionInstanceByName(ExtensionName.gridMenu); + const gridMenuInstance = this.aureliaGrid.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/demos/aurelia/src/examples/slickgrid/utilities.ts b/demos/aurelia/src/examples/slickgrid/utilities.ts index b4c1bff6ef..ae78294c55 100644 --- a/demos/aurelia/src/examples/slickgrid/utilities.ts +++ b/demos/aurelia/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/aurelia/src/my-app.ts b/demos/aurelia/src/my-app.ts index 933828aa59..b2bd69762c 100644 --- a/demos/aurelia/src/my-app.ts +++ b/demos/aurelia/src/my-app.ts @@ -57,6 +57,7 @@ const myRoutes: Routeable[] = [ { path: 'example48', component: () => import('./examples/slickgrid/example48.js'), title: '48- Hybrid Selection Model' }, { path: 'example49', component: () => import('./examples/slickgrid/example49.js'), title: '49- Spreadsheet Drag-Fill' }, { path: 'example50', component: () => import('./examples/slickgrid/example50.js'), title: '50- Master/Detail Grids' }, + { path: 'example51', component: () => import('./examples/slickgrid/example51.js'), title: '51- Menus with Slots' }, { path: 'home', component: () => import('./home-page.js'), title: 'Home' }, ]; @route({ diff --git a/demos/aurelia/test/cypress/e2e/example10.cy.ts b/demos/aurelia/test/cypress/e2e/example10.cy.ts index 06d6048e1d..22544dc7ab 100644 --- a/demos/aurelia/test/cypress/e2e/example10.cy.ts +++ b/demos/aurelia/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/aurelia/test/cypress/e2e/example14.cy.ts b/demos/aurelia/test/cypress/e2e/example14.cy.ts index 402545254b..91f7db34b6 100644 --- a/demos/aurelia/test/cypress/e2e/example14.cy.ts +++ b/demos/aurelia/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/aurelia/test/cypress/e2e/example16.cy.ts b/demos/aurelia/test/cypress/e2e/example16.cy.ts index a1a2073bb5..78dabd7ae7 100644 --- a/demos/aurelia/test/cypress/e2e/example16.cy.ts +++ b/demos/aurelia/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/aurelia/test/cypress/e2e/example18.cy.ts b/demos/aurelia/test/cypress/e2e/example18.cy.ts index 745a31f7e4..eae744afce 100644 --- a/demos/aurelia/test/cypress/e2e/example18.cy.ts +++ b/demos/aurelia/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/aurelia/test/cypress/e2e/example32.cy.ts b/demos/aurelia/test/cypress/e2e/example32.cy.ts index c2c8d5f769..1c8eb7b69e 100644 --- a/demos/aurelia/test/cypress/e2e/example32.cy.ts +++ b/demos/aurelia/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/aurelia/test/cypress/e2e/example33.cy.ts b/demos/aurelia/test/cypress/e2e/example33.cy.ts index fe0cec43d7..d4b06f03f6 100644 --- a/demos/aurelia/test/cypress/e2e/example33.cy.ts +++ b/demos/aurelia/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/aurelia/test/cypress/e2e/example44.cy.ts b/demos/aurelia/test/cypress/e2e/example44.cy.ts index 6f70275d7a..f399ed0364 100644 --- a/demos/aurelia/test/cypress/e2e/example44.cy.ts +++ b/demos/aurelia/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/aurelia/test/cypress/e2e/example48.cy.ts b/demos/aurelia/test/cypress/e2e/example48.cy.ts index a9df9df6b5..40d990bb34 100644 --- a/demos/aurelia/test/cypress/e2e/example48.cy.ts +++ b/demos/aurelia/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/aurelia/test/cypress/e2e/example51.cy.ts b/demos/aurelia/test/cypress/e2e/example51.cy.ts new file mode 100644 index 0000000000..80e5148c97 --- /dev/null +++ b/demos/aurelia/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/aurelia/test/cypress/support/commands.ts b/demos/aurelia/test/cypress/support/commands.ts index a35be6228b..3be5a3685b 100644 --- a/demos/aurelia/test/cypress/support/commands.ts +++ b/demos/aurelia/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/CHANGELOG.md b/demos/react/CHANGELOG.md index e5686b2b82..e6e4818a5d 100644 --- a/demos/react/CHANGELOG.md +++ b/demos/react/CHANGELOG.md @@ -4,6 +4,39 @@ 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 + +* **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 +* **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/react/package.json b/demos/react/package.json index 36c35c1aec..606d096eb3 100644 --- a/demos/react/package.json +++ b/demos/react/package.json @@ -1,7 +1,7 @@ { "name": "slickgrid-react-demo", "private": true, - "version": "9.13.0", + "version": "10.0.0-beta.0", "description": "Slickgrid-React demo", "keywords": [ "react", @@ -31,14 +31,15 @@ "@slickgrid-universal/odata": "workspace:*", "@slickgrid-universal/pagination-component": "workspace:*", "@slickgrid-universal/pdf-export": "workspace:*", + "@slickgrid-universal/react-row-detail-plugin": "workspace:*", "@slickgrid-universal/row-detail-view-plugin": "workspace:*", "@slickgrid-universal/rxjs-observable": "workspace:*", "@slickgrid-universal/text-export": "workspace:*", "bootstrap": "catalog:", "dompurify": "catalog:", "i18next": "catalog:", - "react": "^19.2.4", - "react-dom": "^19.2.4", + "react": "catalog:", + "react-dom": "catalog:", "react-i18next": "^16.5.4", "react-router": "^7.13.0", "slickgrid-react": "workspace:*" @@ -54,8 +55,8 @@ "@popperjs/core": "catalog:", "@types/fnando__sparkline": "catalog:", "@types/node": "catalog:", - "@types/react": "^19.2.10", - "@types/react-dom": "^19.2.3", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", "@types/sortablejs": "catalog:", "@vitejs/plugin-react": "catalog:", "cross-env": "catalog:", @@ -65,7 +66,7 @@ "rxjs": "catalog:", "sass": "catalog:", "typescript": "catalog:", - "vite": "catalog:vite7", + "vite": "catalog:", "vite-tsconfig-paths": "catalog:" } } diff --git a/demos/react/src/examples/slickgrid/App.tsx b/demos/react/src/examples/slickgrid/App.tsx index ac5a4381e0..b7fb17f03d 100644 --- a/demos/react/src/examples/slickgrid/App.tsx +++ b/demos/react/src/examples/slickgrid/App.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import { Routes as BaseRoutes, Link, Navigate, Route, useLocation } from 'react-router'; import { NavBar } from '../../NavBar.js'; + import Example1 from './Example1.js'; import Example2 from './Example2.js'; import Example3 from './Example3.js'; @@ -50,6 +51,7 @@ import Example47 from './Example47.js'; import Example48 from './Example48.js'; import Example49 from './Example49.js'; import Example50 from './Example50.js'; +import Example51 from './Example51.js'; const routes: Array<{ path: string; route: string; component: any; title: string }> = [ { path: 'example1', route: '/example1', component: , title: '1- Basic Grid / 2 Grids' }, @@ -101,6 +103,7 @@ const routes: Array<{ path: string; route: string; component: any; title: string { path: 'example48', route: '/example48', component: , title: '48- Hybrid Selection Model' }, { path: 'example49', route: '/example49', component: , title: '49- Spreadsheet Drag-Fill' }, { path: 'example50', route: '/example50', component: , title: '50- Master/Detail Grids' }, + { path: 'example51', route: '/example51', component: , title: '51- Menus with Slots' }, ]; export default function Routes() { diff --git a/demos/react/src/examples/slickgrid/Example10.tsx b/demos/react/src/examples/slickgrid/Example10.tsx index 7b97c3d940..4a0f23deaf 100644 --- a/demos/react/src/examples/slickgrid/Example10.tsx +++ b/demos/react/src/examples/slickgrid/Example10.tsx @@ -150,7 +150,7 @@ const Example10: React.FC = () => { const gridOptions1: GridOption = { enableAutoResize: false, enableCellNavigation: true, - enableRowSelection: true, + enableSelection: true, enableCheckboxSelector: true, enableFiltering: true, checkboxSelector: { @@ -162,7 +162,7 @@ const Example10: React.FC = () => { // selectableOverride: (row: number, dataContext: any, grid: SlickGrid) => (dataContext.id % 2 === 1) }, multiSelect: false, - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: true, }, @@ -198,12 +198,12 @@ const Example10: React.FC = () => { hideInColumnTitleRow: true, applySelectOnAllPages: true, // when clicking "Select All", should we apply it to all pages (defaults to true) }, - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false, }, enableCheckboxSelector: true, - enableRowSelection: true, + enableSelection: true, gridHeight: 255, gridWidth: 800, enablePagination: true, @@ -303,7 +303,7 @@ const Example10: React.FC = () => { } function onGrid1SelectedRowsChanged(_e: Event, args: any) { - const grid = args && args.grid; + const grid = args?.grid; if (Array.isArray(args.rows)) { const selectedTitles = args.rows.map((idx: number) => { const item = grid.getDataItem(idx); diff --git a/demos/react/src/examples/slickgrid/Example11.tsx b/demos/react/src/examples/slickgrid/Example11.tsx index c3a2396f1f..364acaeb78 100644 --- a/demos/react/src/examples/slickgrid/Example11.tsx +++ b/demos/react/src/examples/slickgrid/Example11.tsx @@ -128,7 +128,7 @@ const Example11: React.FC = () => { editable: true, enableColumnPicker: true, enableCellNavigation: true, - enableRowSelection: true, + enableSelection: true, }); } @@ -302,7 +302,7 @@ const Example11: React.FC = () => {
    • 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/react/src/examples/slickgrid/Example12.tsx b/demos/react/src/examples/slickgrid/Example12.tsx index 20dd7a181b..97cc6e66f5 100644 --- a/demos/react/src/examples/slickgrid/Example12.tsx +++ b/demos/react/src/examples/slickgrid/Example12.tsx @@ -4,7 +4,6 @@ import i18next from 'i18next'; import React, { useEffect, useRef, useState } from 'react'; import { withTranslation } from 'react-i18next'; import { - DelimiterType, Filters, Formatters, SlickgridReact, @@ -160,7 +159,7 @@ const Example12: React.FC = () => { hideInColumnTitleRow: true, }, enableCheckboxSelector: true, - enableRowSelection: true, + enableSelection: true, showCustomFooter: true, // display some metrics in the bottom custom footer customFooterOptions: { metricTexts: { @@ -253,7 +252,7 @@ const Example12: React.FC = () => { function exportToFile(type = 'csv') { textExportService.exportToFile({ - delimiter: type === 'csv' ? DelimiterType.comma : DelimiterType.tab, + delimiter: type === 'csv' ? ',' : '\t', filename: 'myExport', format: type === 'csv' ? 'csv' : 'txt', }); diff --git a/demos/react/src/examples/slickgrid/Example14.tsx b/demos/react/src/examples/slickgrid/Example14.tsx index 7f69f240f2..de8d8162e1 100644 --- a/demos/react/src/examples/slickgrid/Example14.tsx +++ b/demos/react/src/examples/slickgrid/Example14.tsx @@ -7,11 +7,12 @@ import './example14.scss'; // provide custom CSS/SASS styling const Example14: React.FC = () => { const [gridOptions1, setGridOptions1] = useState(undefined); const [gridOptions2, setGridOptions2] = useState(undefined); + const [isColspanSpreading, setIsColspanSpreading] = useState(false); const [columnDefinitions1, setColumnDefinitions1] = useState([]); const [columnDefinitions2, setColumnDefinitions2] = useState([]); const [dataset1, setDataset1] = useState([]); const [dataset2, setDataset2] = useState([]); - // const reactGridRef1 = useRef(null); + const reactGridRef1 = useRef(null); const reactGridRef2 = useRef(null); const [hideSubTitle, setHideSubTitle] = useState(false); @@ -22,9 +23,9 @@ const Example14: React.FC = () => { setDataset2(getData(500)); }, []); - // function reactGrid1Ready(reactGrid: SlickgridReactInstance) { - // reactGridRef1.current = reactGrid; - // } + function reactGrid1Ready(reactGrid: SlickgridReactInstance) { + reactGridRef1.current = reactGrid; + } function reactGrid2Ready(reactGrid: SlickgridReactInstance) { reactGridRef2.current = reactGrid; @@ -162,6 +163,14 @@ const Example14: React.FC = () => { }; } + function spreadColspan() { + const isSpreading = !isColspanSpreading; + setIsColspanSpreading(isSpreading); + reactGridRef1.current?.slickGrid?.setOptions({ spreadHiddenColspan: isSpreading }); + reactGridRef1.current?.slickGrid?.resetActiveCell(); + reactGridRef1.current?.slickGrid?.invalidate(); + } + return !gridOptions1 ? ( '' ) : ( @@ -200,8 +209,23 @@ const Example14: React.FC = () => {

      Grid 1 (with Header Grouping & Colspan) +

      - + + 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) => ` + + `, + }, + { + command: 'sort-desc', + title: 'Sort Descending', + positionOrder: 51, + // Slot renderer using native DOM elements + slotRenderer: () => { + const menuItemElm = createDomElement('div', { className: 'menu-item' }); + const iconElm = createDomElement('i', { className: 'mdi mdi-sort-descending menu-item-icon' }); + const menuItemLabelElm = createDomElement('span', { className: 'menu-item-label', textContent: 'Sort Descending' }); + const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'Alt+↓' }); + menuItemElm.appendChild(iconElm); + menuItemElm.appendChild(menuItemLabelElm); + menuItemElm.appendChild(kbdElm); + return menuItemElm; + }, + }, + ], + }, + }, + }, + { + id: 'duration', + name: 'Duration', + field: 'duration', + sortable: true, + filterable: true, + minWidth: 100, + // Demo: Header Menu with Slot - showing badge and status dot + header: { + menu: { + commandItems: [ + { + command: 'column-resize-by-content', + title: 'Resize by Content', + positionOrder: 47, + // Slot renderer with badge + slotRenderer: () => ` + + `, + }, + { divider: true, command: '', positionOrder: 48 }, + { + command: 'sort-asc', + title: 'Sort Ascending', + iconCssClass: 'mdi mdi-sort-ascending', + positionOrder: 50, + }, + { + command: 'sort-desc', + title: 'Sort Descending', + iconCssClass: 'mdi mdi-sort-descending', + positionOrder: 51, + }, + { divider: true, command: '', positionOrder: 52 }, + { + command: 'clear-sort', + title: 'Remove Sort', + positionOrder: 58, + // Slot renderer with status indicator + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'start', + name: 'Start', + field: 'start', + sortable: true, + formatter: Formatters.dateIso, + filterable: true, + filter: { model: Filters.compoundDate }, + minWidth: 100, + }, + { + id: 'finish', + name: 'Finish', + field: 'finish', + sortable: true, + formatter: Formatters.dateIso, + filterable: true, + filter: { model: Filters.dateRange }, + minWidth: 100, + }, + { + id: 'cost', + name: 'Cost', + field: 'cost', + width: 90, + sortable: true, + filterable: true, + formatter: Formatters.dollar, + // Demo: Header Menu with Slot - showing slotRenderer with callback (item, args) + header: { + menu: { + commandItems: [ + { + command: 'custom-action', + title: 'Advanced Export', + // Demo: Native HTMLElement with event listeners using slotRenderer (full DOM control) + slotRenderer: (cmdItem) => { + // you can use `createDomElement()` from Slickgrid for easier DOM element creation + const containerDiv = createDomElement('div', { className: 'menu-item' }); + const iconDiv = createDomElement('div', { className: 'advanced-export-icon', textContent: '📊' }); + const textSpan = createDomElement('span', { textContent: cmdItem.title || '', style: { flex: '1' } }); + const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'Ctrl+E' }); + containerDiv.appendChild(iconDiv); + containerDiv.appendChild(textSpan); + containerDiv.appendChild(kbdElm); + + // Add native event listeners for hover effects + containerDiv.addEventListener('mouseover', () => { + iconDiv.style.transform = 'scale(1.15)'; + iconDiv.style.background = 'linear-gradient(135deg, #d8dcef 0%, #ffffff 100%)'; + containerDiv.parentElement!.style.backgroundColor = '#854685'; + containerDiv.parentElement!.title = `📈 Export timestamp: ${tempoFormat(new Date(), 'YYYY-MM-DD hh:mm:ss a')}`; + containerDiv.style.color = 'white'; + containerDiv.querySelector('.key-hint')!.style.color = 'black'; + }); + containerDiv.addEventListener('mouseout', () => { + iconDiv.style.transform = 'scale(1)'; + iconDiv.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; + containerDiv.parentElement!.style.backgroundColor = 'white'; + containerDiv.style.color = 'black'; + document.querySelector('.export-timestamp')?.remove(); + }); + + return containerDiv; + }, + action: () => { + alert('Custom export action triggered!'); + }, + }, + { divider: true, command: '' }, + { + command: 'filter-column', + title: 'Filter Column', + // Slot renderer with status indicator and beta badge + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'percentComplete', + name: '% Complete', + field: 'percentComplete', + sortable: true, + filterable: true, + type: 'number', + filter: { model: Filters.slider, operator: '>=' }, + // Demo: Header Menu with Slot - showing interactive element (checkbox) + header: { + menu: { + commandItems: [ + { + command: 'recalc', + title: 'Recalculate', + iconCssClass: 'mdi mdi-refresh', + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'action', + name: 'Action', + field: 'action', + width: 70, + minWidth: 70, + maxWidth: 70, + cssClass: 'justify-center flex', + formatter: () => + `
      `, + excludeFromExport: true, + // Demo: Cell Menu with slot examples (demonstrating defaultMenuItemRenderer at menu level) + cellMenu: { + hideCloseButton: false, + commandTitle: 'Cell Actions', + // Demo: Menu-level default renderer that applies to all items unless overridden + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + commandItems: [ + { + command: 'copy-cell', + title: 'Copy Cell Value', + iconCssClass: 'mdi mdi-content-copy', + action: (_e, args) => { + console.log('Copy cell value:', args.dataContext[args.column.field]); + alert(`Copied: ${args.dataContext[args.column.field]}`); + }, + }, + 'divider', + { + command: 'export-row', + title: 'Export Row', + iconCssClass: 'mdi mdi-download', + action: (_e, args) => { + console.log('Export row:', args.dataContext); + alert(`Export row #${args.dataContext.id}`); + }, + }, + { + command: 'export', + title: 'Export', + iconCssClass: 'mdi mdi-download', + commandItems: [ + { + command: 'export-excel', + title: 'Export as Excel', + iconCssClass: 'mdi mdi-file-excel-outline text-success', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to Excel`); + }, + }, + { + command: 'export-csv', + title: 'Export as CSV', + iconCssClass: 'mdi mdi-file-document-outline', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to CSV`); + }, + }, + { + command: 'export-pdf', + title: 'Export as PDF', + iconCssClass: 'mdi mdi-file-pdf-outline text-red', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to PDF`); + }, + }, + ], + }, + { divider: true, command: '' }, + { + command: 'edit-row', + title: 'Edit Row', + // Individual slotRenderer overrides the defaultMenuItemRenderer + slotRenderer: (_item, args) => ` + + `, + action: (_e, args) => { + console.log('Edit row:', args.dataContext); + alert(`Edit row #${args.dataContext.id}`); + }, + }, + { + command: 'delete-row', + title: 'Delete Row', + iconCssClass: 'mdi mdi-delete text-danger', + action: (_event, args) => { + const dataContext = args.dataContext; + if (confirm(`Do you really want to delete row (${args.row! + 1}) with "${dataContext.title}"`)) { + reactGridRef.current?.gridService.deleteItemById(dataContext.id); + } + }, + }, + ], + }, + }, + ]; + + const gridOptions: GridOption = { + autoResize: { + container: '#demo-container', + }, + enableAutoResize: true, + enableCellNavigation: true, + enableFiltering: true, + enableSorting: true, + enableGrouping: true, + + // Header Menu with slots (already configured in columns above) + enableHeaderMenu: true, + headerMenu: { + // hideCommands: ['column-resize-by-content', 'clear-sort'], + + // Demo: Menu-level default renderer for all header menu items + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + }, + + // Cell Menu with slots (configured in the Action column above) + enableCellMenu: true, + + // Context Menu with slot examples + enableContextMenu: true, + contextMenu: { + // hideCommands: ['clear-grouping', 'copy'], + + // build your command items list + // spread built-in commands and optionally filter/sort them however you want + commandListBuilder: (builtInItems) => { + // commandItems.sort((a, b) => (a === 'divider' || b === 'divider' ? 0 : a.title! > b.title! ? -1 : 1)); + return [ + // filter commands if you want + // ...builtInItems.filter((x) => x !== 'divider' && x.command !== 'copy' && x.command !== 'clear-grouping'), + { + command: 'edit-cell', + title: 'Edit Cell', + // Demo: Individual slotRenderer overrides the menu's defaultMenuItemRenderer + slotRenderer: (cmdItem) => { + // you can use `createDomElement()` from Slickgrid for easier DOM element creation + const containerDiv = createDomElement('div', { className: 'menu-item' }); + const iconDiv = createDomElement('div', { className: 'edit-cell-icon', textContent: '✎' }); + const textSpan = createDomElement('span', { textContent: cmdItem.title || '', style: { flex: '1' } }); + const kbdElm = createDomElement('kbd', { className: 'edit-cell', textContent: 'F2' }); + containerDiv.appendChild(iconDiv); + containerDiv.appendChild(textSpan); + containerDiv.appendChild(kbdElm); + + // Native event listeners for interactive effects + containerDiv.addEventListener('mouseover', () => { + iconDiv.style.transform = 'rotate(15deg) scale(1.1)'; + iconDiv.style.boxShadow = '0 2px 8px rgba(0,200,83,0.4)'; + }); + containerDiv.addEventListener('mouseout', () => { + iconDiv.style.transform = 'rotate(0deg) scale(1)'; + iconDiv.style.boxShadow = 'none'; + }); + + return containerDiv; + }, + action: () => alert('Edit cell'), + }, + ...builtInItems, + { divider: true, command: '' }, + { + command: 'export', + title: 'Export', + iconCssClass: 'mdi mdi-download', + commandItems: [ + { + command: 'export-excel', + title: 'Export as Excel', + iconCssClass: 'mdi mdi-file-excel-outline text-success', + action: () => alert('Export to Excel'), + }, + { + command: 'export-csv', + title: 'Export as CSV', + iconCssClass: 'mdi mdi-file-document-outline', + action: () => alert('Export to CSV'), + }, + { + command: 'export-pdf', + title: 'Export as PDF', + iconCssClass: 'mdi mdi-file-pdf-outline text-danger', + action: () => alert('Export to PDF'), + }, + ], + }, + { divider: true, command: '' }, + { + command: 'delete-row', + title: 'Delete Row', + iconCssClass: 'mdi mdi-delete text-danger', + action: () => alert('Delete row'), + }, + ] as Array; + }, + // Demo: Menu-level default renderer for context menu items + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + }, + + // Grid Menu with slot examples (demonstrating defaultMenuItemRenderer at menu level) + enableGridMenu: true, + gridMenu: { + // hideCommands: ['toggle-preheader', 'toggle-filter'], + + // Demo: Menu-level default renderer that applies to all items (can be overridden per item with slotRenderer) + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + commandListBuilder: (builtInItems) => { + return [ + ...builtInItems, + { divider: true, command: '' }, + { + command: 'export-excel', + title: 'Export to Excel', + iconCssClass: 'mdi mdi-file-excel-outline', + action: () => alert('Export to Excel'), + }, + { + command: 'export-csv', + title: 'Export to CSV', + iconCssClass: 'mdi mdi-download', + // Individual slotRenderer overrides the defaultMenuItemRenderer for this item + slotRenderer: (cmdItem) => ` + + `, + 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, + }, + }; + + setColumnDefinitions(columnDefinitions); + setGridOptions(gridOptions); + } + + function clearGrouping() { + reactGridRef.current?.dataView?.setGrouping([]); + } + + function collapseAllGroups() { + reactGridRef.current?.dataView?.collapseAllGroups(); + } + + function expandAllGroups() { + reactGridRef.current?.dataView?.expandAllGroups(); + } + + function groupByDuration() { + // you need to manually add the sort icon(s) in UI + reactGridRef.current?.slickGrid?.setSortColumns([{ columnId: 'duration', sortAsc: true }]); + reactGridRef.current?.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); + reactGridRef.current?.slickGrid?.invalidate(); // invalidate all rows and re-render + } + + function 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; + } + + function toggleSubTitle() { + const newHideSubTitle = !hideSubTitle; + setHideSubTitle(newHideSubTitle); + const action = newHideSubTitle ? 'add' : 'remove'; + document.querySelector('.subtitle')?.classList[action]('hidden'); + reactGridRef.current?.resizerService.resizeGrid(0); + } + + return !gridOptions ? ( + '' + ) : ( +
      +

      + 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 @@

      Slickgrid-Universal

      Example37 - Hybrid Selection Model Example38 - Spreadsheet Drag-Fill Example39 - Master/Detail Grids + Example40 - Menus with Slots

    diff --git a/demos/vanilla/src/examples/example01.ts b/demos/vanilla/src/examples/example01.ts index 92cc438584..6f6a086e78 100644 --- a/demos/vanilla/src/examples/example01.ts +++ b/demos/vanilla/src/examples/example01.ts @@ -1,4 +1,4 @@ -import { ExtensionName, Formatters, type Column, type GridOption } from '@slickgrid-universal/common'; +import { Formatters, type Column, type GridOption } from '@slickgrid-universal/common'; import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; import { ExampleGridOptions } from './example-grid-options.js'; import { zeroPadding } from './utilities.js'; @@ -266,7 +266,7 @@ export default class Example01 { toggleGridMenu(e: MouseEvent) { if (this.sgb2?.extensionService) { - const gridMenuInstance = this.sgb2.extensionService.getExtensionInstanceByName(ExtensionName.gridMenu); + const gridMenuInstance = this.sgb2.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/demos/vanilla/src/examples/example03.ts b/demos/vanilla/src/examples/example03.ts index 9dbc265d3a..c2a023b836 100644 --- a/demos/vanilla/src/examples/example03.ts +++ b/demos/vanilla/src/examples/example03.ts @@ -355,9 +355,6 @@ export default class Example03 { enableAutoSizeColumns: true, enableAutoResize: true, enableCellNavigation: true, - enablePdfExport: true, - enableTextExport: true, - enableExcelExport: true, excelExportOptions: { exportWithFormatter: true, }, @@ -367,8 +364,12 @@ export default class Example03 { repeatHeadersOnEachPage: true, // defaults to true }, externalResources: [new TextExportService(), this.excelExportService, this.pdfExportService], + // -- NOTE: registered resources are auto-enabled + // enableTextExport: true, + // enablePdfExport: true, + // enableExcelExport: true, enableFiltering: true, - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false, }, @@ -406,7 +407,7 @@ export default class Example03 { grouping: ['duration'], }, enableCheckboxSelector: true, - enableRowSelection: true, + enableSelection: true, checkboxSelector: { hideInFilterHeaderRow: false, hideInColumnTitleRow: true, @@ -423,7 +424,7 @@ export default class Example03 { onCommand: (e, args) => this.executeCommand(e, args), onOptionSelected: (_e, args) => { // change "Effort-Driven" property with new option selected from the Cell Menu - const dataContext = args && args.dataContext; + const dataContext = args?.dataContext; if (dataContext && dataContext.hasOwnProperty('effortDriven')) { dataContext.effortDriven = args.item.option; this.sgb.gridService.updateItem(dataContext); diff --git a/demos/vanilla/src/examples/example04.ts b/demos/vanilla/src/examples/example04.ts index 3c1145de26..ffb5bb5b86 100644 --- a/demos/vanilla/src/examples/example04.ts +++ b/demos/vanilla/src/examples/example04.ts @@ -3,7 +3,6 @@ import { Editors, Filters, Formatters, - OperatorType, type AutocompleterOption, type Column, type ColumnEditorDualInput, @@ -123,7 +122,7 @@ export default class Example04 { collectionFilterBy: { property: 'value', value: 0, - operator: OperatorType.notEqual, + operator: '!=', }, // collectionOverride: (updatedCollection, args) => { // console.log(args); @@ -404,7 +403,7 @@ export default class Example04 { sanitizeDataExport: true, }, externalResources: [new ExcelExportService()], - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false, }, @@ -415,7 +414,7 @@ export default class Example04 { name: 'Sel', // column name will only show when `hideInColumnTitleRow` is true onExtensionRegistered: (instance) => (this.checkboxSelectorInstance = instance), }, - enableRowSelection: true, + enableSelection: true, frozenColumn: this.frozenColumnCount, frozenRow: this.frozenRowCount, // frozenBottom: true, // if you want to freeze the bottom instead of the top, you can enable this property diff --git a/demos/vanilla/src/examples/example05.ts b/demos/vanilla/src/examples/example05.ts index c38ef8d268..5c9a12a0f1 100644 --- a/demos/vanilla/src/examples/example05.ts +++ b/demos/vanilla/src/examples/example05.ts @@ -261,7 +261,7 @@ export default class Example05 { '
    Grid created with Slickgrid-Universal
    ', }, // enableCheckboxSelector: true, - // enableRowSelection: true, + // enableSelection: true, // multiSelect: false, // checkboxSelector: { // hideInFilterHeaderRow: false, diff --git a/demos/vanilla/src/examples/example07.ts b/demos/vanilla/src/examples/example07.ts index 36504c070e..d8d1cefb2f 100644 --- a/demos/vanilla/src/examples/example07.ts +++ b/demos/vanilla/src/examples/example07.ts @@ -1,5 +1,5 @@ import { BindingEventService } from '@slickgrid-universal/binding'; -import { Editors, Filters, Formatters, OperatorType, type Column, type GridOption } from '@slickgrid-universal/common'; +import { Editors, Filters, Formatters, type Column, type GridOption } from '@slickgrid-universal/common'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; import DOMPurify from 'dompurify'; @@ -355,7 +355,7 @@ export default class Example07 { separatorBetweenTextLabels: ' ', }, model: Filters.multipleSelect, - operator: OperatorType.inContains, + operator: 'IN_CONTAINS', }, }, ]; @@ -385,8 +385,8 @@ export default class Example07 { externalResources: [new ExcelExportService()], enableCellNavigation: true, enableCheckboxSelector: true, - enableRowSelection: true, - rowSelectionOptions: { + enableSelection: true, + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false, }, diff --git a/demos/vanilla/src/examples/example08.html b/demos/vanilla/src/examples/example08.html index 490c1b4bc9..83aefa28d1 100644 --- a/demos/vanilla/src/examples/example08.html +++ b/demos/vanilla/src/examples/example08.html @@ -12,7 +12,17 @@

    -

    Grid 1 (with Header Grouping & Colspan)

    +

    + Grid 1 (with Header Grouping & ColSpan) + +

    diff --git a/demos/vanilla/src/examples/example08.ts b/demos/vanilla/src/examples/example08.ts index 1f761ad988..4098b5200b 100644 --- a/demos/vanilla/src/examples/example08.ts +++ b/demos/vanilla/src/examples/example08.ts @@ -1,4 +1,4 @@ -import { type Column, type GridOption, type ItemMetadata, type OperatorString } from '@slickgrid-universal/common'; +import { type Column, type GridOption, type ItemMetadata, type OperatorType } from '@slickgrid-universal/common'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { PdfExportService } from '@slickgrid-universal/pdf-export'; import { TextExportService } from '@slickgrid-universal/text-export'; @@ -16,9 +16,10 @@ export default class Example08 { sgb1: SlickVanillaGridBundle; sgb2: SlickVanillaGridBundle; grid2SearchSelectedColumn: Column; - grid2SelectedOperator: OperatorString; + grid2SelectedOperator: OperatorType; grid2SearchValue: any; - operatorList: OperatorString[] = ['=', '<', '<=', '>', '>=', '<>', 'StartsWith', 'EndsWith']; + operatorList: OperatorType[] = ['=', '<', '<=', '>', '>=', '<>', 'StartsWith', 'EndsWith']; + isColspanSpreading = false; constructor() { this.definedGrid1(); @@ -97,6 +98,7 @@ export default class Example08 { getRowMetadata: (item: any, row: number) => this.renderDifferentColspan(item, row), }, }, + spreadHiddenColspan: this.isColspanSpreading, }; } @@ -251,7 +253,7 @@ export default class Example08 { } selectedOperatorChanged(newOperator: string) { - this.grid2SelectedOperator = newOperator as OperatorString; + this.grid2SelectedOperator = newOperator as OperatorType; this.updateFilter(); } @@ -265,10 +267,17 @@ export default class Example08 { this.updateFilter(); } + spreadColspan() { + this.isColspanSpreading = !this.isColspanSpreading; + this.sgb1.slickGrid?.setOptions({ spreadHiddenColspan: this.isColspanSpreading }); + this.sgb1.slickGrid?.resetActiveCell(); + this.sgb1.slickGrid?.invalidate(); + } + updateFilter() { this.sgb2.filterService.updateSingleFilter({ columnId: `${this.grid2SearchSelectedColumn?.id ?? ''}`, - operator: this.grid2SelectedOperator as OperatorString, + operator: this.grid2SelectedOperator as OperatorType, searchTerms: [this.grid2SearchValue || ''], }); } diff --git a/demos/vanilla/src/examples/example09.ts b/demos/vanilla/src/examples/example09.ts index dc87fbc457..bb24dd0732 100644 --- a/demos/vanilla/src/examples/example09.ts +++ b/demos/vanilla/src/examples/example09.ts @@ -1,7 +1,6 @@ import { BindingEventService } from '@slickgrid-universal/binding'; import { Filters, - OperatorType, type Column, type CurrentColumn, type GridOption, @@ -141,13 +140,13 @@ export default class Example09 { 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], @@ -158,8 +157,8 @@ export default class Example09 { : { // 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 @@ -174,7 +173,7 @@ export default class Example09 { 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) + "$'"; @@ -483,7 +482,7 @@ export default class Example09 { setFiltersDynamically() { // we can Set Filters Dynamically (or different filters) afterward through the FilterService this.sgb?.filterService.updateFilters([ - // { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal }, + // { columnId: 'gender', searchTerms: ['male'], operator: '=' }, { columnId: 'name', searchTerms: ['A'], operator: 'a*' }, ]); } diff --git a/demos/vanilla/src/examples/example10.ts b/demos/vanilla/src/examples/example10.ts index 21f7ecdafd..fe2e2525fc 100644 --- a/demos/vanilla/src/examples/example10.ts +++ b/demos/vanilla/src/examples/example10.ts @@ -3,7 +3,6 @@ import { BindingEventService } from '@slickgrid-universal/binding'; import { Filters, Formatters, - OperatorType, type Column, type CursorPageInfo, type GridOption, @@ -207,7 +206,7 @@ export default class Example10 { gridHeight: 275, gridWidth: 900, 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' } }, }, enableFiltering: true, @@ -235,13 +234,13 @@ export default class Example10 { ], filters: [ // 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: 'EQ' }, + // { 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' }, ], sorters: [ // direction can written as 'asc' (uppercase or lowercase) and/or use the SortDirection type @@ -263,7 +262,7 @@ export default class Example10 { }, ], filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => { - 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. @@ -390,11 +389,11 @@ export default class Example10 { // we can Set Filters Dynamically (or different filters) afterward through the FilterService this.sgb.filterService.updateFilters([ - { columnId: 'gender', searchTerms: ['female'], operator: OperatorType.equal }, - { columnId: 'name', searchTerms: ['Jane'], operator: OperatorType.startsWith }, + { columnId: 'gender', searchTerms: ['female'], operator: 'EQ' }, + { 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' }, ]); } @@ -413,13 +412,13 @@ export default class Example10 { this.sgb?.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: 'EQ' }, + // { 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' }, ]); this.sgb?.sortService.updateSorting([ // direction can written as 'asc' (uppercase or lowercase) and/or use the SortDirection type diff --git a/demos/vanilla/src/examples/example11.ts b/demos/vanilla/src/examples/example11.ts index 31234d908c..f183d81e2d 100644 --- a/demos/vanilla/src/examples/example11.ts +++ b/demos/vanilla/src/examples/example11.ts @@ -6,7 +6,6 @@ import { Filters, formatNumber, Formatters, - OperatorType, SlickGlobalEditorLock, SortComparers, type AutocompleterOption, @@ -87,11 +86,13 @@ export default class Example11 { isUserDefined: false, columns: [...this.allColumnIds] .map((colId) => ({ columnId: `${colId}` })) + // OR the `hidden` props alternative + // .map((colId) => ({ columnId: `${colId}`, hidden: false })) .filter((col) => col.columnId !== 'product' && col.columnId !== 'countryOfOrigin'), // remove "Product", "Country of Origin" filters: [ - { columnId: 'finish', operator: OperatorType.lessThanOrEqual, searchTerms: [`${this.currentYear}-01-01`] }, - { columnId: 'completed', operator: OperatorType.equal, searchTerms: [true] }, - { columnId: 'percentComplete', operator: OperatorType.greaterThan, searchTerms: [50] }, + { columnId: 'finish', operator: '<=', searchTerms: [`${this.currentYear}-01-01`] }, + { columnId: 'completed', operator: '=', searchTerms: [true] }, + { columnId: 'percentComplete', operator: '>', searchTerms: [50] }, ] as CurrentFilter[], sorters: [{ columnId: 'finish', direction: 'desc' }] as CurrentSorter[], }, @@ -100,7 +101,11 @@ export default class Example11 { value: 'greaterCurrentYear', isSelected: false, isUserDefined: false, - columns: [...this.allColumnIds].map((colId) => ({ columnId: `${colId}` })).filter((col) => col.columnId !== 'cost'), // remove "Cost" + columns: [...this.allColumnIds] + .map((colId) => ({ columnId: `${colId}` })) + // OR the `hidden` props alternative + // .map((colId) => ({ columnId: `${colId}`, hidden: false })) + .filter((col) => col.columnId !== 'cost'), // remove "Cost" filters: [{ columnId: 'finish', operator: '>=', searchTerms: [`${this.currentYear + 1}-01-01`] }], sorters: [{ columnId: 'finish', direction: 'asc' }] as CurrentSorter[], }, @@ -400,14 +405,14 @@ export default class Example11 { }, externalResources: [new ExcelExportService(), new SlickCustomTooltip()], enableFiltering: true, - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false, }, rowHeight: 33, headerRowHeight: 35, enableCheckboxSelector: true, - enableRowSelection: true, + enableSelection: true, checkboxSelector: { hideInFilterHeaderRow: false, hideInColumnTitleRow: true, @@ -451,6 +456,10 @@ export default class Example11 { ], onCommand: (e, args) => this.executeCommand(e, args), }, + + // the `hidden` props alternative, we could use column "hidden" props, then we could also include it in the saved state + // using this flag (below) will result in all columns included in the Grid State including hidden columns + // gridStateIncludeHiddenProps: true, }; const storedData = localStorage.getItem(LOCAL_STORAGE_KEY); @@ -737,8 +746,7 @@ export default class Example11 { } this.predefinedViews.forEach((viewSelect) => (viewSelect.isSelected = false)); // reset selection - const currentGridState = this.sgb.gridStateService.getCurrentGridState(); - const { columns, filters, sorters, pinning } = currentGridState; + const { columns, filters, sorters, pinning } = this.sgb.gridStateService.getCurrentGridState(); const viewName = await prompt('Please provide a name for the new View.'); if (viewName) { @@ -792,8 +800,7 @@ export default class Example11 { event.stopPropagation(); return; } - const currentGridState = this.sgb.gridStateService.getCurrentGridState(); - const { columns, filters, sorters, pinning } = currentGridState; + const { columns, filters, sorters, pinning } = this.sgb.gridStateService.getCurrentGridState(); if (this.currentSelectedViewPreset && filters) { const filterName = await prompt(`Update View name or click on OK to continue.`, this.currentSelectedViewPreset.label); @@ -830,7 +837,11 @@ export default class Example11 { this.sgb.gridService.clearPinning(); this.sgb.filterService.clearFilters(); this.sgb.sortService.clearSorting(); - this.sgb.gridStateService.changeColumnsArrangement([...this.columnDefinitions].map((col) => ({ columnId: `${col.id}` }))); + this.sgb.gridStateService.changeColumnsArrangement( + [...this.columnDefinitions].map((col) => ({ columnId: `${col.id}` })) + // OR the `hidden` props alternative + // [...this.columnDefinitions].map((col) => ({ columnId: `${col.id}`, hidden: false })) + ); } localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.predefinedViews)); this.currentSelectedViewPreset = selectedView; diff --git a/demos/vanilla/src/examples/example12.ts b/demos/vanilla/src/examples/example12.ts index e78f5a0127..d9a0bfb4dd 100644 --- a/demos/vanilla/src/examples/example12.ts +++ b/demos/vanilla/src/examples/example12.ts @@ -1,7 +1,6 @@ import { BindingEventService } from '@slickgrid-universal/binding'; import { Editors, - EventNamingStyle, Filters, formatNumber, Formatters, @@ -500,7 +499,7 @@ export default class Example12 { autoFixResizeRequiredGoodCount: 1, datasetIdPropertyName: 'id', darkMode: this._darkMode, - eventNamingStyle: EventNamingStyle.lowerCase, + eventNamingStyle: 'lowerCase', autoAddCustomEditorFormatter: customEditableInputFormatter, enableAddRow: true, // <-- this flag is required to work with the (create & clone) modal types enableCellNavigation: true, @@ -525,7 +524,7 @@ export default class Example12 { }, externalResources: [new ExcelExportService(), this.compositeEditorInstance], enableFiltering: true, - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false, }, @@ -535,7 +534,7 @@ export default class Example12 { rowHeight: 33, headerRowHeight: 35, enableCheckboxSelector: true, - enableRowSelection: true, + enableSelection: true, checkboxSelector: { applySelectOnAllPages: true, hideInFilterHeaderRow: false, @@ -1082,6 +1081,7 @@ export default class Example12 { setTimeout(() => { this.compositeEditorInstance?.openDetails({ + // domElementType: 'div', // use as fallback when default doesn't work (like in Salesforce LWC), accepts 'div' or 'dialog' headerTitle: modalTitle, modalType, insertOptions: { highlightRow: false }, // disable highlight to avoid flaky tests in Cypress diff --git a/demos/vanilla/src/examples/example14.ts b/demos/vanilla/src/examples/example14.ts index 542a72fd72..b0a25f5452 100644 --- a/demos/vanilla/src/examples/example14.ts +++ b/demos/vanilla/src/examples/example14.ts @@ -1,7 +1,6 @@ import { BindingEventService } from '@slickgrid-universal/binding'; import { Editors, - EventNamingStyle, Filters, // utilities formatNumber, @@ -544,7 +543,7 @@ export default class Example14 { }); this.gridOptions = { - eventNamingStyle: EventNamingStyle.lowerCase, + eventNamingStyle: 'lowerCase', editable: true, autoAddCustomEditorFormatter: customEditableInputFormatter, enableCellNavigation: true, @@ -584,14 +583,14 @@ export default class Example14 { }, externalResources: [new SlickCustomTooltip(), new ExcelExportService()], enableFiltering: true, - enableRowSelection: true, + enableSelection: true, enableCheckboxSelector: true, checkboxSelector: { applySelectOnAllPages: true, // already defaults to true hideInFilterHeaderRow: false, hideInColumnTitleRow: true, }, - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false, }, @@ -763,7 +762,7 @@ export default class Example14 { // just for demo purposes, set it back to its original width const columns = this.sgb.slickGrid?.getColumns() as Column[]; columns.forEach((col) => (col.width = col.originalWidth)); - this.sgb.slickGrid?.setColumns(columns); + this.sgb.slickGrid?.updateColumns(); this.sgb.slickGrid?.autosizeColumns(); // simple css class to change selected button in the UI diff --git a/demos/vanilla/src/examples/example15.ts b/demos/vanilla/src/examples/example15.ts index 28adf6f52e..0de56b47e2 100644 --- a/demos/vanilla/src/examples/example15.ts +++ b/demos/vanilla/src/examples/example15.ts @@ -1,13 +1,5 @@ import { BindingEventService } from '@slickgrid-universal/binding'; -import { - Editors, - Filters, - OperatorType, - type Column, - type GridOption, - type GridStateChange, - type Metrics, -} from '@slickgrid-universal/common'; +import { Editors, Filters, type Column, type GridOption, type GridStateChange, type Metrics } from '@slickgrid-universal/common'; import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; import { GridOdataService, type OdataOption, type OdataServiceApi } from '@slickgrid-universal/odata'; import { RxJsResource } from '@slickgrid-universal/rxjs-observable'; @@ -155,7 +147,7 @@ export default class Example15 { 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], @@ -164,8 +156,8 @@ export default class Example15 { 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 @@ -454,7 +446,7 @@ export default class Example15 { setFiltersDynamically() { // we can Set Filters Dynamically (or different filters) afterward through the FilterService this.sgb?.filterService.updateFilters([ - // { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal }, + // { columnId: 'gender', searchTerms: ['male'], operator: '=' }, { columnId: 'name', searchTerms: ['A'], operator: 'a*' }, ]); } diff --git a/demos/vanilla/src/examples/example16.html b/demos/vanilla/src/examples/example16.html index a8451d2923..4ed1896305 100644 --- a/demos/vanilla/src/examples/example16.html +++ b/demos/vanilla/src/examples/example16.html @@ -2,7 +2,12 @@

    Example 16 - Regular & Custom Tooltips (with Salesforce Theme) - @@ -19,17 +24,29 @@

  • -
    +
    - -
    -
    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) + + + +

      + +
      +

      + + 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. + +

      +
      + +
      +
      + + + + +
      +
      + +
      diff --git a/demos/vanilla/src/examples/example40.scss b/demos/vanilla/src/examples/example40.scss new file mode 100644 index 0000000000..54a81b8298 --- /dev/null +++ b/demos/vanilla/src/examples/example40.scss @@ -0,0 +1,140 @@ +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; + } +} + +.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/vanilla/src/examples/example40.ts b/demos/vanilla/src/examples/example40.ts new file mode 100644 index 0000000000..4106ba23d2 --- /dev/null +++ b/demos/vanilla/src/examples/example40.ts @@ -0,0 +1,620 @@ +import { format as tempoFormat } from '@formkit/tempo'; +import { BindingEventService } from '@slickgrid-universal/binding'; +import { + Aggregators, + createDomElement, + Filters, + Formatters, + SortComparers, + SortDirectionNumber, + type Column, + type GridOption, + type Grouping, + type MenuCommandItem, +} from '@slickgrid-universal/common'; +import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; +import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; +import { ExampleGridOptions } from './example-grid-options.js'; +import './example40.scss'; + +interface ReportItem { + id: number; + title: string; + duration: number; + cost: number; + percentComplete: number; + start: Date; + finish: Date; + action?: string; +} + +export default class Example40 { + private _bindingEventService: BindingEventService; + columnDefinitions: Column[]; + gridOptions: GridOption; + dataset: ReportItem[]; + sgb: SlickVanillaGridBundle; + subTitleStyle = 'display: block'; + + constructor() { + this._bindingEventService = new BindingEventService(); + } + + attached() { + this.initializeGrid(); + this.dataset = this.loadData(2000); + const gridContainerElm = document.querySelector(`.grid40`) as HTMLDivElement; + + this.sgb = new Slicker.GridBundle( + gridContainerElm, + this.columnDefinitions, + { ...ExampleGridOptions, ...this.gridOptions }, + this.dataset + ); + } + + dispose() { + this.sgb?.dispose(); + this._bindingEventService.unbindAll(); + } + + initializeGrid() { + // This example demonstrates Menu Slot functionality across all menu types: + // - SlickHeaderMenu, SlickCellMenu, SlickContextMenu, SlickGridMenu + + this.columnDefinitions = [ + { + 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) => ` + + `, + }, + { + command: 'sort-desc', + title: 'Sort Descending', + positionOrder: 51, + // Slot renderer using native DOM elements + slotRenderer: () => { + const menuItemElm = createDomElement('div', { className: 'menu-item' }); + const iconElm = createDomElement('i', { className: 'mdi mdi-sort-descending menu-item-icon' }); + const menuItemLabelElm = createDomElement('span', { className: 'menu-item-label', textContent: 'Sort Descending' }); + const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'Alt+↓' }); + menuItemElm.appendChild(iconElm); + menuItemElm.appendChild(menuItemLabelElm); + menuItemElm.appendChild(kbdElm); + return menuItemElm; + }, + }, + ], + }, + }, + }, + { + id: 'duration', + name: 'Duration', + field: 'duration', + sortable: true, + filterable: true, + minWidth: 100, + // Demo: Header Menu with Slot - showing badge and status dot + header: { + menu: { + commandItems: [ + { + command: 'column-resize-by-content', + title: 'Resize by Content', + positionOrder: 47, + // Slot renderer with badge + slotRenderer: () => ` + + `, + }, + { divider: true, command: '', positionOrder: 48 }, + { + command: 'sort-asc', + title: 'Sort Ascending', + iconCssClass: 'mdi mdi-sort-ascending', + positionOrder: 50, + }, + { + command: 'sort-desc', + title: 'Sort Descending', + iconCssClass: 'mdi mdi-sort-descending', + positionOrder: 51, + }, + { divider: true, command: '', positionOrder: 52 }, + { + command: 'clear-sort', + title: 'Remove Sort', + positionOrder: 58, + // Slot renderer with status indicator + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'start', + name: 'Start', + field: 'start', + sortable: true, + formatter: Formatters.dateIso, + filterable: true, + filter: { model: Filters.compoundDate }, + minWidth: 100, + }, + { + id: 'finish', + name: 'Finish', + field: 'finish', + sortable: true, + formatter: Formatters.dateIso, + filterable: true, + filter: { model: Filters.dateRange }, + minWidth: 100, + }, + { + id: 'cost', + name: 'Cost', + field: 'cost', + width: 90, + sortable: true, + filterable: true, + formatter: Formatters.dollar, + // Demo: Header Menu with Slot - showing slotRenderer with callback (item, args) + header: { + menu: { + commandItems: [ + { + command: 'custom-action', + title: 'Advanced Export', + // Demo: Native HTMLElement with event listeners using slotRenderer (full DOM control) + slotRenderer: (cmdItem) => { + // you can use `createDomElement()` from Slickgrid for easier DOM element creation + const containerDiv = createDomElement('div', { className: 'menu-item' }); + const iconDiv = createDomElement('div', { className: 'advanced-export-icon', textContent: '📊' }); + const textSpan = createDomElement('span', { textContent: cmdItem.title || '', style: { flex: '1' } }); + const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'Ctrl+E' }); + containerDiv.appendChild(iconDiv); + containerDiv.appendChild(textSpan); + containerDiv.appendChild(kbdElm); + + // Add native event listeners for hover effects + containerDiv.addEventListener('mouseover', () => { + iconDiv.style.transform = 'scale(1.15)'; + iconDiv.style.background = 'linear-gradient(135deg, #d8dcef 0%, #ffffff 100%)'; + containerDiv.parentElement!.style.backgroundColor = '#854685'; + containerDiv.parentElement!.title = `📈 Export timestamp: ${tempoFormat(new Date(), 'YYYY-MM-DD hh:mm:ss a')}`; + containerDiv.style.color = 'white'; + containerDiv.querySelector('.key-hint')!.style.color = 'black'; + }); + containerDiv.addEventListener('mouseout', () => { + iconDiv.style.transform = 'scale(1)'; + iconDiv.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; + containerDiv.parentElement!.style.backgroundColor = 'white'; + containerDiv.style.color = 'black'; + document.querySelector('.export-timestamp')?.remove(); + }); + + return containerDiv; + }, + action: () => { + alert('Custom export action triggered!'); + }, + }, + { divider: true, command: '' }, + { + command: 'filter-column', + title: 'Filter Column', + // Slot renderer with status indicator and beta badge + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'percentComplete', + name: '% Complete', + field: 'percentComplete', + sortable: true, + filterable: true, + type: 'number', + filter: { model: Filters.slider, operator: '>=' }, + // Demo: Header Menu with Slot - showing interactive element (checkbox) + header: { + menu: { + commandItems: [ + { + command: 'recalc', + title: 'Recalculate', + iconCssClass: 'mdi mdi-refresh', + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'action', + name: 'Action', + field: 'action', + width: 70, + minWidth: 70, + maxWidth: 70, + cssClass: 'justify-center flex', + formatter: () => `
      `, + excludeFromExport: true, + // Demo: Cell Menu with slot examples (demonstrating defaultMenuItemRenderer at menu level) + cellMenu: { + hideCloseButton: false, + commandTitle: 'Cell Actions', + // Demo: Menu-level default renderer that applies to all items unless overridden + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + commandItems: [ + { + command: 'copy-cell', + title: 'Copy Cell Value', + iconCssClass: 'mdi mdi-content-copy', + action: (_e, args) => { + console.log('Copy cell value:', args.dataContext[args.column.field]); + alert(`Copied: ${args.dataContext[args.column.field]}`); + }, + }, + 'divider', + { + command: 'export-row', + title: 'Export Row', + iconCssClass: 'mdi mdi-download', + action: (_e, args) => { + console.log('Export row:', args.dataContext); + alert(`Export row #${args.dataContext.id}`); + }, + }, + { + command: 'export', + title: 'Export', + iconCssClass: 'mdi mdi-download', + commandItems: [ + { + command: 'export-excel', + title: 'Export as Excel', + iconCssClass: 'mdi mdi-file-excel-outline text-success', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to Excel`); + }, + }, + { + command: 'export-csv', + title: 'Export as CSV', + iconCssClass: 'mdi mdi-file-document-outline', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to CSV`); + }, + }, + { + command: 'export-pdf', + title: 'Export as PDF', + iconCssClass: 'mdi mdi-file-pdf-outline text-red', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to PDF`); + }, + }, + ], + }, + { divider: true, command: '' }, + { + command: 'edit-row', + title: 'Edit Row', + // Individual slotRenderer overrides the defaultMenuItemRenderer + slotRenderer: (_item, args) => ` + + `, + action: (_e, args) => { + console.log('Edit row:', args.dataContext); + alert(`Edit row #${args.dataContext.id}`); + }, + }, + { + command: 'delete-row', + title: 'Delete Row', + iconCssClass: 'mdi mdi-delete text-danger', + action: (_event, args) => { + const dataContext = args.dataContext; + if (confirm(`Do you really want to delete row (${args.row! + 1}) with "${dataContext.title}"`)) { + this.sgb?.instances?.gridService.deleteItemById(dataContext.id); + } + }, + }, + ], + }, + }, + ]; + + this.gridOptions = { + autoResize: { + container: '.demo-container', + }, + enableAutoResize: true, + enableCellNavigation: true, + enableFiltering: true, + enableSorting: true, + enableGrouping: true, + + // Header Menu with slots (already configured in columns above) + enableHeaderMenu: true, + headerMenu: { + // hideCommands: ['column-resize-by-content', 'clear-sort'], + + // Demo: Menu-level default renderer for all header menu items + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + }, + + // Cell Menu with slots (configured in the Action column above) + enableCellMenu: true, + + // Context Menu with slot examples + enableContextMenu: true, + contextMenu: { + // hideCommands: ['clear-grouping', 'copy'], + + // build your command items list + // spread built-in commands and optionally filter/sort them however you want + commandListBuilder: (builtInItems) => { + // commandItems.sort((a, b) => (a === 'divider' || b === 'divider' ? 0 : a.title! > b.title! ? -1 : 1)); + return [ + // filter commands if you want + // ...builtInItems.filter((x) => x !== 'divider' && x.command !== 'copy' && x.command !== 'clear-grouping'), + { + command: 'edit-cell', + title: 'Edit Cell', + // Demo: Individual slotRenderer overrides the menu's defaultMenuItemRenderer + slotRenderer: (cmdItem) => { + // you can use `createDomElement()` from Slickgrid for easier DOM element creation + const containerDiv = createDomElement('div', { className: 'menu-item' }); + const iconDiv = createDomElement('div', { className: 'edit-cell-icon', textContent: '✎' }); + const textSpan = createDomElement('span', { textContent: cmdItem.title || '', style: { flex: '1' } }); + const kbdElm = createDomElement('kbd', { className: 'edit-cell', textContent: 'F2' }); + containerDiv.appendChild(iconDiv); + containerDiv.appendChild(textSpan); + containerDiv.appendChild(kbdElm); + + // Native event listeners for interactive effects + containerDiv.addEventListener('mouseover', () => { + iconDiv.style.transform = 'rotate(15deg) scale(1.1)'; + iconDiv.style.boxShadow = '0 2px 8px rgba(0,200,83,0.4)'; + }); + containerDiv.addEventListener('mouseout', () => { + iconDiv.style.transform = 'rotate(0deg) scale(1)'; + iconDiv.style.boxShadow = 'none'; + }); + + return containerDiv; + }, + action: () => alert('Edit cell'), + }, + ...builtInItems, + { divider: true, command: '' }, + { + command: 'export', + title: 'Export', + iconCssClass: 'mdi mdi-download', + commandItems: [ + { + command: 'export-excel', + title: 'Export as Excel', + iconCssClass: 'mdi mdi-file-excel-outline text-success', + action: () => alert('Export to Excel'), + }, + { + command: 'export-csv', + title: 'Export as CSV', + iconCssClass: 'mdi mdi-file-document-outline', + action: () => alert('Export to CSV'), + }, + { + command: 'export-pdf', + title: 'Export as PDF', + iconCssClass: 'mdi mdi-file-pdf-outline text-danger', + action: () => alert('Export to PDF'), + }, + ], + }, + { divider: true, command: '' }, + { + command: 'delete-row', + title: 'Delete Row', + iconCssClass: 'mdi mdi-delete text-danger', + action: () => alert('Delete row'), + }, + ] as Array; + }, + // Demo: Menu-level default renderer for context menu items + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + }, + + // Grid Menu with slot examples (demonstrating defaultMenuItemRenderer at menu level) + enableGridMenu: true, + gridMenu: { + // hideCommands: ['toggle-preheader', 'toggle-filter'], + + // Demo: Menu-level default renderer that applies to all items (can be overridden per item with slotRenderer) + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + commandListBuilder: (builtInItems) => { + return [ + ...builtInItems, + { divider: true, command: '' }, + { + command: 'export-excel', + title: 'Export to Excel', + iconCssClass: 'mdi mdi-file-excel-outline', + action: () => alert('Export to Excel'), + }, + { + command: 'export-csv', + title: 'Export to CSV', + iconCssClass: 'mdi mdi-download', + // Individual slotRenderer overrides the defaultMenuItemRenderer for this item + slotRenderer: (cmdItem) => ` + + `, + 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) { { - 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 @@ + + + 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 + +``` + #### 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 ![image](https://user-images.githubusercontent.com/643976/208590003-b637dcda-5164-42cc-bfad-e921a22c1837.png) ### 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 ` +
      + ${cmdItem.iconCssClass ? `` : ''} + ${cmdItem.title} +
      + `; + }, + commandItems: [ + { + command: 'action-1', + title: 'Action One', + iconCssClass: 'mdi mdi-check', + // This item uses defaultMenuItemRenderer + }, + { + command: 'custom', + title: 'Custom Item', + // This item overrides defaultMenuItemRenderer with its own slotRenderer + slotRenderer: () => ` +
      + Custom rendering overrides default +
      + ` + } + ] +}; +``` + +### Menu Types & Configuration + +The `slotRenderer` and `defaultMenuItemRenderer` work identically across all menu plugins: + +#### Header Menu +```typescript +const columnDef = { + id: 'name', + header: { + menu: { + defaultMenuItemRenderer: (cmdItem, args) => `
      ${cmdItem.title}
      `, + commandItems: [ + { + command: 'sort', + title: 'Sort', + slotRenderer: () => '
      Custom sort
      ' + } + ] + } + } +}; +``` + +#### Cell Menu +```typescript +const columnDef = { + id: 'action', + cellMenu: { + defaultMenuItemRenderer: (cmdItem, args) => `
      ${cmdItem.title}
      `, + commandItems: [ + { + command: 'edit', + title: 'Edit', + slotRenderer: (cmdItem, args) => `
      Edit row ${args.dataContext.id}
      ` + } + ] + } +}; +``` + +#### Context Menu +```typescript +const gridOptions = { + enableContextMenu: true, + contextMenu: { + defaultMenuItemRenderer: (cmdItem, args) => `
      ${cmdItem.title}
      `, + commandItems: [ + { + command: 'export', + title: 'Export', + slotRenderer: () => '
      📊 Export data
      ' + } + ] + } +}; +``` + +#### Grid Menu +```typescript +const gridOptions = { + enableGridMenu: true, + gridMenu: { + defaultMenuItemRenderer: (cmdItem, args) => `
      ${cmdItem.title}
      `, + commandItems: [ + { + command: 'refresh', + title: 'Refresh', + slotRenderer: () => '
      🔄 Refresh data
      ' + } + ] + } +}; +``` + +### Framework Integration Examples + +#### Vanilla JavaScript +```typescript +const menuItem = { + command: 'custom', + title: 'Action', + slotRenderer: () => ` + + ` +}; +``` + +#### Angular - Dynamic Components +```typescript +// In component class +const menuItem = { + command: 'with-component', + title: 'With Angular Component', + slotRenderer: (cmdItem, args) => { + // Create a placeholder element + const placeholder = document.createElement('div'); + placeholder.id = `angular-slot-${Date.now()}`; + + // Schedule component creation for after rendering + setTimeout(() => { + const element = document.getElementById(placeholder.id); + if (element) { + const componentRef = this.viewContainerRef.createComponent(MyComponent); + element.appendChild(componentRef.location.nativeElement); + } + }, 0); + + return placeholder; + } +}; +``` + +#### React - Using Hooks +```typescript +// Define menu item with slotRenderer +const menuItem = { + command: 'with-react', + title: 'With React Component', + slotRenderer: (cmdItem, args) => { + const container = document.createElement('div'); + container.id = `react-slot-${Date.now()}`; + + // Schedule component render for after menu renders + setTimeout(() => { + const element = document.getElementById(container.id); + if (element) { + ReactDOM.render(, element); + } + }, 0); + + return container; + } +}; +``` + +#### Vue - Using createApp +```typescript +// Define menu item with slotRenderer +const menuItem = { + command: 'with-vue', + title: 'With Vue Component', + slotRenderer: (cmdItem, args) => { + const container = document.createElement('div'); + container.id = `vue-slot-${Date.now()}`; + + // Schedule component mount for after menu renders + setTimeout(() => { + const element = document.getElementById(container.id); + if (element && !element._appInstance) { + const app = createApp(MyComponent, { data: args }); + app.mount(element); + element._appInstance = app; + } + }, 0); + + return container; + } +}; +``` + +### Real-World Use Cases + +#### 1. Add Keyboard Shortcuts +```typescript +{ + command: 'copy', + title: 'Copy', + iconCssClass: 'mdi mdi-content-copy', + slotRenderer: () => ` +
      + + Copy + Ctrl+C +
      + ` +} +``` + +#### 2. Add Status Indicators +```typescript +{ + command: 'filter', + title: 'Filter', + iconCssClass: 'mdi mdi-filter', + slotRenderer: () => ` +
      + + Filter + +
      + ` +} +``` + +#### 3. Add Dynamic Content Based on Context +```typescript +{ + command: 'edit-row', + title: 'Edit Row', + slotRenderer: (cmdItem, args) => ` +
      + + Edit Row #${args.dataContext?.id || 'N/A'} +
      + ` +} +``` + +#### 4. Add Interactive Elements +```typescript +{ + command: 'toggle-setting', + title: 'Auto Refresh', + slotRenderer: (cmdItem, args, event) => { + const container = document.createElement('label'); + container.style.display = 'flex'; + container.style.alignItems = 'center'; + container.style.gap = '8px'; + container.style.marginRight = 'auto'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.addEventListener('change', (e) => { + // Prevent menu item click from firing when toggling checkbox + event?.stopPropagation?.(); + console.log('Auto refresh:', checkbox.checked); + }); + + const label = document.createElement('span'); + label.textContent = cmdItem.title; + + container.appendChild(label); + container.appendChild(checkbox); + return container; + } +} +``` + +#### 5. Add Badges and Status Labels +```typescript +{ + command: 'export-excel', + title: 'Export as Excel', + slotRenderer: (cmdItem, args) => ` +
      + + ${cmdItem.title} + RECOMMENDED +
      + ` +} +``` + +#### 6. Gradient and Styled Icons +```typescript +{ + command: 'advanced-export', + title: 'Advanced Export', + slotRenderer: (cmdItem, args) => { + const container = document.createElement('div'); + container.style.display = 'flex'; + container.style.alignItems = 'center'; + container.style.gap = '8px'; + + const iconDiv = document.createElement('div'); + iconDiv.style.width = '20px'; + iconDiv.style.height = '20px'; + iconDiv.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; + iconDiv.style.borderRadius = '4px'; + iconDiv.style.display = 'flex'; + iconDiv.style.alignItems = 'center'; + iconDiv.style.justifyContent = 'center'; + iconDiv.style.color = 'white'; + iconDiv.style.fontSize = '12px'; + iconDiv.innerHTML = '📊'; + + const textSpan = document.createElement('span'); + textSpan.textContent = cmdItem.title; + + container.appendChild(iconDiv); + container.appendChild(textSpan); + return container; + } +} +``` + +### Notes and Best Practices + +- **HTML strings** are inserted via `innerHTML` - ensure content is sanitized if user-provided +- **HTMLElement objects** are appended directly - safer for dynamic content and allows event listeners +- **Cross-framework compatible** - works in vanilla JS, Angular, React, Vue, Aurelia using the same API +- **Priority order** - Item-level `slotRenderer` overrides menu-level `defaultMenuItemRenderer` +- **Built-in command preservation** - When overriding a built-in command (e.g., `sort-asc`, `sort-desc`, `hide`, etc.) with custom properties like `slotRenderer` or `iconCssClass`, if you don't provide an `action` callback, the library will automatically preserve and use the built-in action for that command. This means you can safely customize the appearance of built-in commands without losing their functionality. +- **Accessibility** - Include proper ARIA attributes when creating custom elements +- **Event handling** - Call `event.stopPropagation()` in interactive elements to prevent menu commands from firing +- **Default fallback** - If neither `slotRenderer` nor `defaultMenuItemRenderer` is provided, the default icon + text rendering is used +- **Performance** - Avoid heavy DOM manipulation inside renderer callbacks (they may be called multiple times) +- **Event parameter** - The optional `event` parameter is passed during click handling and allows you to control menu behavior +- **All menus supported** - This API works uniformly across Header Menu, Cell Menu, Context Menu, and Grid Menu + +### Styling Custom Menu Items + +```css +/* Example CSS for styled menu items */ +.slick-menu-item { + padding: 4px 8px; +} + +.slick-menu-item div { + display: flex; + align-items: center; + gap: 8px; +} + +.slick-menu-item kbd { + background: #f0f0f0; + border: 1px solid #ddd; + border-radius: 3px; + padding: 2px 6px; + font-size: 11px; + font-family: monospace; + color: #666; +} + +.slick-menu-item .badge { + background: #ff6b6b; + color: white; + padding: 2px 6px; + border-radius: 3px; + font-size: 9px; + font-weight: bold; + white-space: nowrap; +} + +.slick-menu-item:hover { + background: #f5f5f5; +} + +.slick-menu-item.slick-menu-item-disabled { + opacity: 0.5; + cursor: not-allowed; +} +``` + +### Migration from Static Rendering + +**Before (Static HTML Title):** +```typescript +{ + command: 'action', + title: 'Action ⭐', // Emoji embedded in title + iconCssClass: 'mdi mdi-star' +} +``` + +**After (Custom Rendering):** +```typescript +{ + command: 'action', + title: 'Action', + slotRenderer: () => ` +
      + + Action + +
      + ` +} +``` + +### Error Handling + +When creating custom renderers, handle potential errors gracefully: + +```typescript +{ + command: 'safe-render', + title: 'Safe Render', + slotRenderer: (cmdItem, args) => { + try { + if (args?.dataContext?.status === 'error') { + return `
      ❌ Error loading
      `; + } + return `
      ✓ Data loaded
      `; + } catch (error) { + console.error('Render error:', error); + return '
      Render error
      '; + } + } +} +``` 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 `
      ` to a `` which is native code, it has better accessibility (aria) support and a baseline support showing as "widely available". A fallback to `
      ` is also available in case `` doens't work for everybody (e.g. it doesn't work in Salesforce LWC, hence the available fallback) +2. Reimplementing Grid Menu to use CSS flexbox instead of using `calc(100% - 18px)` which wasn't ideal, neither customizable, but the new approach is to simply use CSS flexbox which is a much better approach to properly align everything. + +## Changes + +### Removed `@deprecated` code + +_following changes should be transparent to most users_ + +1. `applyHtmlCode()` was removed and replaced with `applyHtmlToElement()` +2. Grid Option `throwWhenFrozenNotAllViewable` was removed and replaced with `invalidColumnFreezeWidthCallback` + +### Selection Models, keeping only `SlickHybridSelectionModel` + +1. rename `rowSelectionOptions` to `selectionOptions` +2. drop both `SlickCellSelectionModel`/`SlickRowSelectionModel` and keep only `SlickHybridSelectionModel` +3. drop both `enableHybridSelection`/`enableRowSelection` merge them into a new `enableSelection` grid option + +`SlickHybridSelectionModel` was previously introduced in order to merge and allow using both Cell/Row Selections separately and/or in combo on the same grid. It was introduced in v9.x to test it out and after testing it for a few months, it's now safe to drop the older `SlickCellSelectionModel` / `SlickRowSelectionModel` models and keep only the hybrid model. Also, since we now have the Hybrid model and it's now accepting options for different selection models, I think it's better to rename `rowSelectionOptions` to `selectionOptions` since it now makes more sense with the hybrid approach. + +```diff +gridOptions = { +- enableHybridSelection: true, +- enableRowSelection: true, ++ enableSelection: true, + +- rowSelectionOptions: { ++ selectionOptions: { + selectActiveRow: false, + + // type can be: ['cell','row','mixed'] defaults to 'mixed' ++ selectionType: 'mixed', + } +}; +``` + +This change does not require any code update from the end user, but it is a change that you should probably be aware of nonetheless. The reason I decided to implement this is because I often forget myself to enable the associated flag and typically if you wanted to load the resource, then it's most probably because you also want it enabled. So for example, if your register `ExcelExportService` then the library will now auto-enable the resource with its associated flag (which in this case is `enableExcelExport:true`)... unless you already disabled the flag (or enabled) yourself, if so then the internal assignment will simply be skipped and yours will prevail. Also just to be clear, the list of auto-enabled external resources is rather small, it will auto-enable the following resources: +(ExcelExportService, PdfExportService, TextExportService, CompositeEditorComponent and RowDetailView). + +### Menu with Commands + +All menu plugins (Cell Menu, Context Menu, Header Menu and Grid Menu) now have a new `commandListBuilder: (items) => items` which now allow you to filter/sort and maybe override built-in commands. With this new feature in place, I'm deprecating all `hide...` properties and also `positionOrder` since you can now do that with the builder. You could also use the `hideCommands` which accepts an array of built-in command names. This well remove huge amount of `hide...` properties (over 30) that keeps increasing anytime a new built-in command gets added (in other words, this will simplify maintenance for both you and me).This well remove huge amount of `hide...` properties (over 30) that keeps increasing anytime a new built-in command gets added (in other words, this will simplify maintenance for both you and me). + +These are currently just deprecations in v10.x but it's strongly recommended to start using the `commandListBuilder` and/or `hideCommands` and move away from the deprecated properties which will be removed in v11.x. For example if we want to hide some built-in commands: + +```diff +gridOptions = { + gridMenu: { +- hideExportCsvCommand: true, +- hideTogglePreHeaderCommand: true, + +// via command name(s) ++ hideCommands: ['export-csv', 'toggle-preheader'], + +// or via builder ++ commandListBuilder: (cmdItems) => [...cmdItems.filter(x => x !== 'divider' && x.command !== 'export-csv' && x.command !== 'toggle-preheader')] + } +} +``` + +--- + +{% hint style="note" %} +**Info** the changes in the next few lines were all mentioned in the previous "Migration Guide v9.0". So, if you have already made these changes then you could skip the section below **but** scroll down further to read the last section "What's next? v11?". +{% endhint %} + +### Interfaces / Enums changes + +Removing most Enums and replacing them with string literal types (`type` instead of `enum` because again `type` aren't transpiled and `enum` are). Making this change will help decrease the build size by transpiling a lot less code. + +```diff +columns = [{ + id: 'age', ... +- type: FieldType.number, ++ type: 'number', +}]; +``` + +Below is a list of Enums that you need to replace with their associated string literals. A suggestion is to do a Search on any of these group name prefixes, e.g.: `FieldType.` and start replacing them + +**Hint** You can use VSCode search & replace, but make sure it's set to Regular Expression pattern + +| Search (regex) | Replace | +| ------------------------------ | -------- | +| `FieldType\.([a-z_]+)(.*)` | `'$1'$2` | + +| Enum Name | from `enum` | to string `type` | Note | +| ----------- | ------------------- | ------------------- | ---- | +| `DelimiterType` | `DelimiterType.comma` | `','` | +| | `DelimiterType.colon` | `':'` | +| | `DelimiterType.space` | `' '` | +| ... | ... | ... | +| `EventNamingStyle` | `EventNamingStyle.camelCase` | `'camelCase'` | +| | `EventNamingStyle.kebabCase` | `'kebabCase'` | +| | `EventNamingStyle.lowerCase` | `'lowerCase'` | +| ... | ... | ... | +| `FieldType` | `FieldType.boolean` | `'boolean'` | +| | `FieldType.number` | `'number'` | +| | `FieldType.dateIso` | `'dateIso'` | +| ... | ... | ... | +| `FileType` | `FileType.csv` | `'csv'` | +| | `FileType.xlsx` | `'xlsx'` | +| ... | ... | ... | +| `GridStateType` | `GridStateType.columns` | `'columns'` | +| | `GridStateType.filters` | `'filters'` | +| | `GridStateType.sorters` | `'sorters'` | +| ... | ... | ... | +| `OperatorType` | `OperatorType.greaterThan` | `'>'` or `'GT'` | See [Operator](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/enums/operator.type.ts) list for all available operators | +| | `OperatorType.lessThanOrEqual` | `'<='` or `'LE'` | +| | `OperatorType.contains` | `'Contains'` or `'CONTAINS'` | Operators are written as PascalCase | +| | `OperatorType.equal` | `'='` or `'EQ'` | +| | `OperatorType.rangeExclusive` | `'RangeExclusive'` | +| ... | ... | ... | +| `SortDirection` | `SortDirection.ASC` | `'ASC'` or `'asc'` | +| | `SortDirection.DESC` | `'DESC'` or `'desc'` | +| ... | ... | ... | + +#### renaming `editorOptions` and `filterOptions` to a more generic `options` property + +```diff +import { type MultipleSelectOption } from '@slickgrid-universal/common'; + +columnDefinitions = [{ + id: 'duration', field: 'duration', name: 'Duration', + editor: { +- editorOptions: { ++ options: { + maxHeight: 250, useSelectOptionLabelToHtml: true, + } as MultipleSelectOption, + }, + filter: { +- filterOptions: { ++ options: { + maxHeight: 250, useSelectOptionLabelToHtml: true, + } as MultipleSelectOption, + } +}]; + +// or reuse the same `options` ++ const msOptions = { maxHeight: 250, useSelectOptionLabelToHtml: true } as MultipleSelectOption; + +columnDefinitions = [{ + id: 'duration', field: 'duration', name: 'Duration', + editor: { ++ options: msOptions, + }, + filter: { ++ options: msOptions, + }, +}]; +``` + +#### renaming all `text-color-xyz` to `color-xyz` + +I decided to remove all `text-color-...` and rename them all to `color-...` which is much shorter and easier to use. + +You can do a "Search and Replace" in VSCode via Regular Expressions to replace them all easily: + +| Search | Replace | +| ------------- | -------- | +| `text-color-` | `color-` | + +For example: +```diff +- Primary Text ++ Primary Text +``` + +#### renaming all `mdi-[0-9]px` to `font-[0-9]px` + +Since I had 2 CSS utilities that do exactly the same, I'm dropping all `mdi-..px` in favor of `font-..px` because it makes more sense to represent font sizes that also work on any type of elements (not just icons). + +You can do a "Search and Replace" in VSCode via Regular Expressions to replace them all easily (**make sure to use `regex` in VSCode Search & Replace**): + +| Search (regex) | Replace | +| ---------------- | ----------- | +| `mdi-([0-9]*)px` | `font-$1px` | + +For example: +```diff +- Checkmark Icon ++ Checkmark Icon +``` + +--- + +## What's next? ...version 11? + +Wait, are you seriously talking about version 11 akready when version 10 actually just shipped? Thats right, I'm already thinking and planning ahead the next major version, which will be in about a year from now. I can already say that the main focus will be around the use of native [CSS anchor positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning) to replace JS code for positioning menus, tooltips, etc... which will help decreasing the build size by using fully native code. CSS anchoring has been around in Chrome for a while but is quite recent in Firefox, so for that reason I'm postponing it for next year. + +### Code being `@deprecated` (to be removed in the future, 2027-Q1) +#### You can already start using these new options and props (shown below) in v10.0 and above. + +Deprecating `ExtensionName` enum which will be replaced by its string literal type, for example: + +**Hint** You can use VSCode search & replace, but make sure it's set to Regular Expression pattern + +| Search (regex) | Replace | +| ------------------------------ | -------- | +| `ExtensionName\.([a-z_]+)(.*)` | `'$1'$2` | + +| Enum Name | from `enum` | to string `type` | +| ---------------- | ------------------- | ------------------- | +| `ExtensionName` | `ExtensionName.autoTooltip` | `'autoTooltip'` | +| | `ExtensionName.draggableGrouping` | `'draggableGrouping'` | +| | `ExtensionName.rowDetail` | `'rowDetail'` | +| ... | ... | ... | diff --git a/docs/migrations/migration-to-9.x.md b/docs/migrations/migration-to-9.x.md index e48159d0b5..9b5bfaa9a8 100644 --- a/docs/migrations/migration-to-9.x.md +++ b/docs/migrations/migration-to-9.x.md @@ -6,7 +6,7 @@ The other great thing about having everything under the same project umbrella is Wait, what happened to version 6 to 8? -I'm skipping versions 6-8 and going straight to v9.0 because some of the framework wrappers (Angular-Slickgrid & Aurelia-Slickgrid) were already at v8.x and so the next available major version bump available for every packages was v9.0. +I'm skipping versions 6-8 and going straight to v9.0 because some of the framework wrappers (Angular-Slickgrid & Aurelia-Slickgrid) were already at v8.x and so the next available major version bump available for every packages was v9.0. If the project is useful to you, please give it a star ⭐ (on [Slickgrid-Universal](https://github.com/ghiscoding/slickgrid-universal)) umbrella project and perhaps buy me a coffee [☕ (Ko-Fi)](https://ko-fi.com/ghiscoding), thanks in advance. @@ -161,23 +161,29 @@ Below is a list of Enums being deprecated and you should think about migrating t | `FieldType` | `FieldType.boolean` | `'boolean'` | | | `FieldType.number` | `'number'` | | | `FieldType.dateIso` | `'dateIso'` | -| | ... | ... | +| ... | ... | ... | | `FileType` | `FileType.csv` | `'csv'` | | | `FileType.xlsx` | `'xlsx'` | -| | ... | ... | +| ... | ... | ... | | `GridStateType` | `GridStateType.columns` | `'columns'` | | | `GridStateType.filters` | `'filters'` | | | `GridStateType.sorters` | `'sorters'` | -| | ... | ... | +| ... | ... | ... | | `OperatorType` | `OperatorType.greaterThan` | `'>'` or `'GT'` | See [Operator](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/enums/operator.type.ts) list for all available operators | | | `OperatorType.lessThanOrEqual` | `'<='` or `'LE'` | | | `OperatorType.contains` | `'Contains'` or `'CONTAINS'` | Operators are written as PascalCase | | | `OperatorType.equal` | `'='` or `'EQ'` | | | `OperatorType.rangeExclusive` | `'RangeExclusive'` | -| | ... | ... | +| ... | ... | ... | | `SortDirection` | `SortDirection.ASC` | `'ASC'` or `'asc'` | | | `SortDirection.DESC` | `'DESC'` or `'desc'` | -| | ... | ... | +| ... | ... | ... | + +**Hint** You can use VSCode search & replace, but make sure it's set to Regular Expression pattern + +| Search (regex) | Replace | +| -------------------------- | -------- | +| `FieldType\.([a-z_]+)(.*)` | `'$1'$2` | **Hint** You can use VSCode search & replace, but make sure it's set to Regular Expression pattern @@ -225,7 +231,7 @@ columnDefinitions = [{ ##### deprecating `text-color-xyz` and renaming them all to `color-xyz` -I decided to deprecate all `text-color-...` and renaming them all to `color-...` which is much simpler to type and use. +I decided to deprecate all `text-color-...` and renaming them all to `color-...` which is much simpler to type and use. You can do a "Search and Replace" in VSCode via Regular Expressions to replace them all easily: diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example-header-menu-slots.html b/examples/vite-demo-vanilla-bundle/src/examples/example-header-menu-slots.html new file mode 100644 index 0000000000..8c5544182c --- /dev/null +++ b/examples/vite-demo-vanilla-bundle/src/examples/example-header-menu-slots.html @@ -0,0 +1,227 @@ + + + + + + Header Menu with Slots - Example + + + +

      Header Menu with Slots - Example

      +

      This example demonstrates the new slot functionality in Header Menu items.

      + +
      + +
      +

      Slot Examples Demonstrated:

      +
        +
      • Resize by Content - Badge using slotContentAfter (HTML string)
      • +
      • Sort Commands - Keyboard shortcuts using slotContentAfter
      • +
      • Clear Sort - Status indicator using slotIconAfter (HTMLElement)
      • +
      • Custom Action - Custom renderer with complete control
      • +
      +
      + + + + diff --git a/frameworks-plugins/angular-row-detail-plugin/.oxlintrc.json b/frameworks-plugins/angular-row-detail-plugin/.oxlintrc.json new file mode 100644 index 0000000000..4beeadc11e --- /dev/null +++ b/frameworks-plugins/angular-row-detail-plugin/.oxlintrc.json @@ -0,0 +1,61 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "extends": ["../../.oxlintrc.json"], + "env": { + "es2025": true, + "browser": true, + "shared-node-browser": true + }, + "plugins": ["oxc", "typescript", "vitest"], + "jsPlugins": ["@angular-eslint/eslint-plugin"], + "ignorePatterns": ["**/*.spec.ts", "**/*.cy.ts", ".angular/*", "dist/", "coverage/", "test/", "**/environment.*.ts", "**/public_api.ts"], + "rules": { + "@angular-eslint/no-forward-ref": "error", + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "app", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "style": "kebab-case" + } + ], + "@angular-eslint/prefer-standalone": "error", + "typescript/no-unused-expressions": "off", + "typescript/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "caughtErrors": "none" + } + ], + "typescript/consistent-type-imports": "off", + "import/extensions": [ + "off", + "ignorePackages", + { + "js": "never", + "ts": "never" + } + ], + "naming-convention": [ + "error", + { + "selector": "directive", + "format": ["camelCase"], + "prefix": ["app"] + }, + { + "selector": "component", + "format": ["kebab-case"] + } + ] + } +} diff --git a/frameworks-plugins/angular-row-detail-plugin/CHANGELOG.md b/frameworks-plugins/angular-row-detail-plugin/CHANGELOG.md new file mode 100644 index 0000000000..8b321b2683 --- /dev/null +++ b/frameworks-plugins/angular-row-detail-plugin/CHANGELOG.md @@ -0,0 +1,23 @@ +# Change Log +## All-in-One SlickGrid agnostic library, visit [Slickgrid-Universal](https://github.com/ghiscoding/slickgrid-universal) 📦🚀 + +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 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) + +### 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 +* 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 +* upgrade to Angular 21 ([#2338](https://github.com/ghiscoding/slickgrid-universal/issues/2338)) ([012def2](https://github.com/ghiscoding/slickgrid-universal/commit/012def265a4e5cb0738ea211d073df129a2eecd9)) - by @ghiscoding diff --git a/frameworks-plugins/angular-row-detail-plugin/README.md b/frameworks-plugins/angular-row-detail-plugin/README.md new file mode 100644 index 0000000000..a9dd8565a6 --- /dev/null +++ b/frameworks-plugins/angular-row-detail-plugin/README.md @@ -0,0 +1,19 @@ +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) +[![lerna--lite](https://img.shields.io/badge/maintained%20with-lerna--lite-e137ff)](https://github.com/ghiscoding/lerna-lite) +[![npm](https://img.shields.io/npm/v/@slickgrid-universal/angular-row-detail-plugin.svg)](https://www.npmjs.com/package/@slickgrid-universal/angular-row-detail-plugin) +[![npm](https://img.shields.io/npm/dy/@slickgrid-universal/angular-row-detail-plugin)](https://www.npmjs.com/package/@slickgrid-universal/angular-row-detail-plugin) +[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@slickgrid-universal/angular-row-detail-plugin?color=success&label=gzip)](https://bundlephobia.com/result?p=@slickgrid-universal/angular-row-detail-plugin) + +[![Actions Status](https://github.com/ghiscoding/slickgrid-universal/workflows/CI%20Build/badge.svg)](https://github.com/ghiscoding/slickgrid-universal/actions) +[![Cypress.io](https://img.shields.io/badge/tested%20with-Cypress-04C38E.svg)](https://www.cypress.io/) +[![Vitest](https://img.shields.io/badge/tested%20with-vitest-fcc72b.svg?logo=vitest)](https://vitest.dev/) +[![codecov](https://codecov.io/gh/ghiscoding/slickgrid-universal/branch/master/graph/badge.svg)](https://codecov.io/gh/ghiscoding/slickgrid-universal) + +## Angular-Slickgrid Row Detail plugin +#### @slickgrid-universal/angular-row-detail-plugin + +Angular-Slickgrid Row Detail plugin + +### Installation +Follow the instruction provided in the main [README](https://github.com/ghiscoding/slickgrid-universal#installation) diff --git a/frameworks-plugins/angular-row-detail-plugin/package.json b/frameworks-plugins/angular-row-detail-plugin/package.json new file mode 100644 index 0000000000..9a52258f31 --- /dev/null +++ b/frameworks-plugins/angular-row-detail-plugin/package.json @@ -0,0 +1,56 @@ +{ + "name": "@slickgrid-universal/angular-row-detail-plugin", + "version": "10.0.0-beta.0", + "description": "Angular Row Detail Plugin for Angular-SlickGrid", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "/dist", + "/src" + ], + "scripts": { + "build": "pnpm run clean && tsc", + "build:incremental": "tsc --incremental --declaration", + "clean": "remove dist tsconfig.tsbuildinfo", + "dev": "pnpm build:incremental", + "test": "ng test --watch", + "test:coverage": "vitest --no-watch --coverage" + }, + "license": "MIT", + "author": "Ghislain B.", + "homepage": "https://github.com/ghiscoding/slickgrid-universal", + "repository": { + "type": "git", + "url": "git+https://github.com/ghiscoding/slickgrid-universal.git", + "directory": "packages/angular-row-detail-plugin" + }, + "bugs": { + "url": "https://github.com/ghiscoding/slickgrid-universal/issues" + }, + "dependencies": { + "@ngx-translate/core": "^17.0.0", + "@slickgrid-universal/common": "workspace:*", + "@slickgrid-universal/event-pub-sub": "workspace:*", + "@slickgrid-universal/row-detail-view-plugin": "workspace:*", + "angular-slickgrid": "workspace:*", + "rxjs": "catalog:" + }, + "devDependencies": { + "@angular-eslint/eslint-plugin": "catalog:", + "@angular/core": "^21.1.4", + "oxlint": "catalog:", + "tslib": "catalog:", + "vitest": "catalog:" + } +} diff --git a/frameworks/angular-slickgrid/src/library/extensions/slickRowDetailView.ts b/frameworks-plugins/angular-row-detail-plugin/src/angularSlickRowDetailView.ts similarity index 90% rename from frameworks/angular-slickgrid/src/library/extensions/slickRowDetailView.ts rename to frameworks-plugins/angular-row-detail-plugin/src/angularSlickRowDetailView.ts index b52d57a664..b536d6cf5f 100644 --- a/frameworks/angular-slickgrid/src/library/extensions/slickRowDetailView.ts +++ b/frameworks-plugins/angular-row-detail-plugin/src/angularSlickRowDetailView.ts @@ -5,20 +5,20 @@ import { createDomElement, SlickEventData, SlickHybridSelectionModel, - SlickRowSelectionModel, unsubscribeAll, type EventSubscription, type OnBeforeRowDetailToggleArgs, type OnRowBackOrOutOfViewportRangeArgs, + type RowDetailViewOption, type RxJsFacade, type SelectionModel, type SlickGrid, } from '@slickgrid-universal/common'; import type { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; import { SlickRowDetailView as UniversalSlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin'; +import { type AngularUtilService, type GridOption } from 'angular-slickgrid'; import { Observable, type Subject } from 'rxjs'; -import type { GridOption, RowDetailView } from '../models/index.js'; -import type { AngularUtilService } from '../services/angularUtil.service.js'; +import type { RowDetailView } from './interfaces'; const ROW_DETAIL_CONTAINER_PREFIX = 'container_'; const PRELOAD_CONTAINER_PREFIX = 'container_loading'; @@ -30,7 +30,8 @@ export interface CreatedView { rendered?: boolean; } -export class SlickRowDetailView extends UniversalSlickRowDetailView { +export class AngularSlickRowDetailView extends UniversalSlickRowDetailView { + static readonly pluginName = 'AngularSlickRowDetailView'; rowDetailContainer!: ViewContainerRef; protected _preloadComponent: Type | undefined; protected _preloadCompRef?: ComponentRef; @@ -44,12 +45,12 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { protected readonly appRef: ApplicationRef, protected readonly eventPubSubService: EventPubSubService, protected readonly gridContainerElement: HTMLDivElement, - protected rxjs?: RxJsFacade + protected rxjs?: RxJsFacade | undefined ) { super(eventPubSubService); } - get addonOptions() { + get addonOptions(): RowDetailViewOption { return this.getOptions(); } @@ -66,19 +67,19 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { return this.gridOptions.rowDetailView; } - addRxJsResource(rxjs: RxJsFacade) { + addRxJsResource(rxjs: RxJsFacade): void { this.rxjs = rxjs; } /** Dispose of the RowDetailView Extension */ - dispose() { + dispose(): void { this.disposeAllViewComponents(); this._subscriptions = unsubscribeAll(this._subscriptions); // also unsubscribe all RxJS subscriptions super.dispose(); } /** Dispose of all the opened Row Detail Panels Angular View Components */ - disposeAllViewComponents() { + disposeAllViewComponents(): void { do { const view = this._views.pop(); if (view) { @@ -88,11 +89,11 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { } /** Get the instance of the SlickGrid addon (control or plugin). */ - getAddonInstance(): SlickRowDetailView | null { + getAddonInstance(): AngularSlickRowDetailView | null { return this; } - init(grid: SlickGrid) { + init(grid: SlickGrid): void { this._grid = grid; super.init(grid); this.register(grid.getSelectionModel()); @@ -102,7 +103,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { * Create the plugin before the Grid creation, else it will behave oddly. * Mostly because the column definitions might change after the grid creation */ - register(rowSelectionPlugin?: SelectionModel) { + register(rowSelectionPlugin?: SelectionModel): AngularSlickRowDetailView { if (typeof this.gridOptions.rowDetailView?.process === 'function') { // we need to keep the user "process" method and replace it with our own execution method // we do this because when we get the item detail, we need to call "onAsyncResponse.notify" for the plugin to work @@ -127,10 +128,9 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { // this also requires the Row Selection Model to be registered as well if (!rowSelectionPlugin || !this._grid.getSelectionModel()) { - const SelectionModelClass = this.gridOptions.enableHybridSelection ? SlickHybridSelectionModel : SlickRowSelectionModel; - rowSelectionPlugin = new SelectionModelClass( - this.gridOptions.selectionOptions ?? this.gridOptions.rowSelectionOptions ?? { selectActiveRow: true } - ); + const selectionType = this.gridOptions.selectionOptions?.selectionType || 'row'; + const selectActiveRow = this.gridOptions.selectionOptions?.selectActiveRow ?? true; + rowSelectionPlugin = new SlickHybridSelectionModel({ ...this.gridOptions.selectionOptions, selectionType, selectActiveRow }); this._grid.setSelectionModel(rowSelectionPlugin); } @@ -213,7 +213,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { this.eventHandler.subscribe(this._grid.onColumnsReordered, this.redrawAllViewComponents.bind(this, false)); // on row selection changed, we also need to redraw - if (this.gridOptions.enableRowSelection || this.gridOptions.enableHybridSelection || this.gridOptions.enableCheckboxSelector) { + if (this.gridOptions.enableSelection || this.gridOptions.enableCheckboxSelector) { this.eventHandler.subscribe(this._grid.onSelectedRowsChanged, this.redrawAllViewComponents.bind(this, false)); } @@ -236,7 +236,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { } /** Redraw (re-render) all the expanded row detail View Components */ - redrawAllViewComponents(forceRedraw = false) { + redrawAllViewComponents(forceRedraw = false): void { this.resetRenderedRows(); this._views.forEach((view) => { if (!view.rendered || forceRedraw) { @@ -247,7 +247,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { } /** Redraw the necessary View Component */ - redrawViewComponent(createdView: CreatedView) { + redrawViewComponent(createdView: CreatedView): void { const containerElement = this.gridContainerElement.querySelector(`.${ROW_DETAIL_CONTAINER_PREFIX}${createdView.id}`); if (containerElement) { this.renderViewModel(createdView.dataContext); @@ -255,7 +255,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { } /** Render (or re-render) the View Component (Row Detail) */ - renderPreloadView() { + renderPreloadView(): void { const containerElement = this.gridContainerElement.querySelector(`.${PRELOAD_CONTAINER_PREFIX}`); if (this._preloadComponent && containerElement) { const preloadComp = this.angularUtilService.createAngularComponentAppendToDom( @@ -331,7 +331,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { * the plugin will then use item to populate the row detail panel with the "postTemplate" * @param item */ - protected notifyTemplate(item: any) { + protected notifyTemplate(item: any): void { this.onAsyncResponse.notify({ item }, new SlickEventData(), this); } @@ -339,7 +339,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { * On Processing, we will notify the plugin with the new item detail once backend server call completes * @param item */ - protected async onProcessing(item: any) { + protected async onProcessing(item: any): Promise { if (item && typeof this._userProcessFn === 'function') { let awaitedItemDetail: any; const userProcessFn = this._userProcessFn(item); @@ -371,7 +371,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { * if it's expanding we will add it to our View Components reference array if we don't already have it * or if it's collapsing we will remove it from our View Components reference array */ - protected handleOnBeforeRowDetailToggle(_e: SlickEventData, args: { grid: SlickGrid; item: any }) { + protected handleOnBeforeRowDetailToggle(_e: SlickEventData, args: { grid: SlickGrid; item: any }): void { // expanding if (args?.item?.__collapsed) { // expanding row detail @@ -388,7 +388,10 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { } /** When Row comes back to Viewport Range, we need to redraw the View */ - protected handleOnRowBackToViewportRange(_e: SlickEventData, args: OnRowBackOrOutOfViewportRangeArgs) { + protected handleOnRowBackToViewportRange( + _e: SlickEventData, + args: OnRowBackOrOutOfViewportRangeArgs + ): void { const viewModel = this._views.find((x) => x.id === args.rowId); if (viewModel && !viewModel.rendered) { this.redrawViewComponent(viewModel); diff --git a/frameworks-plugins/angular-row-detail-plugin/src/index.ts b/frameworks-plugins/angular-row-detail-plugin/src/index.ts new file mode 100644 index 0000000000..568ec06053 --- /dev/null +++ b/frameworks-plugins/angular-row-detail-plugin/src/index.ts @@ -0,0 +1 @@ +export * from './angularSlickRowDetailView'; diff --git a/frameworks-plugins/angular-row-detail-plugin/src/interfaces.ts b/frameworks-plugins/angular-row-detail-plugin/src/interfaces.ts new file mode 100644 index 0000000000..f2f9d89017 --- /dev/null +++ b/frameworks-plugins/angular-row-detail-plugin/src/interfaces.ts @@ -0,0 +1,16 @@ +import type { Type } from '@angular/core'; +import type { RowDetailView as UniversalRowDetailView } from '@slickgrid-universal/common'; + +export interface RowDetailView extends UniversalRowDetailView { + /** + * Optionally pass your Parent Component reference to your Child Component (row detail component). + * note:: If anyone finds a better way of passing the parent to the row detail extension, please reach out and/or create a PR + */ + parentRef?: any; + + /** View Component of the preload template (typically a spinner) which shows after opening on the row detail but before the row detail is ready */ + preloadComponent?: Type; + + /** View Component that will be loaded in the row detail after the async function completed */ + viewComponent: Type; +} diff --git a/frameworks-plugins/angular-row-detail-plugin/tsconfig.json b/frameworks-plugins/angular-row-detail-plugin/tsconfig.json new file mode 100644 index 0000000000..5b4bd341dd --- /dev/null +++ b/frameworks-plugins/angular-row-detail-plugin/tsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "pretty": true, + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2022", + "lib": ["esnext", "dom"], + "typeRoots": ["../types", "../../node_modules/@types"], + "declaration": true, + "declarationDir": "dist", + "esModuleInterop": true, + "inlineSources": true, + "noImplicitAny": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmitHelpers": true, + "importHelpers": true, + "skipLibCheck": true, + "sourceMap": true, + "newLine": "lf", + "outDir": "dist" + }, + "include": ["src"], + "exclude": [".vscode", "dist", "doc", "node_modules", "test", "**/*.spec.ts"], + "angularCompilerOptions": { + "compilationMode": "partial", + "preserveWhitespaces": true, + "strictMetadataEmit": true, + "strictTemplates": true, + "extendedDiagnostics": { + "checks": { + "optionalChainNotNullable": "suppress" + } + } + } +} diff --git a/frameworks-plugins/aurelia-row-detail-plugin/CHANGELOG.md b/frameworks-plugins/aurelia-row-detail-plugin/CHANGELOG.md new file mode 100644 index 0000000000..a147049b3e --- /dev/null +++ b/frameworks-plugins/aurelia-row-detail-plugin/CHANGELOG.md @@ -0,0 +1,19 @@ +# Change Log +## All-in-One SlickGrid agnostic library, visit [Slickgrid-Universal](https://github.com/ghiscoding/slickgrid-universal) 📦🚀 + +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) + +### 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 diff --git a/frameworks-plugins/aurelia-row-detail-plugin/README.md b/frameworks-plugins/aurelia-row-detail-plugin/README.md new file mode 100644 index 0000000000..451d83f21d --- /dev/null +++ b/frameworks-plugins/aurelia-row-detail-plugin/README.md @@ -0,0 +1,19 @@ +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) +[![lerna--lite](https://img.shields.io/badge/maintained%20with-lerna--lite-e137ff)](https://github.com/ghiscoding/lerna-lite) +[![npm](https://img.shields.io/npm/v/@slickgrid-universal/aurelia-row-detail-plugin.svg)](https://www.npmjs.com/package/@slickgrid-universal/aurelia-row-detail-plugin) +[![npm](https://img.shields.io/npm/dy/@slickgrid-universal/aurelia-row-detail-plugin)](https://www.npmjs.com/package/@slickgrid-universal/aurelia-row-detail-plugin) +[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@slickgrid-universal/aurelia-row-detail-plugin?color=success&label=gzip)](https://bundlephobia.com/result?p=@slickgrid-universal/aurelia-row-detail-plugin) + +[![Actions Status](https://github.com/ghiscoding/slickgrid-universal/workflows/CI%20Build/badge.svg)](https://github.com/ghiscoding/slickgrid-universal/actions) +[![Cypress.io](https://img.shields.io/badge/tested%20with-Cypress-04C38E.svg)](https://www.cypress.io/) +[![Vitest](https://img.shields.io/badge/tested%20with-vitest-fcc72b.svg?logo=vitest)](https://vitest.dev/) +[![codecov](https://codecov.io/gh/ghiscoding/slickgrid-universal/branch/master/graph/badge.svg)](https://codecov.io/gh/ghiscoding/slickgrid-universal) + +## Aurelia-Slickgrid Row Detail plugin +#### @slickgrid-universal/aurelia-row-detail-plugin + +Aurelia-Slickgrid Row Detail plugin + +### Installation +Follow the instruction provided in the main [README](https://github.com/ghiscoding/slickgrid-universal#installation) diff --git a/frameworks-plugins/aurelia-row-detail-plugin/package.json b/frameworks-plugins/aurelia-row-detail-plugin/package.json new file mode 100644 index 0000000000..ace179b231 --- /dev/null +++ b/frameworks-plugins/aurelia-row-detail-plugin/package.json @@ -0,0 +1,51 @@ +{ + "name": "@slickgrid-universal/aurelia-row-detail-plugin", + "version": "10.0.0-beta.0", + "description": "Aurelia Row Detail Plugin for Aurelia-SlickGrid", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "/dist", + "/src" + ], + "scripts": { + "build": "pnpm run clean && tsc", + "build:incremental": "tsc --incremental --declaration", + "clean": "remove dist tsconfig.tsbuildinfo", + "dev": "pnpm build:incremental", + "test": "ng test --watch", + "test:coverage": "vitest --no-watch --coverage" + }, + "license": "MIT", + "author": "Ghislain B.", + "homepage": "https://github.com/ghiscoding/slickgrid-universal", + "repository": { + "type": "git", + "url": "git+https://github.com/ghiscoding/slickgrid-universal.git", + "directory": "packages/aurelia-row-detail-plugin" + }, + "bugs": { + "url": "https://github.com/ghiscoding/slickgrid-universal/issues" + }, + "dependencies": { + "@aurelia/kernel": "^2.0.0-beta.25", + "@aurelia/runtime-html": "^2.0.0-beta.25", + "@slickgrid-universal/common": "workspace:*", + "@slickgrid-universal/event-pub-sub": "workspace:*", + "@slickgrid-universal/row-detail-view-plugin": "workspace:*", + "aurelia-slickgrid": "workspace:*", + "rxjs": "catalog:", + "tslib": "catalog:" + } +} diff --git a/frameworks/aurelia-slickgrid/src/extensions/slickRowDetailView.ts b/frameworks-plugins/aurelia-row-detail-plugin/src/aureliaSlickRowDetailView.ts similarity index 93% rename from frameworks/aurelia-slickgrid/src/extensions/slickRowDetailView.ts rename to frameworks-plugins/aurelia-row-detail-plugin/src/aureliaSlickRowDetailView.ts index ea79152086..47712e7eda 100644 --- a/frameworks/aurelia-slickgrid/src/extensions/slickRowDetailView.ts +++ b/frameworks-plugins/aurelia-row-detail-plugin/src/aureliaSlickRowDetailView.ts @@ -1,10 +1,10 @@ +import { transient, type Constructable } from '@aurelia/kernel'; import type { ICustomElementController } from '@aurelia/runtime-html'; import { addToArrayWhenNotExists, createDomElement, SlickEventData, SlickHybridSelectionModel, - SlickRowSelectionModel, unsubscribeAll, type EventSubscription, type OnBeforeRowDetailToggleArgs, @@ -14,15 +14,15 @@ import { } from '@slickgrid-universal/common'; import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; import { SlickRowDetailView as UniversalSlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin'; -import { resolve, transient, type Constructable } from 'aurelia'; -import type { CreatedView, GridOption, RowDetailView, ViewModelBindableInputData } from '../models/index.js'; -import { AureliaUtilService } from '../services/aureliaUtil.service.js'; +import type { AureliaUtilService, CreatedView, GridOption, ViewModelBindableInputData } from 'aurelia-slickgrid'; +import type { RowDetailView } from './interfaces'; const ROW_DETAIL_CONTAINER_PREFIX = 'container_'; const PRELOAD_CONTAINER_PREFIX = 'container_loading'; @transient() -export class SlickRowDetailView extends UniversalSlickRowDetailView { +export class AureliaSlickRowDetailView extends UniversalSlickRowDetailView { + static readonly pluginName = 'AureliaSlickRowDetailView'; protected _preloadViewModel?: Constructable; protected _preloadController?: ICustomElementController; protected _slots: CreatedView[] = []; @@ -31,9 +31,9 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { protected _viewModel?: Constructable; constructor( - protected readonly aureliaUtilService: AureliaUtilService = resolve(AureliaUtilService), - private readonly eventPubSubService: EventPubSubService = resolve(EventPubSubService), - private readonly gridContainerElement: HTMLElement = resolve(HTMLElement) + protected readonly aureliaUtilService: AureliaUtilService, + protected readonly eventPubSubService: EventPubSubService, + protected readonly gridContainerElement: HTMLElement ) { super(eventPubSubService); } @@ -72,7 +72,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { } /** Get the instance of the SlickGrid addon (control or plugin). */ - getAddonInstance(): SlickRowDetailView | null { + getAddonInstance(): AureliaSlickRowDetailView | null { return this; } @@ -112,10 +112,9 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { if (this._grid && this.gridOptions) { // this also requires the Row Selection Model to be registered as well if (!rowSelectionPlugin || !this._grid.getSelectionModel()) { - const SelectionModelClass = this.gridOptions.enableHybridSelection ? SlickHybridSelectionModel : SlickRowSelectionModel; - rowSelectionPlugin = new SelectionModelClass( - this.gridOptions.selectionOptions ?? this.gridOptions.rowSelectionOptions ?? { selectActiveRow: true } - ); + const selectionType = this.gridOptions.selectionOptions?.selectionType || 'row'; + const selectActiveRow = this.gridOptions.selectionOptions?.selectActiveRow ?? true; + rowSelectionPlugin = new SlickHybridSelectionModel({ ...this.gridOptions.selectionOptions, selectionType, selectActiveRow }); this._grid.setSelectionModel(rowSelectionPlugin); } @@ -195,7 +194,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { this._eventHandler.subscribe(this._grid.onColumnsReordered, this.redrawAllViewSlots.bind(this, false)); // on row selection changed, we also need to redraw - if (this.gridOptions.enableRowSelection || this.gridOptions.enableHybridSelection || this.gridOptions.enableCheckboxSelector) { + if (this.gridOptions.enableSelection || this.gridOptions.enableCheckboxSelector) { this._eventHandler.subscribe(this._grid.onSelectedRowsChanged, this.redrawAllViewSlots.bind(this, false)); } diff --git a/frameworks-plugins/aurelia-row-detail-plugin/src/index.ts b/frameworks-plugins/aurelia-row-detail-plugin/src/index.ts new file mode 100644 index 0000000000..21b3702077 --- /dev/null +++ b/frameworks-plugins/aurelia-row-detail-plugin/src/index.ts @@ -0,0 +1 @@ +export * from './aureliaSlickRowDetailView.js'; diff --git a/frameworks-plugins/aurelia-row-detail-plugin/src/interfaces.ts b/frameworks-plugins/aurelia-row-detail-plugin/src/interfaces.ts new file mode 100644 index 0000000000..7c257b7437 --- /dev/null +++ b/frameworks-plugins/aurelia-row-detail-plugin/src/interfaces.ts @@ -0,0 +1,16 @@ +import type { Constructable } from '@aurelia/kernel'; +import type { RowDetailView as UniversalRowDetailView } from '@slickgrid-universal/common'; + +export interface RowDetailView extends UniversalRowDetailView { + /** + * Optionally pass your Parent Component reference to your Child Component (row detail component). + * note:: If anyone finds a better way of passing the parent to the row detail extension, please reach out and/or create a PR + */ + parentRef?: any; + + /** View Model of the preload template which shows after opening row detail & before row detail data shows up */ + preloadViewModel?: Constructable; + + /** View Model template that will be loaded once the async function finishes */ + viewModel?: Constructable; +} diff --git a/frameworks-plugins/aurelia-row-detail-plugin/tsconfig.json b/frameworks-plugins/aurelia-row-detail-plugin/tsconfig.json new file mode 100644 index 0000000000..201c1b1dc0 --- /dev/null +++ b/frameworks-plugins/aurelia-row-detail-plugin/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "pretty": true, + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2022", + "lib": ["esnext", "dom"], + "typeRoots": ["../types", "../../node_modules/@types"], + "declaration": true, + "declarationDir": "dist", + "esModuleInterop": true, + "inlineSources": true, + "noImplicitAny": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmitHelpers": true, + "importHelpers": true, + "skipLibCheck": true, + "sourceMap": true, + "newLine": "lf", + "outDir": "dist" + }, + "include": ["src"], + "exclude": [".vscode", "dist", "doc", "node_modules", "test", "**/*.spec.ts"] +} diff --git a/frameworks-plugins/react-row-detail-plugin/CHANGELOG.md b/frameworks-plugins/react-row-detail-plugin/CHANGELOG.md new file mode 100644 index 0000000000..a147049b3e --- /dev/null +++ b/frameworks-plugins/react-row-detail-plugin/CHANGELOG.md @@ -0,0 +1,19 @@ +# Change Log +## All-in-One SlickGrid agnostic library, visit [Slickgrid-Universal](https://github.com/ghiscoding/slickgrid-universal) 📦🚀 + +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) + +### 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 diff --git a/frameworks-plugins/react-row-detail-plugin/README.md b/frameworks-plugins/react-row-detail-plugin/README.md new file mode 100644 index 0000000000..01a02fee7e --- /dev/null +++ b/frameworks-plugins/react-row-detail-plugin/README.md @@ -0,0 +1,19 @@ +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) +[![lerna--lite](https://img.shields.io/badge/maintained%20with-lerna--lite-e137ff)](https://github.com/ghiscoding/lerna-lite) +[![npm](https://img.shields.io/npm/v/@slickgrid-universal/react-row-detail-plugin.svg)](https://www.npmjs.com/package/@slickgrid-universal/react-row-detail-plugin) +[![npm](https://img.shields.io/npm/dy/@slickgrid-universal/react-row-detail-plugin)](https://www.npmjs.com/package/@slickgrid-universal/react-row-detail-plugin) +[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@slickgrid-universal/react-row-detail-plugin?color=success&label=gzip)](https://bundlephobia.com/result?p=@slickgrid-universal/react-row-detail-plugin) + +[![Actions Status](https://github.com/ghiscoding/slickgrid-universal/workflows/CI%20Build/badge.svg)](https://github.com/ghiscoding/slickgrid-universal/actions) +[![Cypress.io](https://img.shields.io/badge/tested%20with-Cypress-04C38E.svg)](https://www.cypress.io/) +[![Vitest](https://img.shields.io/badge/tested%20with-vitest-fcc72b.svg?logo=vitest)](https://vitest.dev/) +[![codecov](https://codecov.io/gh/ghiscoding/slickgrid-universal/branch/master/graph/badge.svg)](https://codecov.io/gh/ghiscoding/slickgrid-universal) + +## Slickgrid-React Row Detail plugin +#### @slickgrid-universal/react-row-detail-plugin + +Slickgrid-React Row Detail plugin + +### Installation +Follow the instruction provided in the main [README](https://github.com/ghiscoding/slickgrid-universal#installation) diff --git a/frameworks-plugins/react-row-detail-plugin/package.json b/frameworks-plugins/react-row-detail-plugin/package.json new file mode 100644 index 0000000000..d601835442 --- /dev/null +++ b/frameworks-plugins/react-row-detail-plugin/package.json @@ -0,0 +1,54 @@ +{ + "name": "@slickgrid-universal/react-row-detail-plugin", + "version": "10.0.0-beta.0", + "description": "React Row Detail Plugin for SlickGrid-React", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "/dist", + "/src" + ], + "scripts": { + "build": "pnpm run clean && tsc", + "build:incremental": "tsc --incremental --declaration", + "clean": "remove dist tsconfig.tsbuildinfo", + "dev": "pnpm build:incremental", + "test": "ng test --watch", + "test:coverage": "vitest --no-watch --coverage" + }, + "license": "MIT", + "author": "Ghislain B.", + "homepage": "https://github.com/ghiscoding/slickgrid-universal", + "repository": { + "type": "git", + "url": "git+https://github.com/ghiscoding/slickgrid-universal.git", + "directory": "packages/react-row-detail-plugin" + }, + "bugs": { + "url": "https://github.com/ghiscoding/slickgrid-universal/issues" + }, + "dependencies": { + "@slickgrid-universal/common": "workspace:*", + "@slickgrid-universal/event-pub-sub": "workspace:*", + "@slickgrid-universal/row-detail-view-plugin": "workspace:*", + "react": "catalog:", + "react-dom": "catalog:", + "slickgrid-react": "workspace:*", + "tslib": "catalog:" + }, + "devDependencies": { + "@types/react": "catalog:", + "@types/react-dom": "catalog:" + } +} diff --git a/frameworks-plugins/react-row-detail-plugin/src/index.ts b/frameworks-plugins/react-row-detail-plugin/src/index.ts new file mode 100644 index 0000000000..98dc2918f0 --- /dev/null +++ b/frameworks-plugins/react-row-detail-plugin/src/index.ts @@ -0,0 +1 @@ +export * from './reactSlickRowDetailView.js'; diff --git a/frameworks-plugins/react-row-detail-plugin/src/interfaces.ts b/frameworks-plugins/react-row-detail-plugin/src/interfaces.ts new file mode 100644 index 0000000000..e68e9988ae --- /dev/null +++ b/frameworks-plugins/react-row-detail-plugin/src/interfaces.ts @@ -0,0 +1,15 @@ +import type { RowDetailView as UniversalRowDetailView } from '@slickgrid-universal/common'; + +export interface RowDetailView extends UniversalRowDetailView { + /** + * Optionally pass your Parent Component reference to your Child Component (row detail component). + * note:: If anyone finds a better way of passing the parent to the row detail extension, please reach out and/or create a PR + */ + parentRef?: any; + + /** View Model of the preload template which shows after opening row detail & before row detail data shows up */ + preloadComponent?: any; + + /** View Model template that will be loaded once the async function finishes */ + viewComponent?: any; +} diff --git a/frameworks/slickgrid-react/src/extensions/slickRowDetailView.ts b/frameworks-plugins/react-row-detail-plugin/src/reactSlickRowDetailView.ts similarity index 94% rename from frameworks/slickgrid-react/src/extensions/slickRowDetailView.ts rename to frameworks-plugins/react-row-detail-plugin/src/reactSlickRowDetailView.ts index 2e02b63a81..6bc2fafc91 100644 --- a/frameworks/slickgrid-react/src/extensions/slickRowDetailView.ts +++ b/frameworks-plugins/react-row-detail-plugin/src/reactSlickRowDetailView.ts @@ -3,7 +3,6 @@ import { createDomElement, SlickEventData, SlickHybridSelectionModel, - SlickRowSelectionModel, unsubscribeAll, type EventSubscription, type OnBeforeRowDetailToggleArgs, @@ -14,8 +13,8 @@ import { import { type EventPubSubService } from '@slickgrid-universal/event-pub-sub'; import { SlickRowDetailView as UniversalSlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin'; import type { Root } from 'react-dom/client'; -import type { GridOption, RowDetailView, ViewModelBindableInputData } from '../models/index.js'; -import { createReactComponentDynamically } from '../services/reactUtils.js'; +import { createReactComponentDynamically, type GridOption, type ViewModelBindableInputData } from 'slickgrid-react'; +import type { RowDetailView } from './interfaces'; const ROW_DETAIL_CONTAINER_PREFIX = 'container_'; const PRELOAD_CONTAINER_PREFIX = 'container_loading'; @@ -26,9 +25,9 @@ export interface CreatedView { root: Root | null; rendered?: boolean; } -// interface SRDV extends React.Component, UniversalSlickRowDetailView {}s -export class SlickRowDetailView extends UniversalSlickRowDetailView { +export class ReactSlickRowDetailView extends UniversalSlickRowDetailView { + static readonly pluginName = 'ReactSlickRowDetailView'; protected _component?: any; protected _preloadComponent?: any; protected _preloadRoot?: Root; @@ -76,7 +75,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { } /** Get the instance of the SlickGrid addon (control or plugin). */ - getAddonInstance(): SlickRowDetailView | null { + getAddonInstance(): ReactSlickRowDetailView | null { return this; } @@ -117,10 +116,9 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { if (this._grid && this.gridOptions) { // this also requires the Row Selection Model to be registered as well if (!rowSelectionPlugin || !this._grid.getSelectionModel()) { - const SelectionModelClass = this.gridOptions.enableHybridSelection ? SlickHybridSelectionModel : SlickRowSelectionModel; - rowSelectionPlugin = new SelectionModelClass( - this.gridOptions.selectionOptions ?? this.gridOptions.rowSelectionOptions ?? { selectActiveRow: true } - ); + const selectionType = this.gridOptions.selectionOptions?.selectionType || 'row'; + const selectActiveRow = this.gridOptions.selectionOptions?.selectActiveRow ?? true; + rowSelectionPlugin = new SlickHybridSelectionModel({ ...this.gridOptions.selectionOptions, selectionType, selectActiveRow }); this._grid.setSelectionModel(rowSelectionPlugin); } @@ -200,7 +198,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { this.eventHandler.subscribe(this._grid.onColumnsReordered, () => this.redrawAllViewComponents(false)); // on row selection changed, we also need to redraw - if (this.gridOptions.enableRowSelection || this.gridOptions.enableHybridSelection || this.gridOptions.enableCheckboxSelector) { + if (this.gridOptions.enableSelection || this.gridOptions.enableCheckboxSelector) { this._eventHandler.subscribe(this._grid.onSelectedRowsChanged, () => this.redrawAllViewComponents(false)); } diff --git a/frameworks-plugins/react-row-detail-plugin/tsconfig.json b/frameworks-plugins/react-row-detail-plugin/tsconfig.json new file mode 100644 index 0000000000..201c1b1dc0 --- /dev/null +++ b/frameworks-plugins/react-row-detail-plugin/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "pretty": true, + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2022", + "lib": ["esnext", "dom"], + "typeRoots": ["../types", "../../node_modules/@types"], + "declaration": true, + "declarationDir": "dist", + "esModuleInterop": true, + "inlineSources": true, + "noImplicitAny": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmitHelpers": true, + "importHelpers": true, + "skipLibCheck": true, + "sourceMap": true, + "newLine": "lf", + "outDir": "dist" + }, + "include": ["src"], + "exclude": [".vscode", "dist", "doc", "node_modules", "test", "**/*.spec.ts"] +} diff --git a/frameworks-plugins/vue-row-detail-plugin/CHANGELOG.md b/frameworks-plugins/vue-row-detail-plugin/CHANGELOG.md new file mode 100644 index 0000000000..a147049b3e --- /dev/null +++ b/frameworks-plugins/vue-row-detail-plugin/CHANGELOG.md @@ -0,0 +1,19 @@ +# Change Log +## All-in-One SlickGrid agnostic library, visit [Slickgrid-Universal](https://github.com/ghiscoding/slickgrid-universal) 📦🚀 + +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) + +### 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 diff --git a/frameworks-plugins/vue-row-detail-plugin/README.md b/frameworks-plugins/vue-row-detail-plugin/README.md new file mode 100644 index 0000000000..d67f38d224 --- /dev/null +++ b/frameworks-plugins/vue-row-detail-plugin/README.md @@ -0,0 +1,19 @@ +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) +[![lerna--lite](https://img.shields.io/badge/maintained%20with-lerna--lite-e137ff)](https://github.com/ghiscoding/lerna-lite) +[![npm](https://img.shields.io/npm/v/@slickgrid-universal/vue-row-detail-plugin.svg)](https://www.npmjs.com/package/@slickgrid-universal/vue-row-detail-plugin) +[![npm](https://img.shields.io/npm/dy/@slickgrid-universal/vue-row-detail-plugin)](https://www.npmjs.com/package/@slickgrid-universal/vue-row-detail-plugin) +[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@slickgrid-universal/vue-row-detail-plugin?color=success&label=gzip)](https://bundlephobia.com/result?p=@slickgrid-universal/vue-row-detail-plugin) + +[![Actions Status](https://github.com/ghiscoding/slickgrid-universal/workflows/CI%20Build/badge.svg)](https://github.com/ghiscoding/slickgrid-universal/actions) +[![Cypress.io](https://img.shields.io/badge/tested%20with-Cypress-04C38E.svg)](https://www.cypress.io/) +[![Vitest](https://img.shields.io/badge/tested%20with-vitest-fcc72b.svg?logo=vitest)](https://vitest.dev/) +[![codecov](https://codecov.io/gh/ghiscoding/slickgrid-universal/branch/master/graph/badge.svg)](https://codecov.io/gh/ghiscoding/slickgrid-universal) + +## Slickgrid-Vue Row Detail plugin +#### @slickgrid-universal/vue-row-detail-plugin + +Slickgrid-Vue Row Detail plugin + +### Installation +Follow the instruction provided in the main [README](https://github.com/ghiscoding/slickgrid-universal#installation) diff --git a/frameworks-plugins/vue-row-detail-plugin/package.json b/frameworks-plugins/vue-row-detail-plugin/package.json new file mode 100644 index 0000000000..416f535351 --- /dev/null +++ b/frameworks-plugins/vue-row-detail-plugin/package.json @@ -0,0 +1,49 @@ +{ + "name": "@slickgrid-universal/vue-row-detail-plugin", + "version": "10.0.0-beta.0", + "description": "Vue Row Detail Plugin for SlickGrid-Vue", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "/dist", + "/src" + ], + "scripts": { + "build": "pnpm run clean && tsc", + "build:incremental": "tsc --incremental --declaration", + "clean": "remove dist tsconfig.tsbuildinfo", + "dev": "pnpm build:incremental", + "test": "ng test --watch", + "test:coverage": "vitest --no-watch --coverage" + }, + "license": "MIT", + "author": "Ghislain B.", + "homepage": "https://github.com/ghiscoding/slickgrid-universal", + "repository": { + "type": "git", + "url": "git+https://github.com/ghiscoding/slickgrid-universal.git", + "directory": "packages/vue-row-detail-plugin" + }, + "bugs": { + "url": "https://github.com/ghiscoding/slickgrid-universal/issues" + }, + "dependencies": { + "@slickgrid-universal/common": "workspace:*", + "@slickgrid-universal/event-pub-sub": "workspace:*", + "@slickgrid-universal/row-detail-view-plugin": "workspace:*", + "slickgrid-vue": "workspace:*", + "tslib": "catalog:", + "vue": "catalog:" + } +} diff --git a/frameworks-plugins/vue-row-detail-plugin/src/index.ts b/frameworks-plugins/vue-row-detail-plugin/src/index.ts new file mode 100644 index 0000000000..b1d55d2a6b --- /dev/null +++ b/frameworks-plugins/vue-row-detail-plugin/src/index.ts @@ -0,0 +1 @@ +export * from './vueSlickRowDetailView.js'; diff --git a/frameworks-plugins/vue-row-detail-plugin/src/interfaces.ts b/frameworks-plugins/vue-row-detail-plugin/src/interfaces.ts new file mode 100644 index 0000000000..c3b3a7c4d6 --- /dev/null +++ b/frameworks-plugins/vue-row-detail-plugin/src/interfaces.ts @@ -0,0 +1,16 @@ +import type { RowDetailView as UniversalRowDetailView } from '@slickgrid-universal/common'; +import type { DefineComponent } from 'vue'; + +export interface RowDetailView extends UniversalRowDetailView { + /** + * Optionally pass your Parent Component reference to your Child Component (row detail component). + * note:: If anyone finds a better way of passing the parent to the row detail extension, please reach out and/or create a PR + */ + parentRef?: any; + + /** View Model of the preload template which shows after opening row detail & before row detail data shows up */ + preloadComponent?: DefineComponent; + + /** View Model template that will be loaded once the async function finishes */ + viewComponent?: DefineComponent; +} diff --git a/frameworks/slickgrid-vue/src/extensions/slickRowDetailView.ts b/frameworks-plugins/vue-row-detail-plugin/src/vueSlickRowDetailView.ts similarity index 95% rename from frameworks/slickgrid-vue/src/extensions/slickRowDetailView.ts rename to frameworks-plugins/vue-row-detail-plugin/src/vueSlickRowDetailView.ts index c4f1a27e42..e21a93a84e 100644 --- a/frameworks/slickgrid-vue/src/extensions/slickRowDetailView.ts +++ b/frameworks-plugins/vue-row-detail-plugin/src/vueSlickRowDetailView.ts @@ -3,7 +3,6 @@ import { emptyElement, SlickEventData, SlickHybridSelectionModel, - SlickRowSelectionModel, unsubscribeAll, type EventSubscription, type OnBeforeRowDetailToggleArgs, @@ -13,8 +12,9 @@ import { } from '@slickgrid-universal/common'; import { type EventPubSubService } from '@slickgrid-universal/event-pub-sub'; import { SlickRowDetailView as UniversalSlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin'; +import type { GridOption, ViewModelBindableInputData } from 'slickgrid-vue'; import { createApp, type App, type ComponentPublicInstance } from 'vue'; -import type { GridOption, RowDetailView, ViewModelBindableInputData } from '../models/index.js'; +import type { RowDetailView } from './interfaces'; const ROW_DETAIL_CONTAINER_PREFIX = 'container_'; const PRELOAD_CONTAINER_PREFIX = 'container_loading'; @@ -28,7 +28,8 @@ export interface CreatedView { instance: ComponentPublicInstance | null; } -export class SlickRowDetailView extends UniversalSlickRowDetailView { +export class VueSlickRowDetailView extends UniversalSlickRowDetailView { + static readonly pluginName = 'VueSlickRowDetailView'; protected _component?: any; protected _preloadComponent?: any; protected _preloadApp?: App; @@ -85,7 +86,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { } /** Get the instance of the SlickGrid addon (control or plugin). */ - getAddonInstance(): SlickRowDetailView | null { + getAddonInstance(): VueSlickRowDetailView | null { return this; } @@ -126,10 +127,9 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { if (this._grid && this.gridOptions) { // this also requires the Row Selection Model to be registered as well if (!rowSelectionPlugin || !this._grid.getSelectionModel()) { - const SelectionModelClass = this.gridOptions.enableHybridSelection ? SlickHybridSelectionModel : SlickRowSelectionModel; - rowSelectionPlugin = new SelectionModelClass( - this.gridOptions.selectionOptions ?? this.gridOptions.rowSelectionOptions ?? { selectActiveRow: true } - ); + const selectionType = this.gridOptions.selectionOptions?.selectionType || 'row'; + const selectActiveRow = this.gridOptions.selectionOptions?.selectActiveRow ?? true; + rowSelectionPlugin = new SlickHybridSelectionModel({ ...this.gridOptions.selectionOptions, selectionType, selectActiveRow }); this._grid.setSelectionModel(rowSelectionPlugin); } @@ -209,7 +209,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView { this.eventHandler.subscribe(this._grid.onColumnsReordered, () => this.redrawAllViewComponents(false)); // on row selection changed, we also need to redraw - if (this.gridOptions.enableRowSelection || this.gridOptions.enableHybridSelection || this.gridOptions.enableCheckboxSelector) { + if (this.gridOptions.enableSelection || this.gridOptions.enableCheckboxSelector) { this._eventHandler.subscribe(this._grid.onSelectedRowsChanged, () => this.redrawAllViewComponents(false)); } diff --git a/frameworks-plugins/vue-row-detail-plugin/tsconfig.json b/frameworks-plugins/vue-row-detail-plugin/tsconfig.json new file mode 100644 index 0000000000..201c1b1dc0 --- /dev/null +++ b/frameworks-plugins/vue-row-detail-plugin/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "pretty": true, + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2022", + "lib": ["esnext", "dom"], + "typeRoots": ["../types", "../../node_modules/@types"], + "declaration": true, + "declarationDir": "dist", + "esModuleInterop": true, + "inlineSources": true, + "noImplicitAny": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmitHelpers": true, + "importHelpers": true, + "skipLibCheck": true, + "sourceMap": true, + "newLine": "lf", + "outDir": "dist" + }, + "include": ["src"], + "exclude": [".vscode", "dist", "doc", "node_modules", "test", "**/*.spec.ts"] +} diff --git a/frameworks/angular-slickgrid/.oxlintrc.json b/frameworks/angular-slickgrid/.oxlintrc.json index 80033b8200..c81394cf72 100644 --- a/frameworks/angular-slickgrid/.oxlintrc.json +++ b/frameworks/angular-slickgrid/.oxlintrc.json @@ -23,10 +23,11 @@ "error", { "type": "element", + "prefix": ["app", "angular-slickgrid", "swt", "custom"], "style": "kebab-case" } ], - "@angular-eslint/prefer-standalone": "off", + "@angular-eslint/prefer-standalone": "error", "typescript/no-unused-expressions": "off", "typescript/no-unused-vars": [ "error", diff --git a/frameworks/angular-slickgrid/CHANGELOG.md b/frameworks/angular-slickgrid/CHANGELOG.md index 7df9e787c7..7a6c384b3f 100644 --- a/frameworks/angular-slickgrid/CHANGELOG.md +++ b/frameworks/angular-slickgrid/CHANGELOG.md @@ -4,6 +4,49 @@ 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) +* 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 +* 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 +* 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 + +* **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 +* 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/frameworks/angular-slickgrid/README.md b/frameworks/angular-slickgrid/README.md index 71017a1ab7..e57c367796 100644 --- a/frameworks/angular-slickgrid/README.md +++ b/frameworks/angular-slickgrid/README.md @@ -112,6 +112,7 @@ Make sure to check out the [Releases](https://github.com/ghiscoding/slickgrid-un | Angular-Slickgrid | Angular | Migration Guide | Notes | Date | | :----------------:|:-------:|-----------------| ------| ---- | +| 10.x | >=21.0 | [Migration 10.x](https://ghiscoding.gitbook.io/angular-slickgrid/migrations/migration-to-10.x) | Smaller code, requires Slickgrid-Universal [10.x](https://github.com/ghiscoding/slickgrid-universal/releases/tag/v10.0.0) | TBD | | 9.x | >=19.0 | [Migration 9.x](https://ghiscoding.gitbook.io/angular-slickgrid/migrations/migration-to-9.x) | ESM-Only, requires Slickgrid-Universal [9.x](https://github.com/ghiscoding/slickgrid-universal/releases/tag/v9.0.0) | 2025-05-10 | | 8.x | >=18.0 | [Migration 8.x](https://ghiscoding.gitbook.io/angular-slickgrid/migrations/migration-to-8.x) | Modern UI / Dark Mode, requires Slickgrid-Universal [5.x](https://github.com/ghiscoding/slickgrid-universal/releases/tag/v5.0.0) | 2024-05-22 | | 7.x | >=17.0 | [Migration 7.x](https://ghiscoding.gitbook.io/angular-slickgrid/migrations/migration-to-7.x) | merge SlickGrid into Slickgrid-Universal,
      requires Slickgrid-Universal [4.x](https://github.com/ghiscoding/slickgrid-universal/releases/tag/v4.0.2) | 2023-12-15 | diff --git a/frameworks/angular-slickgrid/angular.json b/frameworks/angular-slickgrid/angular.json index 6580331637..4c919c6695 100644 --- a/frameworks/angular-slickgrid/angular.json +++ b/frameworks/angular-slickgrid/angular.json @@ -9,7 +9,7 @@ "projectType": "library", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": { "base": "website", @@ -17,7 +17,7 @@ }, "index": "src/index.html", "browser": "src/main.ts", - "polyfills": ["zone.js"], + "polyfills": [], "tsConfig": "tsconfig.app.json", "allowedCommonJsDependencies": ["@fnando/sparkline", "core-js", "html2canvas", "raf", "rgbcolor"], "assets": [ @@ -91,7 +91,7 @@ } }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "options": { "buildTarget": "angular-slickgrid:build" }, @@ -106,9 +106,6 @@ "options": { "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] } - }, - "test": { - "builder": "@analogjs/vitest-angular:test" } } } @@ -116,10 +113,30 @@ "schematics": { "@schematics/angular:component": { "prefix": "app", - "style": "scss" + "style": "scss", + "type": "component" }, "@schematics/angular:directive": { - "prefix": "app" + "prefix": "app", + "type": "directive" + }, + "@schematics/angular:service": { + "type": "service" + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." } }, "cli": { diff --git a/frameworks/angular-slickgrid/docs/TOC.md b/frameworks/angular-slickgrid/docs/TOC.md index d4dac58b0a..b89496d92c 100644 --- a/frameworks/angular-slickgrid/docs/TOC.md +++ b/frameworks/angular-slickgrid/docs/TOC.md @@ -48,18 +48,19 @@ * [Auto-Resize / Resizer Service](grid-functionalities/grid-auto-resize.md) * [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 Tooltip](grid-functionalities/Custom-Tooltip-\(plugin\).md) +* [Composite Editor Modal](grid-functionalities/composite-editor-modal.md) +* [Custom Tooltip](grid-functionalities/custom-tooltip.md) * [Add, Update or Highlight a Datagrid Item](grid-functionalities/add-update-highlight.md) * [Dynamically Add CSS Classes to Item Rows](grid-functionalities/dynamic-item-metadata.md) * [Column & Row Spanning](grid-functionalities/column-row-spanning.md) -* [Context Menu](grid-functionalities/Context-Menu.md) -* [Custom Footer](grid-functionalities/Custom-Footer.md) +* [Context Menu](grid-functionalities/context-menu.md) +* [Custom Footer](grid-functionalities/custom-footer.md) * [Excel Copy Buffer Plugin](grid-functionalities/excel-copy-buffer.md) * [Export to Excel](grid-functionalities/Export-to-Excel.md) * [Export to PDF](grid-functionalities/Export-to-PDF.md) * [Export to File (csv/txt)](grid-functionalities/Export-to-Text-File.md) -* [Grid Menu](grid-functionalities/Grid-Menu.md) +* [Load Global Grid Options](grid-functionalities/global-options.md) +* [Grid Menu](grid-functionalities/grid-menu.md) * [Grid State & Presets](grid-functionalities/grid-state-preset.md) * [Grouping & Aggregators](grid-functionalities/grouping-and-aggregators.md) * [Header & Footer Slots](grid-functionalities/header-footer-slots.md) @@ -79,9 +80,9 @@ ## Localization -* [with Custom Locales](localization/Localization-with-Custom-Locales.md) -* [with ngx-translate](localization/Localization-with-ngx-translate.md) - * [Component Sample](localization/Localization---Component-Sample.md) +* [with Custom Locales](localization/localization-with-custom-locales.md) +* [with ngx-translate](localization/localization-with-ngx-translate.md) + * [Component Sample](localization/localization-component-sample.md) ## Backend Services @@ -107,3 +108,4 @@ * [Migration Guide to 7.x (2023-12-15)](migrations/migration-to-7.x.md) * [Migration Guide to 8.x (2024-05-23)](migrations/migration-to-8.x.md) * [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/frameworks/angular-slickgrid/docs/backend-services/OData.md b/frameworks/angular-slickgrid/docs/backend-services/OData.md index f187fd4f12..940b081936 100644 --- a/frameworks/angular-slickgrid/docs/backend-services/OData.md +++ b/frameworks/angular-slickgrid/docs/backend-services/OData.md @@ -249,13 +249,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/frameworks/angular-slickgrid/docs/backend-services/graphql/GraphQL-Filtering.md b/frameworks/angular-slickgrid/docs/backend-services/graphql/GraphQL-Filtering.md index 403ed6da3d..85b9f3824a 100644 --- a/frameworks/angular-slickgrid/docs/backend-services/graphql/GraphQL-Filtering.md +++ b/frameworks/angular-slickgrid/docs/backend-services/graphql/GraphQL-Filtering.md @@ -139,14 +139,14 @@ 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. ```ts 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/frameworks/angular-slickgrid/docs/column-functionalities/cell-menu.md b/frameworks/angular-slickgrid/docs/column-functionalities/cell-menu.md index ded4c4aada..cd8417ff8e 100644 --- a/frameworks/angular-slickgrid/docs/column-functionalities/cell-menu.md +++ b/frameworks/angular-slickgrid/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 && 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 } } ]; @@ -164,15 +169,17 @@ 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', + { + 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 && args.dataContext; - return !dataContext.completed; + { + 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' }, @@ -187,7 +194,8 @@ this.columnDefinitions = [ 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/frameworks/angular-slickgrid/docs/column-functionalities/editors.md b/frameworks/angular-slickgrid/docs/column-functionalities/editors.md index 311d27453d..24f2903504 100644 --- a/frameworks/angular-slickgrid/docs/column-functionalities/editors.md +++ b/frameworks/angular-slickgrid/docs/column-functionalities/editors.md @@ -244,7 +244,7 @@ this.columnDefinitions = [ ``` #### Default Sanitize-Html Options -If you find that the HTML that you passed is being sanitized and you wish to change it, then you can change the default `sanitizeHtmlOptions` property defined in the Global Grid Options, for more info on how to change these global options, see the [Wiki - Global Grid Options](../grid-functionalities/Global-Options.md). The current defaults are: +If you find that the HTML that you passed is being sanitized and you wish to change it, then you can change the default `sanitizeHtmlOptions` property defined in the Global Grid Options, for more info on how to change these global options, see the [Wiki - Global Grid Options](../grid-functionalities/global-options.md). The current defaults are: ```typescript sanitizeHtmlOptions: { @@ -282,7 +282,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' } @@ -501,10 +501,10 @@ So if we take all of these informations and we want to create our own Custom Edi ```ts 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 && args.grid; + const grid = args.grid; const columnDef = args.column; const dataContext = args.item; - 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/frameworks/angular-slickgrid/docs/column-functionalities/editors/date-editor-flatpickr.md b/frameworks/angular-slickgrid/docs/column-functionalities/editors/date-editor-flatpickr.md deleted file mode 100644 index e4b9a6a298..0000000000 --- a/frameworks/angular-slickgrid/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/angular-slickgrid-demos/#/editor) | [Demo Component](https://github.com/ghiscoding/slickgrid-universal/blob/master/frameworks/angular-slickgrid/src/demos/examples/grid-editor.component.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 -defineGrid() { - 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: { minDate: '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 3](https://ghiscoding.github.io/angular-slickgrid-demos/#/editor)) -```ts -defineGrid() { - 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/frameworks/angular-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md b/frameworks/angular-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md index 9e791eaad1..911884149c 100644 --- a/frameworks/angular-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md +++ b/frameworks/angular-slickgrid/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 - Wiki](../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). @@ -37,7 +37,7 @@ this.columnDefinitions = [ ``` ### Editor Options (`MultipleSelectOption` interface) -All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library. +All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library. ```ts editor: { @@ -190,8 +190,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. @@ -217,7 +217,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/frameworks/angular-slickgrid/docs/column-functionalities/filters/autocomplete-filter.md b/frameworks/angular-slickgrid/docs/column-functionalities/filters/autocomplete-filter.md index b8fe318c42..350ff0de56 100644 --- a/frameworks/angular-slickgrid/docs/column-functionalities/filters/autocomplete-filter.md +++ b/frameworks/angular-slickgrid/docs/column-functionalities/filters/autocomplete-filter.md @@ -155,7 +155,6 @@ export class GridBasicComponent implements OnInit { } ``` ## Autocomplete - force user input -##### Requires version `2.6.0+` If you want to add the autocomplete functionality but want the user to be able to input a new option, then follow the example below: ```ts diff --git a/frameworks/angular-slickgrid/docs/column-functionalities/filters/compound-filters.md b/frameworks/angular-slickgrid/docs/column-functionalities/filters/compound-filters.md index 769ca25ed3..85c9e30890 100644 --- a/frameworks/angular-slickgrid/docs/column-functionalities/filters/compound-filters.md +++ b/frameworks/angular-slickgrid/docs/column-functionalities/filters/compound-filters.md @@ -103,7 +103,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. @@ -154,7 +154,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 = [ @@ -196,7 +196,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/frameworks/angular-slickgrid/docs/column-functionalities/filters/range-filters.md b/frameworks/angular-slickgrid/docs/column-functionalities/filters/range-filters.md index c1a4b72f6d..b180cda8b0 100644 --- a/frameworks/angular-slickgrid/docs/column-functionalities/filters/range-filters.md +++ b/frameworks/angular-slickgrid/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 @@ -141,7 +141,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/frameworks/angular-slickgrid/docs/column-functionalities/filters/select-filter.md b/frameworks/angular-slickgrid/docs/column-functionalities/filters/select-filter.md index ed5e839b60..f135a24310 100644 --- a/frameworks/angular-slickgrid/docs/column-functionalities/filters/select-filter.md +++ b/frameworks/angular-slickgrid/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. @@ -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: { @@ -576,7 +576,7 @@ this.columnDefinitions = [ ``` ### Filter Options (`MultipleSelectOption` interface) -All the available options that can be provided as filter `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your filter `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library. +All the available options that can be provided as filter `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your filter `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library. ```ts filter: { @@ -599,8 +599,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 filter `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 filter `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. @@ -629,7 +629,7 @@ this.columnDefinitions = [ 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/frameworks/angular-slickgrid/docs/column-functionalities/filters/single-search-filter.md b/frameworks/angular-slickgrid/docs/column-functionalities/filters/single-search-filter.md index 5a81495514..43857ca361 100644 --- a/frameworks/angular-slickgrid/docs/column-functionalities/filters/single-search-filter.md +++ b/frameworks/angular-slickgrid/docs/column-functionalities/filters/single-search-filter.md @@ -17,12 +17,16 @@ Some users might want to have 1 main single search for filtering the grid data i + @for (operator of operatorList; track operator) { + + } + @@ -39,7 +43,7 @@ export class MyComponent { columnDefinitions: Column[]; gridOptions: GridOption; dataset: any[]; - operatorList: OperatorString[] = ['=', '<', '<=', '>', '>=', '<>']; + operatorList: OperatorType[] = ['=', '<', '<=', '>', '>=', '<>']; selectedOperator = '='; searchValue = ''; selectedColumn: Column; @@ -54,7 +58,7 @@ export class MyComponent { 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/frameworks/angular-slickgrid/docs/column-functionalities/filters/slider-filter.md b/frameworks/angular-slickgrid/docs/column-functionalities/filters/slider-filter.md index 96b556a8a0..125c1a9040 100644 --- a/frameworks/angular-slickgrid/docs/column-functionalities/filters/slider-filter.md +++ b/frameworks/angular-slickgrid/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/frameworks/angular-slickgrid/docs/getting-started/quick-start.md b/frameworks/angular-slickgrid/docs/getting-started/quick-start.md index 47d4e52a57..ae294a7336 100644 --- a/frameworks/angular-slickgrid/docs/getting-started/quick-start.md +++ b/frameworks/angular-slickgrid/docs/getting-started/quick-start.md @@ -8,18 +8,20 @@ Install the `Angular-Slickgrid`, and other external packages like `Bootstrap` ```bash npm install --save angular-slickgrid bootstrap # the last dep is optional ``` + #### Important note about `ngx-translate` -#### Now optional -`ngx-translate` is now optional as of version `2.10.0`, see more info below at [step 5](#5-installsetup-ngx-translate-for-localization-optional) -**NOTE** however, please note that `@ngx-translate` is still going to be installed behind the scene just to make DI (dependency injection) build properly because of our use of `@Optional()` but it should be removed by the build tree shaking process once you run a production build. See their version compatibility table below - -| Angular Version | @ngx-translate/core | -|---------------------|---------------------| -| 16+ | 15.x | -| 13+ (**Ivy only**) | 14.x | -| 10-13 | 13.x | -| 8-9 | 12.x | -| 7 | 11.x | + +**NOTE** however, please note that `@ngx-translate` is still going to be installed behind the scene just to make DI (dependency injection) build properly because of our use of `@Optional()`. Since it's optional, it should be removed by the build tree shaking process once you run a production build. See their version compatibility table below: + +| Angular Version | @ngx-translate/core | +|-------------------------|---------------------| +| 21+ | 17.x | +| 16 - 19+ | 16.x | +| 16 - 17+ | 16.x (15.x) | +| 13 - 15 (**Ivy only**) | 14.x | +| 10-12 | 13.x | +| 8-9 | 12.x | +| 7 | 11.x | ### 2. Modify the `angular.json` and `tsconfig.app.json` files #### previous Angular versions were using `.angular-cli.json` @@ -66,7 +68,7 @@ You could also compile the SASS files with your own customization, for that simp ); ``` -### 4. Include it in your App Module (or App Config for Standalone) +### 4. for `Angular-Slickgrid` <= 9.0 - Include it in your App Module (or App Config for Standalone) Below are 2 different setups (with App Module (legacy) or Standalone) but in both cases the `AngularSlickgridModule.forRoot()` is **required**, so make sure to include it. Also note that the GitHub demo is strictly built with an App Module which is considered the legacy approach. #### App Module (legacy) @@ -133,12 +135,45 @@ The new updated version of `ng-packagr` use strict metadata and you might get er }) ``` -### 5. Install/Setup `ngx-translate` for Localization (optional) +### 5. for `Angular-Slickgrid` >= 10.x - Standalone Component + +```ts +import { AngularSlickgridComponent, GridOption } from 'angular-slickgrid'; + +// optional Grid Option +const gridOptionConfig: GridOption = { + enableAutoResize: true, + autoResize: { + container: '#demo-container', + rightPadding: 10, + }, + sanitizer: (dirtyHtml) => DOMPurify.sanitize(dirtyHtml, { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }), +}; + +bootstrapApplication(AppComponent, { + providers: [ + AngularSlickgridComponent, + { provide: 'defaultGridOption', useValue: gridOptionConfig }, + provideAppInitializer(() => { + const initializerFn = appInitializerFactory(inject(TranslateService), inject(Injector)); + return initializerFn(); + }), + provideTranslateService({ + fallbackLang: 'en', + loader: provideTranslateHttpLoader({ prefix: './assets/i18n/', suffix: '.json' }), + }), + provideHttpClient(withInterceptorsFromDi()), + provideZoneChangeDetection(), + ], +}).catch((err) => console.log(err)); +``` + +### 6. Install/Setup `ngx-translate` for Localization (optional) #### If you don't want to use any Translate Service and use only 1 Locale then take a look at this [demo](https://github.com/ghiscoding/angular-slickgrid-demos/tree/master/bootstrap4-demo-with-locales) To provide locales other than English (default locale), you have 2 options that you can go with. If you only use English, there is nothing to do (you can still change some of the texts in the grid via option 1.) -1. Using [Custom Locale](../localization/Localization-with-Custom-Locales.md), that is when you use **only 1** locale (other than English)... this is a new feature starting from version `2.10.0` and up. -2. Using [Localization with I18N](../localization/Localization-with-ngx-translate.md), that is when you want to use multiple locales dynamically. -3. **NOTE** you still need to install `@ngx-translate` (since it is a peer dependency) but it should be removed after doing a production build since it's optional. +1. Using [Custom Locale](../localization/localization-with-custom-locales.md), that is when you use **only 1** locale (other than English)... this is a new feature starting from version `2.10.0` and up. +2. Using [Localization with I18N](../localization/localization-with-ngx-translate.md), that is when you want to use multiple locales dynamically. +3. **NOTE** `@ngx-translate` will still be installed (since it's an internal dependency), but it should be removed after doing a production build because of our usage of `@Optional()`. ##### Translation Keys Also note that every time you want to use a translation key, you simply have to use a property with the `Key` suffix. For example if you wish to have a column definition `name` with a translation, just use the `nameKey: 'TRANSLATE_KEY'` instead of `name`. Below is a list of keys that can be used in the lib @@ -151,7 +186,7 @@ Also note that every time you want to use a translation key, you simply have to | columnGroup | columnGroupKey | | optionTitle | optionTitleKey | -### 6. Create a basic grid +### 7. Create a basic grid And finally, you are now ready to use it in your project, for example let's create both html/ts files for a `grid-basic.component` example, configure the Column Definitions, Grid Options and pass a Dataset to the grid: ```ts import { Column, GridOption } from 'angular-slickgrid'; @@ -199,7 +234,7 @@ define Angular-Slickgrid in your View ``` -### 7. Explore the Documentation +### 8. Explore the Documentation The last step is really to explore all the pages that are available in the documentation, everything you need to use the library should be available in here and so you should visit it often. For example a good starter is to look at the following - for all the `Grid Options`, take a look at all the [Grid Options](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/gridOption.interface.ts) interface. @@ -209,10 +244,10 @@ The last step is really to explore all the pages that are available in the docum - [Grid Menu](../grid-functionalities/Grid-Menu.md) ... and much more, just explore the Documentation through all the available pages. -### 8. How to load data with `HttpClient`? +### 9. How to load data with `HttpClient`? You might notice that all demos are made with mocked dataset that are embedded in each examples, that is mainly for demo purposes, but you might be wondering how to connect this with an `HttpClient`? Easy... just replace the mocked data, assigned to the `dataset` property, by your `HttpClient` call and that's it. The `dataset` property can be changed at any time, which is why you can use local data and/or connect it to a `Promise` or an `Observable` with `HttpClient` (internally it's just a SETTER that refreshes the grid). See [Example 24](https://ghiscoding.github.io/angular-slickgrid-demos/#/gridtabs) for a demo showing how to load a JSON file with `HttpClient`. -### 9. Live Demo - Clone the Examples +### 10. Live Demo - Clone the Examples The best way to get started is to clone the [Angular-Slickgrid-demos](https://github.com/ghiscoding/angular-slickgrid-demos), it has multiple examples and it is also updated frequently since it is used for the GitHub Bootstrap 5 demo page. `Angular-Slickgrid` has 3 `Bootstrap` repos, you can see a demo of each ones below. - [Bootstrap 5 demo](https://ghiscoding.github.io/angular-slickgrid-bs5-demo) / [examples repo](https://github.com/ghiscoding/angular-slickgrid-demos/tree/master/bootstrap5-demo-with-translate) (using `ngx-translate`) - [Bootstrap 5 - examples repo](https://github.com/ghiscoding/angular-slickgrid-demos/tree/master/bootstrap5-demo-with-locales) (single Locale, without using `ngx-translate`) @@ -220,11 +255,11 @@ The best way to get started is to clone the [Angular-Slickgrid-demos](https://gi ##### All Live Demo Examples have links to the actual code If you would like to see the code to a particular Example, just click on the "see code" which is available in all live examples. -### 10. CSP Compliance +### 11. CSP Compliance The project supports Content Security Policy (CSP) as long as you provide an optional `sanitizer` in your grid options (we recommend DOMPurify). Review the [CSP Compliance](../developer-guides/csp-compliance.md) documentation for more info. -### 11. Missing Features compared to SlickGrid? +### 12. Missing Features compared to SlickGrid? What if `Angular-Slickgrid` is missing feature(s) versus the original `SlickGrid`? Fear not and just use the `SlickGrid` and `DataView` objects directly, which are expose from the start through Custom Events. For more info continue reading on [Docs - SlickGrid & DataView objects](../slick-grid-dataview-objects/SlickGrid-&-DataView-Objects.md) -### 12. Troubleshooting - Build Errors/Warnings +### 13. Troubleshooting - Build Errors/Warnings Visit the [Troubleshooting](./troubleshooting.md) section for more common errors. \ No newline at end of file diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/Export-to-Text-File.md b/frameworks/angular-slickgrid/docs/grid-functionalities/Export-to-Text-File.md index e4fd7a3105..a6e6b642b1 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/Export-to-Text-File.md +++ b/frameworks/angular-slickgrid/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/frameworks/angular-slickgrid/docs/grid-functionalities/Header-Menu-&-Header-Buttons.md b/frameworks/angular-slickgrid/docs/grid-functionalities/Header-Menu-&-Header-Buttons.md index 7742b7653e..9129356b5f 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/Header-Menu-&-Header-Buttons.md +++ b/frameworks/angular-slickgrid/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,100 @@ 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 +155,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/frameworks/angular-slickgrid/docs/grid-functionalities/Row-based-edit.md b/frameworks/angular-slickgrid/docs/grid-functionalities/Row-based-edit.md index fd12263a79..05d91cc4b4 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/Row-based-edit.md +++ b/frameworks/angular-slickgrid/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/frameworks/angular-slickgrid/docs/grid-functionalities/Composite-Editor-Modal.md b/frameworks/angular-slickgrid/docs/grid-functionalities/composite-editor-modal.md similarity index 98% rename from frameworks/angular-slickgrid/docs/grid-functionalities/Composite-Editor-Modal.md rename to frameworks/angular-slickgrid/docs/grid-functionalities/composite-editor-modal.md index 6affee005b..ce3c9cf392 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/Composite-Editor-Modal.md +++ b/frameworks/angular-slickgrid/docs/grid-functionalities/composite-editor-modal.md @@ -296,7 +296,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({ @@ -476,7 +476,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'); } diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/Context-Menu.md b/frameworks/angular-slickgrid/docs/grid-functionalities/context-menu.md similarity index 77% rename from frameworks/angular-slickgrid/docs/grid-functionalities/Context-Menu.md rename to frameworks/angular-slickgrid/docs/grid-functionalities/context-menu.md index 4396e689ff..0a4c6cd168 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/Context-Menu.md +++ b/frameworks/angular-slickgrid/docs/grid-functionalities/context-menu.md @@ -23,7 +23,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 = { @@ -63,6 +63,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). @@ -79,7 +171,7 @@ this.gridOptions = { // 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; this.sgb.gridService.updateItem(dataContext); @@ -139,7 +231,7 @@ For example, say we want the Context Menu to only be available on the first 20 r ```ts contextMenu: { 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 }, }, @@ -153,7 +245,7 @@ contextMenu: { option: 0, title: 'n/a', 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; }, }, @@ -177,6 +269,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. + ### Show Menu only over Certain Columns Say you want to show the Context Menu only when the user is over certain columns of the grid. For that, you could use the `commandShownOverColumnIds` (or `optionShownOverColumnIds`) array, by default these arrays are empty and when that is the case then the menu will be accessible from any columns. So if we want to have the Context Menu available only over the first 2 columns, we would have an array of those 2 column ids. For example, the following would show the Context Menu everywhere except the last 2 columns (priority, action) since they are not part of the array. ```ts diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/Custom-Footer.md b/frameworks/angular-slickgrid/docs/grid-functionalities/custom-footer.md similarity index 100% rename from frameworks/angular-slickgrid/docs/grid-functionalities/Custom-Footer.md rename to frameworks/angular-slickgrid/docs/grid-functionalities/custom-footer.md diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/Custom-Tooltip-(plugin).md b/frameworks/angular-slickgrid/docs/grid-functionalities/custom-tooltip.md similarity index 95% rename from frameworks/angular-slickgrid/docs/grid-functionalities/Custom-Tooltip-(plugin).md rename to frameworks/angular-slickgrid/docs/grid-functionalities/custom-tooltip.md index 51e2ee8607..d0c8b87123 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/Custom-Tooltip-(plugin).md +++ b/frameworks/angular-slickgrid/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 + +``` + #### 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/frameworks/angular-slickgrid/docs/grid-functionalities/export-to-excel.md b/frameworks/angular-slickgrid/docs/grid-functionalities/export-to-excel.md index 0b64f92612..71ee73eb6c 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/export-to-excel.md +++ b/frameworks/angular-slickgrid/docs/grid-functionalities/export-to-excel.md @@ -120,9 +120,7 @@ What we can see from the example, is that it will use all Formatters (when exist ### Custom Column Width -**NOTE** now deprecated, please use [Custom Cell Styling](#custom-cell-styling) instead - -You can define a custom Excel column width (the width Excel's own width which is not in pixel). You can define a custom width per column (in your column definitions) and/or for the entire grid (in your grid options). +See [Custom Cell Styling](#custom-cell-styling) to define cell width. #### Per Column You could set a custom width per column diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/Global-Options.md b/frameworks/angular-slickgrid/docs/grid-functionalities/global-options.md similarity index 54% rename from frameworks/angular-slickgrid/docs/grid-functionalities/Global-Options.md rename to frameworks/angular-slickgrid/docs/grid-functionalities/global-options.md index 478b9a1874..22e1e1d293 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/Global-Options.md +++ b/frameworks/angular-slickgrid/docs/grid-functionalities/global-options.md @@ -1,5 +1,6 @@ You might find yourself re-using the same configurations over and over, in that case we got you covered. You can change any of the global options directly in your App Module through `forRoot` which accept an optional object of Grid Options. +### 1. for `Angular-Slickgrid` <= 9.0 - Include it in your App Module (or App Config for Standalone) ```typescript import { AngularSlickgridModule } from 'angular-slickgrid'; @@ -15,8 +16,9 @@ import { AngularSlickgridModule } from 'angular-slickgrid'; enableFiltering: true, enableCellNavigation: true, enablePagination: true, - enableRowSelection: true, + enableSelection: true, enableTranslate: true, + sanitizer: (dirtyHtml) => DOMPurify.sanitize(dirtyHtml, { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }), //... }), ], @@ -25,5 +27,30 @@ import { AngularSlickgridModule } from 'angular-slickgrid'; export class AppModule { } ``` + +### 2. for `Angular-Slickgrid` >= 10.x - Standalone Component + +```typescript +import { AngularSlickgridComponent, GridOption } from 'angular-slickgrid'; + +// optional Grid Option +const gridOptionConfig: GridOption = { + enableAutoResize: true, + autoResize: { + container: '#demo-container', + rightPadding: 10, + }, + sanitizer: (dirtyHtml) => DOMPurify.sanitize(dirtyHtml, { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }), +}; + +bootstrapApplication(AppComponent, { + providers: [ + AngularSlickgridComponent, + { provide: 'defaultGridOption', useValue: gridOptionConfig }, + // .... + ], +}).catch((err) => console.log(err)); +``` + ### List of Global Options -For the complete list of available Grid Option, you can take a look at the [Default Grid Options](https://github.com/ghiscoding/slickgrid-universal/blob/master/frameworks/angular-slickgrid/src/app/angular-slickgrid/global-grid-options.ts) file and/or technically any of the options from the [grid options - interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/frameworks/angular-slickgrid/src/app/angular-slickgrid/models/gridOption.interface.ts) are configurable. \ No newline at end of file +For the complete list of available Grid Option, you can take a look at the [Default Grid Options](https://github.com/ghiscoding/slickgrid-universal/blob/master/frameworks/angular-slickgrid/src/app/angular-slickgrid/global-grid-options.ts) file and/or technically any of the options from the [grid options - interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/frameworks/angular-slickgrid/src/library/models/gridOption.interface.ts) are configurable. \ No newline at end of file diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/Grid-Menu.md b/frameworks/angular-slickgrid/docs/grid-functionalities/grid-menu.md similarity index 82% rename from frameworks/angular-slickgrid/docs/grid-functionalities/Grid-Menu.md rename to frameworks/angular-slickgrid/docs/grid-functionalities/grid-menu.md index 96afd4a8ae..75ee618cde 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/Grid-Menu.md +++ b/frameworks/angular-slickgrid/docs/grid-functionalities/grid-menu.md @@ -74,6 +74,11 @@ 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. + +See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples. + #### Events There are multiple events/callback hooks which are accessible from the Grid Options @@ -107,6 +112,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/6pac/SlickGrid/blob/master/controls/slick.gridmenu.js) 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[Command]`, for example, see below for the defaults. @@ -124,6 +134,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. diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/grid-state-preset.md b/frameworks/angular-slickgrid/docs/grid-functionalities/grid-state-preset.md index 46cf1cd585..4f9301b3c6 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/grid-state-preset.md +++ b/frameworks/angular-slickgrid/docs/grid-functionalities/grid-state-preset.md @@ -58,6 +58,8 @@ export class GridDemoComponent { } ``` +> **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 @@ -76,12 +78,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; @@ -209,6 +211,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 @@ -219,7 +225,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 @@ -228,6 +234,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. ```ts @@ -245,4 +256,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. +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. diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/row-detail.md b/frameworks/angular-slickgrid/docs/grid-functionalities/row-detail.md index 29f6f9727e..17aae34959 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/row-detail.md +++ b/frameworks/angular-slickgrid/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/angular-slickgrid-demos/#/rowdetail) / [Demo Component](https://github.com/ghiscoding/slickgrid-universal/blob/master/frameworks/angular-slickgrid/src/demos/examples/grid-rowdetail.component.ts) @@ -25,6 +25,8 @@ A Row Detail allows you to open a detail panel which can contain extra and/or mo ## Usage +> Starting from version 10, Row Detail is now an optional package and must be installed separately (`@slickgrid-universal/angular-row-detail-plugin`) + ##### View ```html =10.x - rowSelectionOptions: { + selectionOptions: { selectActiveRow: true }, + externalResources: [AngularSlickRowDetailView], // for v10 and above rowDetailView: { // We can load the "process" asynchronously in 2 different ways (httpClient OR even Promise) process: (item) => this.http.get(`api/item/${item.id}`), @@ -108,7 +113,7 @@ Row Detail is an addon (commonly known as a plugin and are opt-in addon), becaus ```ts changeDetailViewRowCount() { if (this.angularGrid && this.angularGrid.extensionService) { - const rowDetailInstance = this.angularGrid.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView); + const rowDetailInstance = this.angularGrid.extensionService.getExtensionInstanceByName('rowDetailView'); const options = rowDetailInstance.getOptions(); options.panelRows = this.detailViewRowCount; // change number of rows dynamically rowDetailInstance.setOptions(options); @@ -124,7 +129,7 @@ Same as previous paragraph, after we get the SlickGrid addon instance, we can ca ```ts closeAllRowDetail() { if (this.angularGrid && this.angularGrid.extensionService) { - const rowDetailInstance = this.angularGrid.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView); + const rowDetailInstance = this.angularGrid.extensionService.getExtensionInstanceByName('rowDetailView'); rowDetailInstance.collapseAll(); } } @@ -134,7 +139,7 @@ This requires a bit more work, you can call the method `collapseDetailView(item) ```ts closeRowDetail(gridRowIndex: number) { if (this.angularGrid && this.angularGrid.extensionService) { - const rowDetailInstance = this.angularGrid.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView); + const rowDetailInstance = this.angularGrid.extensionService.getExtensionInstanceByName('rowDetailView'); const item = this.angularGrid.gridService.getDataItemByRowIndex(gridRowIndex); rowDetailInstance.collapseDetailView(item); } @@ -178,7 +183,7 @@ Same concept as the preload, we pass an Angular Component to the `viewComponent` // ... row detail options // View Component to load when row detail data is ready - // also make sure that it's part of your App Module `entryComponents` array + // also make sure that it's part of your App Module `entryComponents` array (for `angular-slickgrid` < 10) viewComponent: RowDetailViewComponent, } }; @@ -241,7 +246,7 @@ export class RowDetailViewComponent { } } ``` -###### App Module +###### App Module (for `angular-slickgrid` < 10) Also make sure that it's part of your App Module `entryComponents` array since this will be a dynamically created component. ```ts @@ -358,7 +363,7 @@ export class RowDetailViewComponent { ``` ## 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 Angular-Slickgrid 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 Angular-Slickgrid 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. ```html @@ -396,6 +401,7 @@ Main Grid Component ```ts import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { AngularSlickRowDetailView } from '@slickgrid-universal/angular-row-detail-plugin'; // for v10 and above import { AngularGridInstance, Column, GridOption, GridState } from 'angular-slickgrid'; @Component({ @@ -427,15 +433,10 @@ export class MainGridComponent implements OnInit { 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 - const rowDetail = new SlickRowDetailView(pubSubService as EventPubSubService); - return [{ name: ExtensionName.rowDetailView, instance: rowDetail }]; - }, + externalResources: [AngularSlickRowDetailView], // for v10 and above rowDetailView: { process: (item: any) => simulateServerAsyncCall(item), loadOnce: false, // IMPORTANT, you can't use loadOnce with inner grid because only HTML template are re-rendered, not JS events diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/row-selection.md b/frameworks/angular-slickgrid/docs/grid-functionalities/row-selection.md index 5575781ba0..60758ab79a 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/row-selection.md +++ b/frameworks/angular-slickgrid/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 @@ -19,7 +19,7 @@ For row selection, you can simply play with couple of grid options (see below) a [Demo Page](https://ghiscoding.github.io/angular-slickgrid-demos/#/selection) / [Demo Component](https://github.com/ghiscoding/slickgrid-universal/blob/master/frameworks/angular-slickgrid/src/demos/examples/grid-rowselection.component.ts) ## Single Row Selection -For a single row selection, you need to have `enableCellNavigation: true`, `enableRowSelection: true` and `multiSelect: false` and as described earlier, subscribe to `onSelectedRowsChanged` (for that you need to bind to `(gridChanged)`). +For a single row selection, you need to have `enableCellNavigation: true`, `enableSelection: true` (or `enableRowSelection` in <=9.x) and `multiSelect: false` and as described earlier, subscribe to `onSelectedRowsChanged` (for that you need to bind to `(gridChanged)`). **Note:** if you want to change from Multiple Selections to Single Selection (and vice-versa), you could use the grid options `enableCellNavigation` flag, however this is not possible when using Inline Editors since this flag is required. However, there is no other known ways of toggling dynamically. @@ -39,7 +39,7 @@ this.gridOptions = { enableAutoResize: true, enableCellNavigation: true, enableCheckboxSelector: true, - enableRowSelection: true, + enableSelection: true, // or `enableRowSelection` in <=9.x multiSelect: false, } @@ -54,7 +54,7 @@ onSelectedRowsChanged(e, args) { ``` ## Multiple Row Selections -As for multiple row selections, you need to provide an extra grid option of `rowSelectionOptions` which is an object and within it, you need to disable the `selectActiveRow` flag. The other configurations are the same as a Single Selection, which is to enable `enableCheckboxSelector` and `enableRowSelection`. Then as describe earlier, you will subscribe to `onSelectedRowsChanged` (for that you need to bind to `(gridChanged)`). +As for multiple row selections, you need to provide an extra grid option of `rowSelectionOptions` which is an object and within it, you need to disable the `selectActiveRow` flag. The other configurations are the same as a Single Selection, which is to enable `enableCheckboxSelector` and `enableSelection` (or `enableRowSelection` in <=9.x). Then as describe earlier, you will subscribe to `onSelectedRowsChanged` (for that you need to bind to `(gridChanged)`). #### View ```html @@ -74,9 +74,12 @@ export class Example1 implements OnInit { enableAutoResize: true, enableCellNavigation: true, enableCheckboxSelector: true, - enableRowSelection: true, + + // `enableRowSelection` in <=9.x OR `enableSelection` in >=10.0 + enableSelection: true, + // `rowSelectionOptions` in <=9.x OR `selectionOptions` in >=10.x - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false }, @@ -133,7 +136,7 @@ You can use `selectableOverride` to provide custom logic to disable certain rows export class Example1 implements OnInit { prepareGrid() { this.gridOptions = { - enableRowSelection: true, + enableSelection: true, // or `enableRowSelection` in <=9.x enableCheckboxSelector: true, checkboxSelector: { // you can override the logic for showing (or not) the expand icon @@ -142,7 +145,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, }, @@ -225,16 +228,16 @@ export class Example1 { Starting with v9.10.0, you can now use the new Hybrid Selection Model, this new model will allow you to do Cell Selection & Row Selection in the same grid. This wasn't previously doable before that version because SlickGrid only ever allows 1 selection model to be loaded at once and so we had to load either `SlickCellSelectionModel` or `SlickRowSelectionModel` but never both of them at the same time. The new Hybrid Selection Model is merging both of these plugins in a single plugin allowing us to do both type of selections. > [!NOTE] -> You can use `enableHybridSelection: true` grid option to enable the new Hybrid Model, this new model will eventually replace both cell/row selection model in the future since there's no need to keep all these models when only 1 is more than enough +> You can use `{ enableSelection: true, selectionOptions: { selectionType: 'mixed' }}` grid option to enable the new Hybrid Model, this new model will eventually replace both cell/row selection model in the future since there's no need to keep all these models when only 1 is more than enough For example, we could use the Excel Copy Buffer (Cell Selection) and use `rowSelectColumnIds` (Row Selection) ```ts this.gridOptions = { // enable new hybrid selection model (rows & cells) - enableHybridSelection: true, + enableSelection: true, // `rowSelectionOptions` in <=9.x OR `selectionOptions` in >=10.x - rowSelectionOptions: { + selectionOptions: { selectActiveRow: true, rowSelectColumnIds: ['selector'], }, @@ -270,7 +273,7 @@ You can also `onDragReplaceCells` event to drag and fill cell values to the exte ```ts this.gridOptions = { // enable new hybrid selection model (rows & cells) - enableHybridSelection: true, + enableSelection: true, // ... }; @@ -293,7 +296,7 @@ copyDraggedCellRange(args: OnDragReplaceCellsEventArgs) { ``` ## 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 Angular-Slickgrid 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 Angular-Slickgrid 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. ```html diff --git a/frameworks/angular-slickgrid/docs/localization/Localization---Component-Sample.md b/frameworks/angular-slickgrid/docs/localization/localization-component-sample.md similarity index 98% rename from frameworks/angular-slickgrid/docs/localization/Localization---Component-Sample.md rename to frameworks/angular-slickgrid/docs/localization/localization-component-sample.md index 0da856515a..5f15684043 100644 --- a/frameworks/angular-slickgrid/docs/localization/Localization---Component-Sample.md +++ b/frameworks/angular-slickgrid/docs/localization/localization-component-sample.md @@ -2,7 +2,7 @@ You need to add a translation key via the property `nameKey` to each column definition, for example: `nameKey: 'TITLE'` ##### Translation Files -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](https://github.com/ghiscoding/slickgrid-universal/tree/master/frameworks/angular-slickgrid/src/assets/i18n) translation folder (from that file, you need all translations shown before the translation 'BILLING', the next few ones are for the demo page only). If you need more information on how to import translations, please review the other [docs](../localization/Localization-with-ngx-translate.md#locales) page. +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](https://github.com/ghiscoding/slickgrid-universal/tree/master/frameworks/angular-slickgrid/src/assets/i18n) translation folder (from that file, you need all translations shown before the translation 'BILLING', the next few ones are for the demo page only). If you need more information on how to import translations, please review the other [docs](../localization/localization-with-ngx-translate.md#locales) page. ##### Note For the `Select` Filter, you will use `labelKey` instead of `label`. Anytime a translation key will come in play, we will add the word `key` to the end (hence `nameKey`, `labelKey`, more to come...) diff --git a/frameworks/angular-slickgrid/docs/localization/Localization-with-Custom-Locales.md b/frameworks/angular-slickgrid/docs/localization/localization-with-custom-locales.md similarity index 70% rename from frameworks/angular-slickgrid/docs/localization/Localization-with-Custom-Locales.md rename to frameworks/angular-slickgrid/docs/localization/localization-with-custom-locales.md index d21e05c785..e36934b33e 100644 --- a/frameworks/angular-slickgrid/docs/localization/Localization-with-Custom-Locales.md +++ b/frameworks/angular-slickgrid/docs/localization/localization-with-custom-locales.md @@ -26,7 +26,7 @@ export const localeFrench = { // ... the rest of the text ``` -#### 2. Use the Custom Locales +#### 2. for `Angular-Slickgrid` <= 9.0 - Use the Custom Locales ##### Through the `forRoot()` (globally) This will literally configure Custom Locales for the entire project, so if you want to do it once, that is the place to do it. ```ts @@ -42,6 +42,54 @@ This will literally configure Custom Locales for the entire project, so if you w ] }); ``` + +##### Through the Grid Option of any grid +You can alternatively provide Custom Locales through any grid declaration through the `locales` Grid Options (it's the same as the global one, except that it's per grid) + +```ts +import { localeFrench } from 'locales/fr'; + +export class MyGridComponent { + prepareGrid() { + this.columnDefinitions = [ /* ... */ ]; + + this.gridOptions = { + enableAutoResize: true, + + // provide Custom Locale to this grid only + locales: localeFrench + }; + } +} +``` + +### 3. for `Angular-Slickgrid` >= 10.x - Use the Custom Locales + +##### in your `main.ts` App boostrap + +This will literally configure Custom Locales for the entire project, so if you want to do it once, that is the place to do it. + +```ts +import { AngularSlickgridComponent, GridOption } from 'angular-slickgrid'; + +// optional Grid Option +const gridOptionConfig: GridOption = { + // add any Global Grid Options/Config you might want + // ... + + // Provide a custom locales set + locales: localeFrench, +}; + +bootstrapApplication(AppComponent, { + providers: [ + AngularSlickgridComponent, + { provide: 'defaultGridOption', useValue: gridOptionConfig }, + // ... + ], +}).catch((err) => console.log(err)); +``` + ##### Through the Grid Option of any grid You can alternatively provide Custom Locales through any grid declaration through the `locales` Grid Options (it's the same as the global one, except that it's per grid) @@ -62,5 +110,5 @@ export class MyGridComponent { } ``` -#### 3. Use the lib (without ngx-translate) +#### 4. Use the lib (without ngx-translate) There's nothing else to do, just use the library without defining or providing TranslateService and you're good to go. Read through the Wiki of the [Quick Start](../getting-started/quick-start.md) for basic grid samples. \ No newline at end of file diff --git a/frameworks/angular-slickgrid/docs/localization/Localization-with-ngx-translate.md b/frameworks/angular-slickgrid/docs/localization/localization-with-ngx-translate.md similarity index 56% rename from frameworks/angular-slickgrid/docs/localization/Localization-with-ngx-translate.md rename to frameworks/angular-slickgrid/docs/localization/localization-with-ngx-translate.md index a7ed2def35..92b900efd2 100644 --- a/frameworks/angular-slickgrid/docs/localization/Localization-with-ngx-translate.md +++ b/frameworks/angular-slickgrid/docs/localization/localization-with-ngx-translate.md @@ -7,28 +7,34 @@ We use `ngx-translate` because the `i18n` from Angular core is yet to support dy #### ngx-translate setup [Demo Component](https://github.com/ghiscoding/slickgrid-universal/tree/master/frameworks/angular-slickgrid/src/library) -### Angular Versions -##### Angular 7 -Angular 7 should be fine with current dependencies in the lib, they are set at `@ngx-translate/core` version `11.x` and `@ngx-translate/http-loader` at version `4.x` +### Angular Versions and `ngx-translate` + +In order to use `ngx-translate`, you will have to use `@ngx-translate/core` and possibly `@ngx-translate/http-loader`. Check the table below to find the correct version to use and get more info from their website: https://ngx-translate.org/getting-started/installation/ -##### Angular 4-5-6 -Use `@ngx-translate/core` version `9.x` for Angular 4-5 and version `10.x` for Angular 6. While `@ngx-translate/http-loader` has version `2.x` for Angular 4-5 and version `3.x` for Angular 6. -``` -npm install @ngx-translate/core@9.1.1 # change to the version that works for you -npm install @ngx-translate/http-loader@2.0.0 # change to the version that works for you -``` -### Minimal installation (~even if you are not using any other locales~) -~#### If you are only using 1 locale, you still need to import/configure `ngx-translate` to use `Angular-Slickgrid`~ -~Since `ngx-translate` is now a dependency of `Angular-Slickgrid`, you will need to add `ngx-translate` to your bundle and import/configure it in your App Module. The minimum setup is the following:~ +| Angular Version | @ngx-translate/core | +|-------------------------|---------------------| +| 21+ | 17.x | +| 16 - 19+ | 16.x | +| 16 - 17+ | 16.x (15.x) | +| 13 - 15 (**Ivy only**) | 14.x | +| 10-12 | 13.x | +| 8-9 | 12.x | +| 7 | 11.x | -Actually, this is no longer true, if you use only 1 locale, you can now disregard `ngx-translate` completely, head over to the new [Wiki - Providing Custom Locale](Localization-with-Custom-Locales.md). But if you still wish to install the minimum installation to get `ngx-translate` then continue the reading. +### 1. for `Angular-Slickgrid` <= 9.0 + +#### Minimal installation (~even if you are not using any other locales~) +If you use only 1 locale, you can now disregard `ngx-translate` installation completely, head over to the new [Wiki - Providing Custom Locale](localization-with-custom-locales.md) for more details. But if you still wish to install the minimum installation to get `ngx-translate` then continue reading. ##### Install NPM package + ```typescript npm install @ngx-translate/core ``` + ##### App Module + ```typescript import { NgModule } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; @@ -51,11 +57,9 @@ export class AppModule { } ### Regular Installation #### NPM You need to make sure that you have `@ngx-translate/core` installed. Then optionally, you will need a loader, the most recommended one is `@ngx-translate/http-loader`. For more installation and usage information, you can visit the official [ngx-translate site](https://github.com/ngx-translate/core#installation) + ```bash npm install @ngx-translate/core @ngx-translate/http-loader - -## OR with yarn -yarn add @ngx-translate/core @ngx-translate/http-loader ``` #### App initializer (optional) @@ -70,6 +74,7 @@ import { LOCATION_INITIALIZED } from '@angular/common'; import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +// for @ngx-translate <=16.0 // AoT requires an exported function for factories export function createTranslateLoader(http: HttpClient) { return new TranslateHttpLoader(http, './assets/i18n/', '.json'); @@ -81,7 +86,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj const locationInitialized = injector.get(LOCATION_INITIALIZED, Promise.resolve(null)); locationInitialized.then(() => { const langToSet = 'en'; - translate.setDefaultLang('en'); + translate.setFallbackLang('en'); // OR `translate.setDefaultLang('en')` for ngx-translate<=16 translate.use(langToSet).subscribe(() => { // console.info(`Successfully initialized '${langToSet}' language.'`); }, err => { @@ -100,6 +105,8 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj imports: [ BrowserModule, HttpClientModule, + + // for @ngx-translate <=16.0 (ONLY) TranslateModule.forRoot({ loader: { provide: TranslateLoader, @@ -115,6 +122,11 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj deps: [TranslateService, Injector], multi: true }, + // for @ngx-translate >=17.0 + provideTranslateService({ + fallbackLang: 'en', + loader: provideTranslateHttpLoader({ prefix: './assets/i18n/', suffix: '.json' }), + }), GridOdataService, ResizerService ], @@ -123,8 +135,10 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj export class AppModule { } ``` -#### Angular 7 +#### Angular 7+ + The new updated version of `ng-packagr` use strict metadata and you might get errors about `Lambda not supported`, to bypass this problem you can add the `@dynamic` over the `@NgModule` as so: + ```ts // @dynamic @NgModule({ @@ -132,17 +146,93 @@ The new updated version of `ng-packagr` use strict metadata and you might get er }) ``` -#### Locales +### 2. for `Angular-Slickgrid` >= 10.0 + +#### App Initializer +The App initializer is useful to fetch all translactions locales asynchronously before any of the component loads. This step is important if you need to fetch translations from JSON assets in an asynchronous step before any other component loads. + +You can move the App Initializer to a separate file or simply add it to your `main.ts` + +```ts +// app-initializer.ts +import { LOCATION_INITIALIZED } from '@angular/common'; +import { type Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +// use an Initializer Factory as describe here: https://github.com/ngx-translate/core/issues/517#issuecomment-299637956 +export function appInitializerFactory(translate: TranslateService, injector: Injector) { + return () => + new Promise((resolve: any) => { + const locationInitialized = injector.get(LOCATION_INITIALIZED, Promise.resolve(null)); + locationInitialized.then(() => { + const langToSet = 'en'; + translate.setFallbackLang('en'); + translate.use(langToSet).subscribe({ + next: () => { + // console.info(`Successfully initialized '${langToSet}' language.'`); + }, + error: () => console.error(`Problem with '${langToSet}' language initialization.'`), + complete: () => resolve(null), + }); + }); + }); +} +``` + +then use it in your App `main.ts` + +```ts +// main.ts +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { enableProdMode, importProvidersFrom, inject, Injector, provideAppInitializer, provideZoneChangeDetection } from '@angular/core'; +import { provideTranslateService, TranslateService } from '@ngx-translate/core'; +import { provideTranslateHttpLoader } from '@ngx-translate/http-loader'; +import { AppComponent } from './demos/app.component'; +import { appInitializerFactory } from './demos/app.initializer'; +import { AngularSlickgridComponent, GridOption } from 'angular-slickgrid'; + +// define Angular-Slickgrid default grid options that are common to all grids +const gridOptionConfig: GridOption = { + enableAutoResize: true, + // ... +}; + +bootstrapApplication(AppComponent, { + providers: [ + AngularSlickgridComponent, + { provide: 'defaultGridOption', useValue: gridOptionConfig }, + + // load your App Initializer to pre-fetch your translations + provideAppInitializer(() => { + const initializerFn = appInitializerFactory(inject(TranslateService), inject(Injector)); + return initializerFn(); + }), + + // configure ngx-translate + provideTranslateService({ + fallbackLang: 'en', + loader: provideTranslateHttpLoader({ prefix: './assets/i18n/', suffix: '.json' }), + }), + provideHttpClient(withInterceptorsFromDi()), + ], +}).catch((err) => console.log(err)); +``` + +### 3. Locales + The final step is that you need the actual translations. Note that `ngx-translate` does not support multiple files, with that in mind see below for the following options that you have. + 1. Manually copy the translation keys/values 2. Manually copy the JSON files to your `src/assets` folder -2. Modify `angular-cli.json` to copy the JSON files to your `src/assets` folder. +3. Modify `angular-cli.json` to copy the JSON files to your `src/assets` folder. - I tried following these [instructions](https://github.com/angular/angular-cli/issues/3555#issuecomment-351772402) but that didn't work -3. Modify your `package.json` and add a script to copy the JSON files to your `src/assets` folder - - install NPM packages `cross-env` and `copyfiles` +4. Modify your `package.json` and add a script to copy the JSON files to your `src/assets` folder + - install NPM packages `native-copyfiles` (`npm install native-copyfiles`) - add a new script in your `package.json` - run the below script **once** with `npm run copy:i18n` and you should now have the JSON files in your `src/assets` folder + ```typescript -"copy:i18n": "cross-env copyfiles -f node_modules/angular-slickgrid/i18n/*.json src/assets/i18n" +"copy:i18n": "copyfiles -f node_modules/angular-slickgrid/i18n/*.json src/assets/i18n" ``` + 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](https://github.com/ghiscoding/slickgrid-universal/tree/master/frameworks/angular-slickgrid/src/assets/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/frameworks/angular-slickgrid/docs/menu-slots.md b/frameworks/angular-slickgrid/docs/menu-slots.md new file mode 100644 index 0000000000..14b9f9bb47 --- /dev/null +++ b/frameworks/angular-slickgrid/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 ` +
      + ${cmdItem.iconCssClass ? `` : ''} + ${cmdItem.title} +
      + `; + }, + commandItems: [ + { + command: 'action-1', + title: 'Action One', + iconCssClass: 'mdi mdi-check', + // This item uses defaultMenuItemRenderer + }, + { + command: 'custom', + title: 'Custom Item', + // This item overrides defaultMenuItemRenderer with its own slotRenderer + slotRenderer: () => ` +
      + Custom rendering overrides default +
      + ` + } + ] +}; +``` + +### Menu Types & Configuration + +The `slotRenderer` and `defaultMenuItemRenderer` work identically across all menu plugins: + +#### Header Menu +```typescript +const columnDef = { + id: 'name', + header: { + menu: { + defaultMenuItemRenderer: (cmdItem, args) => `
      ${cmdItem.title}
      `, + commandItems: [ + { + command: 'sort', + title: 'Sort', + slotRenderer: () => '
      Custom sort
      ' + } + ] + } + } +}; +``` + +#### Cell Menu +```typescript +const columnDef = { + id: 'action', + cellMenu: { + defaultMenuItemRenderer: (cmdItem, args) => `
      ${cmdItem.title}
      `, + commandItems: [ + { + command: 'edit', + title: 'Edit', + slotRenderer: (cmdItem, args) => `
      Edit row ${args.dataContext.id}
      ` + } + ] + } +}; +``` + +#### Context Menu +```typescript +const gridOptions = { + enableContextMenu: true, + contextMenu: { + defaultMenuItemRenderer: (cmdItem, args) => `
      ${cmdItem.title}
      `, + commandItems: [ + { + command: 'export', + title: 'Export', + slotRenderer: () => '
      📊 Export data
      ' + } + ] + } +}; +``` + +#### Grid Menu +```typescript +const gridOptions = { + enableGridMenu: true, + gridMenu: { + defaultMenuItemRenderer: (cmdItem, args) => `
      ${cmdItem.title}
      `, + commandItems: [ + { + command: 'refresh', + title: 'Refresh', + slotRenderer: () => '
      🔄 Refresh data
      ' + } + ] + } +}; +``` + +### Framework Integration Examples + +#### Vanilla JavaScript +```typescript +const menuItem = { + command: 'custom', + title: 'Action', + slotRenderer: () => ` + + ` +}; +``` + +#### Angular - Dynamic Components +```typescript +// In component class +const menuItem = { + command: 'with-component', + title: 'With Angular Component', + slotRenderer: (cmdItem, args) => { + // Create a placeholder element + const placeholder = document.createElement('div'); + placeholder.id = `angular-slot-${Date.now()}`; + + // Schedule component creation for after rendering + setTimeout(() => { + const element = document.getElementById(placeholder.id); + if (element) { + const componentRef = this.viewContainerRef.createComponent(MyComponent); + element.appendChild(componentRef.location.nativeElement); + } + }, 0); + + return placeholder; + } +}; +``` + +#### React - Using Hooks +```typescript +// Define menu item with slotRenderer +const menuItem = { + command: 'with-react', + title: 'With React Component', + slotRenderer: (cmdItem, args) => { + const container = document.createElement('div'); + container.id = `react-slot-${Date.now()}`; + + // Schedule component render for after menu renders + setTimeout(() => { + const element = document.getElementById(container.id); + if (element) { + ReactDOM.render(, element); + } + }, 0); + + return container; + } +}; +``` + +#### Vue - Using createApp +```typescript +// Define menu item with slotRenderer +const menuItem = { + command: 'with-vue', + title: 'With Vue Component', + slotRenderer: (cmdItem, args) => { + const container = document.createElement('div'); + container.id = `vue-slot-${Date.now()}`; + + // Schedule component mount for after menu renders + setTimeout(() => { + const element = document.getElementById(container.id); + if (element && !element._appInstance) { + const app = createApp(MyComponent, { data: args }); + app.mount(element); + element._appInstance = app; + } + }, 0); + + return container; + } +}; +``` + +### Real-World Use Cases + +#### 1. Add Keyboard Shortcuts +```typescript +{ + command: 'copy', + title: 'Copy', + iconCssClass: 'mdi mdi-content-copy', + slotRenderer: () => ` +
      + + Copy + Ctrl+C +
      + ` +} +``` + +#### 2. Add Status Indicators +```typescript +{ + command: 'filter', + title: 'Filter', + iconCssClass: 'mdi mdi-filter', + slotRenderer: () => ` +
      + + Filter + +
      + ` +} +``` + +#### 3. Add Dynamic Content Based on Context +```typescript +{ + command: 'edit-row', + title: 'Edit Row', + slotRenderer: (cmdItem, args) => ` +
      + + Edit Row #${args.dataContext?.id || 'N/A'} +
      + ` +} +``` + +#### 4. Add Interactive Elements +```typescript +{ + command: 'toggle-setting', + title: 'Auto Refresh', + slotRenderer: (cmdItem, args, event) => { + const container = document.createElement('label'); + container.style.display = 'flex'; + container.style.alignItems = 'center'; + container.style.gap = '8px'; + container.style.marginRight = 'auto'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.addEventListener('change', (e) => { + // Prevent menu item click from firing when toggling checkbox + event?.stopPropagation?.(); + console.log('Auto refresh:', checkbox.checked); + }); + + const label = document.createElement('span'); + label.textContent = cmdItem.title; + + container.appendChild(label); + container.appendChild(checkbox); + return container; + } +} +``` + +#### 5. Add Badges and Status Labels +```typescript +{ + command: 'export-excel', + title: 'Export as Excel', + slotRenderer: (cmdItem, args) => ` +
      + + ${cmdItem.title} + RECOMMENDED +
      + ` +} +``` + +#### 6. Gradient and Styled Icons +```typescript +{ + command: 'advanced-export', + title: 'Advanced Export', + slotRenderer: (cmdItem, args) => { + const container = document.createElement('div'); + container.style.display = 'flex'; + container.style.alignItems = 'center'; + container.style.gap = '8px'; + + const iconDiv = document.createElement('div'); + iconDiv.style.width = '20px'; + iconDiv.style.height = '20px'; + iconDiv.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; + iconDiv.style.borderRadius = '4px'; + iconDiv.style.display = 'flex'; + iconDiv.style.alignItems = 'center'; + iconDiv.style.justifyContent = 'center'; + iconDiv.style.color = 'white'; + iconDiv.style.fontSize = '12px'; + iconDiv.innerHTML = '📊'; + + const textSpan = document.createElement('span'); + textSpan.textContent = cmdItem.title; + + container.appendChild(iconDiv); + container.appendChild(textSpan); + return container; + } +} +``` + +### Notes and Best Practices + +- **HTML strings** are inserted via `innerHTML` - ensure content is sanitized if user-provided +- **HTMLElement objects** are appended directly - safer for dynamic content and allows event listeners +- **Cross-framework compatible** - works in vanilla JS, Angular, React, Vue, Aurelia using the same API +- **Priority order** - Item-level `slotRenderer` overrides menu-level `defaultMenuItemRenderer` +- **Built-in command preservation** - When overriding a built-in command (e.g., `sort-asc`, `sort-desc`, `hide`, etc.) with custom properties like `slotRenderer` or `iconCssClass`, if you don't provide an `action` callback, the library will automatically preserve and use the built-in action for that command. This means you can safely customize the appearance of built-in commands without losing their functionality. +- **Accessibility** - Include proper ARIA attributes when creating custom elements +- **Event handling** - Call `event.stopPropagation()` in interactive elements to prevent menu commands from firing +- **Default fallback** - If neither `slotRenderer` nor `defaultMenuItemRenderer` is provided, the default icon + text rendering is used +- **Performance** - Avoid heavy DOM manipulation inside renderer callbacks (they may be called multiple times) +- **Event parameter** - The optional `event` parameter is passed during click handling and allows you to control menu behavior +- **All menus supported** - This API works uniformly across Header Menu, Cell Menu, Context Menu, and Grid Menu + +### Styling Custom Menu Items + +```css +/* Example CSS for styled menu items */ +.slick-menu-item { + padding: 4px 8px; +} + +.slick-menu-item div { + display: flex; + align-items: center; + gap: 8px; +} + +.slick-menu-item kbd { + background: #f0f0f0; + border: 1px solid #ddd; + border-radius: 3px; + padding: 2px 6px; + font-size: 11px; + font-family: monospace; + color: #666; +} + +.slick-menu-item .badge { + background: #ff6b6b; + color: white; + padding: 2px 6px; + border-radius: 3px; + font-size: 9px; + font-weight: bold; + white-space: nowrap; +} + +.slick-menu-item:hover { + background: #f5f5f5; +} + +.slick-menu-item.slick-menu-item-disabled { + opacity: 0.5; + cursor: not-allowed; +} +``` + +### Migration from Static Rendering + +**Before (Static HTML Title):** +```typescript +{ + command: 'action', + title: 'Action ⭐', // Emoji embedded in title + iconCssClass: 'mdi mdi-star' +} +``` + +**After (Custom Rendering):** +```typescript +{ + command: 'action', + title: 'Action', + slotRenderer: () => ` +
      + + Action + +
      + ` +} +``` + +### Error Handling + +When creating custom renderers, handle potential errors gracefully: + +```typescript +{ + command: 'safe-render', + title: 'Safe Render', + slotRenderer: (cmdItem, args) => { + try { + if (args?.dataContext?.status === 'error') { + return `
      ❌ Error loading
      `; + } + return `
      ✓ Data loaded
      `; + } catch (error) { + console.error('Render error:', error); + return '
      Render error
      '; + } + } +} +``` diff --git a/frameworks/angular-slickgrid/docs/migrations/migration-to-10.x.md b/frameworks/angular-slickgrid/docs/migrations/migration-to-10.x.md new file mode 100644 index 0000000000..175159a65c --- /dev/null +++ b/frameworks/angular-slickgrid/docs/migrations/migration-to-10.x.md @@ -0,0 +1,344 @@ +## 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... + +Also, this release fully aligns Angular-Slickgrid with modern Angular patterns, including Angular 21 support, Standalone Components for simplified setup, and zoneless change detection support which allows you to drop the `zone.js` dependency for improved performance and smaller bundle sizes. + +#### Major Changes - Quick Summary +- [`hidden` columns](#hidden-columns) +- [Row Detail (now optional)](#row-detail-now-optional) +- [ngx-translate@v17](#ngx-translate-v17x-now-required) +- [Migrating to Standalone Component](#migrating-to-standalone-component) +- [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 `
      ` to a `` which is native code, it has better accessibility (aria) support and a baseline support showing as "widely available". A fallback to `
      ` is also available in case `` doens't work for everybody (e.g. it doesn't work in Salesforce LWC, hence the available fallback) +2. Reimplementing Grid Menu to use CSS flexbox instead of using `calc(100% - 18px)` which wasn't ideal, neither customizable, but the new approach is to simply use CSS flexbox which is a much better approach to properly align everything. + +### Row Detail (now optional) + +Since I don't think that Row Detail is being used by everyone, I'm making it an optional plugin (package). This should help decrease build size quite a bit for users who never use it. If however you are one of them using it, then you now need to manually add it as an external resource. + +```diff ++ import { AngularSlickRowDetailView } from '@slickgrid-universal/angular-row-detail-plugin'; + +export class Example { + defineGrid() { + this.gridOptions = { + enableRowDetailView: true, ++ externalResources: [AngularSlickRowDetailView], + rowDetailView: { + // ... + } + }; + } +} +``` + +## Changes + +### Removed `@deprecated` code + +_following changes should be transparent to most users_ + +1. `applyHtmlCode()` was removed and replaced with `applyHtmlToElement()` +2. Grid Option `throwWhenFrozenNotAllViewable` was removed and replaced with `invalidColumnFreezeWidthCallback` + +### Selection Models, keeping only `SlickHybridSelectionModel` + +1. rename `rowSelectionOptions` to `selectionOptions` +2. drop both `SlickCellSelectionModel`/`SlickRowSelectionModel` and keep only `SlickHybridSelectionModel` +3. drop both `enableHybridSelection`/`enableRowSelection` merge them into a new `enableSelection` grid option + +`SlickHybridSelectionModel` was previously introduced in order to merge and allow using both Cell/Row Selections separately and/or in combo on the same grid. It was introduced in v9.x to test it out and after testing it for a few months, it's now safe to drop the older `SlickCellSelectionModel` / `SlickRowSelectionModel` models and keep only the hybrid model. Also, since we now have the Hybrid model and it's now accepting options for different selection models, I think it's better to rename `rowSelectionOptions` to `selectionOptions` since it now makes more sense with the hybrid approach. + +```diff +gridOptions = { +- enableHybridSelection: true, +- enableRowSelection: true, ++ enableSelection: true, + +- rowSelectionOptions: { ++ selectionOptions: { + selectActiveRow: false, + + // type can be: ['cell','row','mixed'] defaults to 'mixed' ++ selectionType: 'mixed', + } +}; +``` + +This change does not require any code update from the end user, but it is a change that you should probably be aware of nonetheless. The reason I decided to implement this is because I often forget myself to enable the associated flag and typically if you wanted to load the resource, then it's most probably because you also want it enabled. So for example, if your register `ExcelExportService` then the library will now auto-enable the resource with its associated flag (which in this case is `enableExcelExport:true`)... unless you already disabled the flag (or enabled) yourself, if so then the internal assignment will simply be skipped and yours will prevail. Also just to be clear, the list of auto-enabled external resources is rather small, it will auto-enable the following resources: +(ExcelExportService, PdfExportService, TextExportService, CompositeEditorComponent and RowDetailView). + +### Menu with Commands + +All menu plugins (Cell Menu, Context Menu, Header Menu and Grid Menu) now have a new `commandListBuilder: (items) => items` which now allow you to filter/sort and maybe override built-in commands. With this new feature in place, I'm deprecating all `hide...` properties and also `positionOrder` since you can now do that with the builder. You could also use the `hideCommands` which accepts an array of built-in command names. This well remove huge amount of `hide...` properties (over 30) that keeps increasing anytime a new built-in command gets added (in other words, this will simplify maintenance for both you and me). + +These are currently just deprecations in v10.x but it's strongly recommended to start using the `commandListBuilder` and/or `hideCommands` and move away from the deprecated properties which will be removed in v11.x. For example if we want to hide some built-in commands: + +```diff +gridOptions = { + gridMenu: { +- hideExportCsvCommand: true, +- hideTogglePreHeaderCommand: true, + +// via command name(s) ++ hideCommands: ['export-csv', 'toggle-preheader'], + +// or via builder ++ commandListBuilder: (cmdItems) => [...cmdItems.filter(x => x !== 'divider' && x.command !== 'export-csv' && x.command !== 'toggle-preheader')] + } +} +``` + +### `ngx-translate` v17.x now required + +Because of the Angular v21 upgrade, the user (you) will also need to upgrade [`ngx-translate`](https://ngx-translate.org/) to its latest version 17.x. + +```diff +# package.json +"dependencies": { +- "@ngx-translate/core": "^16.0.4", +- "@ngx-translate/http-loader": "^16.0.1", ++ "@ngx-translate/core": "^17.0.0", ++ "@ngx-translate/http-loader": "^17.0.0", +} +``` + +For the complete list of changes, please follow `ngx-translate` migration from their website: +- https://ngx-translate.org/getting-started/migration-guide/ + +### Angular Zoneless Mode + +Starting with v10, Angular-Slickgrid itself now runs in zoneless mode by default. However, your application can still use `zone.js` if you wish and this is entirely at your discretion. For more information about zoneless Angular, see the official Angular documentation: https://angular.dev/guide/zoneless + +#### Supporting Both Zone.js and Zoneless Users + +Angular-Slickgrid now works out-of-the-box in zoneless Angular apps, but still supports applications using `zone.js`: + +- If your app uses `zone.js`, you do not need to change anything, manual change detection (e.g., `markForCheck()`, `detectChanges()`) is still not required. +- If your app is zoneless, you do not need to add `zone.js` and should follow the zoneless setup instructions above. +- The library no longer calls `markForCheck()` or `detectChanges()` internally, so UI updates are handled automatically in both modes. +- If you have custom code that relies on manual change detection, review and update it as needed. +- For example, I had to use Signal to ensure UI changes were detected in my OData/GraphQL demos when using the `BackendServiceApi` with a `postProcess` callback and you might need changes when using Pagination as well (in my case I switched to Signals). + +> **Tip:** In zoneless Angular, always use signals for any state that should update the UI. For example, if you have a property like `selectedLanguage`, declare it as a signal (`selectedLanguage = signal('en')`) and update it with `selectedLanguage.set('fr')`. In your template, use `selectedLanguage()` to display or bind the value. This ensures UI updates are automatic and you never need manual change detection. + +For more details, review the official Angular documentation: https://angular.dev/guide/zoneless + +### Migrating to Standalone Component + +Angular-Slickgrid is now a Standalone Component and the `AngularSlickgridModule` was dropped, this also requires you to make some small changes in your App `main.ts` and in all your components that use Angular-Slickgrid. + +```diff +// main.ts +- import { AngularSlickgridModule } from 'angular-slickgrid'; ++ import { AngularSlickgridComponent, GridOption } from 'angular-slickgrid'; + +// optional global Grid Option ++ const gridOptionConfig: GridOption = { ++ // ... ++ sanitizer: (dirtyHtml) => DOMPurify.sanitize(dirtyHtml, { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }), ++ }; + +bootstrapApplication(AppComponent, { + providers: [ + importProvidersFrom( +- AngularSlickgridModule.forRoot({ +- // ... +- sanitizer: (dirtyHtml) => DOMPurify.sanitize(dirtyHtml, { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }), +- }) + ), ++ AngularSlickgridComponent, ++ { provide: 'defaultGridOption', useValue: gridOptionConfig }, + // ... + ], +}); +``` +```diff +// ... all your Components +- import { AngularSlickgridModule } from 'angular-slickgrid'; ++ import { AngularSlickgridComponent } from 'angular-slickgrid'; + +@Component({ + // ... +- imports: [AngularSlickgridModule], ++ imports: [AngularSlickgridComponent], +}) +``` + +--- + +{% hint style="note" %} +**Info** the changes in the next few lines were all mentioned in the previous "Migration Guide v9.0". So, if you have already made these changes then you could skip the section below **but** scroll down further to read the last section "What's next? v11?". +{% endhint %} + +### Interfaces / Enums changes + +Removing most Enums and replacing them with string literal types (`type` instead of `enum` because again `type` aren't transpiled and `enum` are). Making this change will help decrease the build size by transpiling a lot less code. + +```diff +columns = [{ + id: 'age', ... +- type: FieldType.number, ++ type: 'number', +}]; +``` + +Below is a list of Enums that you need to replace with their associated string literals. A suggestion is to do a Search on any of these group name prefixes, e.g.: `FieldType.` and start replacing them + +**Hint** You can use VSCode search & replace, but make sure it's set to Regular Expression pattern + +| Search (regex) | Replace | +| ------------------------------ | -------- | +| `FieldType\.([a-z_]+)(.*)` | `'$1'$2` | + +| Enum Name | from `enum` | to string `type` | Note | +| ----------- | ------------------- | ------------------- | ---- | +| `DelimiterType` | `DelimiterType.comma` | `','` | +| | `DelimiterType.colon` | `':'` | +| | `DelimiterType.space` | `' '` | +| ... | ... | ... | +| `EventNamingStyle` | `EventNamingStyle.camelCase` | `'camelCase'` | +| | `EventNamingStyle.kebabCase` | `'kebabCase'` | +| | `EventNamingStyle.lowerCase` | `'lowerCase'` | +| ... | ... | ... | +| `FieldType` | `FieldType.boolean` | `'boolean'` | +| | `FieldType.number` | `'number'` | +| | `FieldType.dateIso` | `'dateIso'` | +| ... | ... | ... | +| `FileType` | `FileType.csv` | `'csv'` | +| | `FileType.xlsx` | `'xlsx'` | +| ... | ... | ... | +| `GridStateType` | `GridStateType.columns` | `'columns'` | +| | `GridStateType.filters` | `'filters'` | +| | `GridStateType.sorters` | `'sorters'` | +| ... | ... | ... | +| `OperatorType` | `OperatorType.greaterThan` | `'>'` or `'GT'` | See [Operator](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/enums/operator.type.ts) list for all available operators | +| | `OperatorType.lessThanOrEqual` | `'<='` or `'LE'` | +| | `OperatorType.contains` | `'Contains'` or `'CONTAINS'` | Operators are written as PascalCase | +| | `OperatorType.equal` | `'='` or `'EQ'` | +| | `OperatorType.rangeExclusive` | `'RangeExclusive'` | +| ... | ... | ... | +| `SortDirection` | `SortDirection.ASC` | `'ASC'` or `'asc'` | +| | `SortDirection.DESC` | `'DESC'` or `'desc'` | +| ... | ... | ... | + +#### renaming `editorOptions` and `filterOptions` to a more generic `options` property + +```diff +import { type MultipleSelectOption } from '@slickgrid-universal/common'; + +columnDefinitions = [{ + id: 'duration', field: 'duration', name: 'Duration', + editor: { +- editorOptions: { ++ options: { + maxHeight: 250, useSelectOptionLabelToHtml: true, + } as MultipleSelectOption, + }, + filter: { +- filterOptions: { ++ options: { + maxHeight: 250, useSelectOptionLabelToHtml: true, + } as MultipleSelectOption, + } +}]; + +// or reuse the same `options` ++ const msOptions = { maxHeight: 250, useSelectOptionLabelToHtml: true } as MultipleSelectOption; + +columnDefinitions = [{ + id: 'duration', field: 'duration', name: 'Duration', + editor: { ++ options: msOptions, + }, + filter: { ++ options: msOptions, + }, +}]; +``` + +#### renaming all `text-color-xyz` to `color-xyz` + +I decided to remove all `text-color-...` and rename them all to `color-...` which is much shorter and easier to use. + +You can do a "Search and Replace" in VSCode via Regular Expressions to replace them all easily: + +| Search | Replace | +| ------------- | -------- | +| `text-color-` | `color-` | + +For example: +```diff +- Primary Text ++ Primary Text +``` + +#### renaming all `mdi-[0-9]px` to `font-[0-9]px` + +Since I had 2 CSS utilities that do exactly the same, I'm dropping all `mdi-..px` in favor of `font-..px` because it makes more sense to represent font sizes that also work on any type of elements (not just icons). + +You can do a "Search and Replace" in VSCode via Regular Expressions to replace them all easily (**make sure to use `regex` in VSCode Search & Replace**): + +| Search (regex) | Replace | +| ---------------- | ----------- | +| `mdi-([0-9]*)px` | `font-$1px` | + +For example: +```diff +- Checkmark Icon ++ Checkmark Icon +``` + +--- + +## What's next? ...version 11? + +Wait, are you seriously talking about version 11 akready when version 10 actually just shipped? Thats right, I'm already thinking and planning ahead the next major version, which will be in about a year from now. I can already say that the main focus will be around the use of native [CSS anchor positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning) to replace JS code for positioning menus, tooltips, etc... which will help decreasing the build size by using fully native code. CSS anchoring has been around in Chrome for a while but is quite recent in Firefox, so for that reason I'm postponing it for next year. + +### Code being `@deprecated` (to be removed in the future, 2027-Q1) +#### You can already start using these new options and props (shown below) in v10.0 and above. + +Deprecating `ExtensionName` enum which will be replaced by its string literal type, for example: + +**Hint** You can use VSCode search & replace, but make sure it's set to Regular Expression pattern + +| Search (regex) | Replace | +| ------------------------------ | -------- | +| `ExtensionName\.([a-z_]+)(.*)` | `'$1'$2` | + +| Enum Name | from `enum` | to string `type` | +| ---------------- | --------------------------------- | --------------------- | +| `ExtensionName` | `ExtensionName.autoTooltip` | `'autoTooltip'` | +| | `ExtensionName.draggableGrouping` | `'draggableGrouping'` | +| | `ExtensionName.rowDetail` | `'rowDetail'` | +| ... | ... | ... | + +### Potential but Postponed Code Change + +Signals are becoming increasingly prevalent in Angular, however Angular-Slickgrid continues to use traditional `@Input`/`@Output` decorators. Users who prefer Signals can still use them by calling signal functions in templates: `[dataset]="dataset()"`. + +For a library component, maintaining compatibility with both approaches is pragmatic and may not require a full migration. If we decide to migrate Angular-Slickgrid to use Signals internally, this change would be deferred to version 11 or later. \ No newline at end of file diff --git a/frameworks/angular-slickgrid/docs/migrations/migration-to-9.x.md b/frameworks/angular-slickgrid/docs/migrations/migration-to-9.x.md index e7928e979b..4fd88ffdbe 100644 --- a/frameworks/angular-slickgrid/docs/migrations/migration-to-9.x.md +++ b/frameworks/angular-slickgrid/docs/migrations/migration-to-9.x.md @@ -182,29 +182,29 @@ Below is a list of Enums being deprecated and you should think about migrating t | `FieldType` | `FieldType.boolean` | `'boolean'` | | | `FieldType.number` | `'number'` | | | `FieldType.dateIso` | `'dateIso'` | -| | ... | ... | +| ... | ... | ... | | `FileType` | `FileType.csv` | `'csv'` | | | `FileType.xlsx` | `'xlsx'` | -| | ... | ... | +| ... | ... | ... | | `GridStateType` | `GridStateType.columns` | `'columns'` | | | `GridStateType.filters` | `'filters'` | | | `GridStateType.sorters` | `'sorters'` | -| | ... | ... | +| ... | ... | ... | | `OperatorType` | `OperatorType.greaterThan` | `'>'` or `'GT'` | See [Operator](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/enums/operator.type.ts) list for all available operators | | | `OperatorType.lessThanOrEqual` | `'<='` or `'LE'` | | | `OperatorType.contains` | `'Contains'` or `'CONTAINS'` | Operators are written as PascalCase | | | `OperatorType.equal` | `'='` or `'EQ'` | | | `OperatorType.rangeExclusive` | `'RangeExclusive'` | -| | ... | ... | +| ... | ... | ... | | `SortDirection` | `SortDirection.ASC` | `'ASC'` or `'asc'` | | | `SortDirection.DESC` | `'DESC'` or `'desc'` | -| | ... | ... | +| ... | ... | ... | **Hint** You can use VSCode search & replace, but make sure it's set to Regular Expression pattern -| Search (regex) | Replace | -| ------------------------------ | -------- | -| `FieldType\.([a-z_]+)(.*)` | `'$1'$2` | +| Search (regex) | Replace | +| -------------------------- | -------- | +| `FieldType\.([a-z_]+)(.*)` | `'$1'$2` | ##### deprecating `editorOptions` and `filterOptions`, they are being renamed as a more generic `options` name @@ -245,7 +245,7 @@ columnDefinitions = [{ ##### deprecating `text-color-xyz` and renaming them all to `color-xyz` -I decided to deprecate all `text-color-...` and renaming them all to `color-...` which is much simpler to type and use. +I decided to deprecate all `text-color-...` and renaming them all to `color-...` which is much simpler to type and use. You can do a "Search and Replace" in VSCode via Regular Expressions to replace them all easily: diff --git a/frameworks/angular-slickgrid/docs/styling/styling.md b/frameworks/angular-slickgrid/docs/styling/styling.md index 7096abaa8e..bfb2c903fb 100644 --- a/frameworks/angular-slickgrid/docs/styling/styling.md +++ b/frameworks/angular-slickgrid/docs/styling/styling.md @@ -3,6 +3,7 @@ - [Using CSS Variables](#using-css-variables-instead-of-sass) - [Bootstrap & Other Frameworks](#bootstrap-support) - [SVG Icons](#svg-icons) +- [Injecting css classes into grid container](#injecting-css-classes-into-grid-container) ### CSS/SASS Styles Load the default Bootstrap theme style and/or customize it to your taste (customization can applied by using SASS) @@ -248,3 +249,17 @@ $color-darken-percentage: 6%; color-se-warning-light
      ``` + +### Injecting css classes into grid container +If you want to inject custom css classes into the inner grids container, you can do so by providing an array of strings to the `containerClasses` property of the Grid. + +```html + +``` \ No newline at end of file diff --git a/frameworks/angular-slickgrid/package.json b/frameworks/angular-slickgrid/package.json index ccdac22d01..2175da5bb4 100644 --- a/frameworks/angular-slickgrid/package.json +++ b/frameworks/angular-slickgrid/package.json @@ -1,6 +1,6 @@ { "name": "angular-slickgrid", - "version": "9.13.0", + "version": "10.0.0-beta.0", "description": "Slickgrid components made available in Angular", "keywords": [ "angular", @@ -42,54 +42,47 @@ "pack-tarball": "npm pack", "pack-lib": "npm pack ./dist", "replace-workspace": "node ./scripts/replace-workspace.mjs", - "test": "ng test --watch", + "test": "vitest --watch", "test:coverage": "vitest --no-watch --coverage" }, "dependencies": { - "@ngx-translate/core": "^16.0.4", + "@ngx-translate/core": "^17.0.0", "@slickgrid-universal/common": "workspace:*", "@slickgrid-universal/custom-footer-component": "workspace:*", "@slickgrid-universal/empty-warning-component": "workspace:*", "@slickgrid-universal/event-pub-sub": "workspace:*", "@slickgrid-universal/pagination-component": "workspace:*", - "@slickgrid-universal/row-detail-view-plugin": "workspace:*", "@slickgrid-universal/rxjs-observable": "workspace:*", "@slickgrid-universal/utils": "workspace:*", "dequal": "catalog:", "rxjs": "catalog:" }, "peerDependencies": { - "@angular/core": ">=19.0.0" + "@angular/core": ">=21.0.0" }, "devDependencies": { "@4tw/cypress-drag-drop": "catalog:", - "@analogjs/platform": "^2.2.2", - "@analogjs/vite-plugin-angular": "^2.2.2", - "@analogjs/vitest-angular": "^2.2.2", - "@angular-devkit/build-angular": "^19.2.19", - "@angular-eslint/eslint-plugin": "^20.7.0", - "@angular/animations": "^19.2.18", - "@angular/build": "19.2.19", - "@angular/cli": "^19.2.19", - "@angular/common": "^19.2.18", - "@angular/compiler": "^19.2.18", - "@angular/compiler-cli": "^19.2.18", - "@angular/core": "^19.2.18", - "@angular/forms": "^19.2.18", - "@angular/language-service": "^19.2.18", - "@angular/platform-browser": "^19.2.18", - "@angular/platform-browser-dynamic": "^19.2.18", - "@angular/router": "^19.2.18", + "@angular-eslint/eslint-plugin": "catalog:", + "@angular/animations": "^21.1.4", + "@angular/build": "^21.1.4", + "@angular/cli": "^21.1.4", + "@angular/common": "^21.1.4", + "@angular/compiler": "^21.1.4", + "@angular/compiler-cli": "^21.1.4", + "@angular/core": "^21.1.4", + "@angular/forms": "^21.1.4", + "@angular/language-service": "^21.1.4", + "@angular/platform-browser": "^21.1.4", + "@angular/platform-browser-dynamic": "^21.1.4", + "@angular/router": "^21.1.4", "@faker-js/faker": "catalog:", "@fnando/sparkline": "catalog:", "@formkit/tempo": "catalog:", "@gc-utils/fs-extra": "^0.4.0", - "@ng-select/ng-select": "^14.9.0", - "@ngx-translate/http-loader": "^16.0.1", - "@nx/angular": "^21.6.10", - "@nx/devkit": "^21.6.10", - "@nx/vite": "^21.6.10", + "@ng-select/ng-select": "^21.4.0", + "@ngx-translate/http-loader": "^17.0.0", "@popperjs/core": "catalog:", + "@slickgrid-universal/angular-row-detail-plugin": "link:../../frameworks-plugins/angular-row-detail-plugin", "@slickgrid-universal/composite-editor-component": "workspace:*", "@slickgrid-universal/custom-tooltip-plugin": "workspace:*", "@slickgrid-universal/excel-export": "workspace:*", @@ -109,18 +102,17 @@ "jsdom": "catalog:", "jsdom-global": "catalog:", "native-copyfiles": "catalog:", - "ng-packagr": "^19.2.2", - "ngx-bootstrap": "^19.0.2", + "ng-packagr": "^21.1.0", + "ngx-bootstrap": "^21.0.1", "oxlint": "catalog:", "remove-glob": "catalog:", "rxjs": "catalog:", "sass": "catalog:", "sortablejs": "catalog:", "tslib": "catalog:", - "typescript": "~5.8.3", - "vite": "catalog:vite6", + "typescript": "~5.9.3", + "vite": "catalog:", "vitest": "catalog:", - "yaml": "^2.8.2", - "zone.js": "~0.16.0" + "yaml": "^2.8.2" } } diff --git a/frameworks/angular-slickgrid/src/demos/app-routing.module.ts b/frameworks/angular-slickgrid/src/demos/app-routing.module.ts index 312ef50bbc..7b1d88e1bf 100644 --- a/frameworks/angular-slickgrid/src/demos/app-routing.module.ts +++ b/frameworks/angular-slickgrid/src/demos/app-routing.module.ts @@ -1,117 +1,58 @@ -import { NgModule } from '@angular/core'; -import { provideRouter, RouterModule, type Routes } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; -import { Example1Component } from './examples/example01.component'; -import { Example2Component } from './examples/example02.component'; -import { Example3Component } from './examples/example03.component'; -import { Example4Component } from './examples/example04.component'; -import { Example5Component } from './examples/example05.component'; -import { Example6Component } from './examples/example06.component'; -import { Example7Component } from './examples/example07.component'; -import { Example8Component } from './examples/example08.component'; -import { Example9Component } from './examples/example09.component'; -import { Example10Component } from './examples/example10.component'; -import { Example11Component } from './examples/example11.component'; -import { Example12Component } from './examples/example12.component'; -import { Example13Component } from './examples/example13.component'; -import { Example14Component } from './examples/example14.component'; -import { Example15Component } from './examples/example15.component'; -import { Example16Component } from './examples/example16.component'; -import { Example17Component } from './examples/example17.component'; -import { Example18Component } from './examples/example18.component'; -import { Example19Component } from './examples/example19.component'; -import { Example20Component } from './examples/example20.component'; -import { Example21Component } from './examples/example21.component'; -import { Example22Component } from './examples/example22.component'; -import { Example23Component } from './examples/example23.component'; -import { Example24Component } from './examples/example24.component'; -import { Example25Component } from './examples/example25.component'; -import { Example26Component } from './examples/example26.component'; -import { Example27Component } from './examples/example27.component'; -import { Example28Component } from './examples/example28.component'; -import { Example29Component } from './examples/example29.component'; -import { Example30Component } from './examples/example30.component'; -import { Example32Component } from './examples/example32.component'; -import { Example33Component } from './examples/example33.component'; -import { Example34Component } from './examples/example34.component'; -import { Example35Component } from './examples/example35.component'; -import { Example36Component } from './examples/example36.component'; -import { Example37Component } from './examples/example37.component'; -import { Example38Component } from './examples/example38.component'; -import { Example39Component } from './examples/example39.component'; -import { Example40Component } from './examples/example40.component'; -import { Example41Component } from './examples/example41.component'; -import { Example42Component } from './examples/example42.component'; -import { Example43Component } from './examples/example43.component'; -import { Example44Component } from './examples/example44.component'; -import { Example45Component } from './examples/example45.component'; -import { Example46Component } from './examples/example46.component'; -import { Example47Component } from './examples/example47.component'; -import { Example48Component } from './examples/example48.component'; -import { Example49Component } from './examples/example49.component'; -import { Example50Component } from './examples/example50.component'; -import { HomeComponent } from './examples/home.component'; -import { SwtCommonGridTestComponent } from './examples/swt-common-grid-test.component'; +import { type Routes } from '@angular/router'; -const routes: Routes = [ - { path: 'home', component: HomeComponent }, - { path: 'example01', component: Example1Component }, - { path: 'example02', component: Example2Component }, - { path: 'example03', component: Example3Component }, - { path: 'example04', component: Example4Component }, - { path: 'example05', component: Example5Component }, - { path: 'example06', component: Example6Component }, - { path: 'example07', component: Example7Component }, - { path: 'example08', component: Example8Component }, - { path: 'example09', component: Example9Component }, - { path: 'example10', component: Example10Component }, - { path: 'example11', component: Example11Component }, - { path: 'example12', component: Example12Component }, - { path: 'example13', component: Example13Component }, - { path: 'example14', component: Example14Component }, - { path: 'example15', component: Example15Component }, - { path: 'example16', component: Example16Component }, - { path: 'example17', component: Example17Component }, - { path: 'example18', component: Example18Component }, - { path: 'example19', component: Example19Component }, - { path: 'example20', component: Example20Component }, - { path: 'example21', component: Example21Component }, - { path: 'example22', component: Example22Component }, - { path: 'example23', component: Example23Component }, - { path: 'example24', component: Example24Component }, - { path: 'example25', component: Example25Component }, - { path: 'example26', component: Example26Component }, - { path: 'example27', component: Example27Component }, - { path: 'example28', component: Example28Component }, - { path: 'example29', component: Example29Component }, - { path: 'example30', component: Example30Component }, - { path: 'example31', component: SwtCommonGridTestComponent }, - { path: 'example32', component: Example32Component }, - { path: 'example33', component: Example33Component }, - { path: 'example34', component: Example34Component }, - { path: 'example35', component: Example35Component }, - { path: 'example36', component: Example36Component }, - { path: 'example37', component: Example37Component }, - { path: 'example38', component: Example38Component }, - { path: 'example39', component: Example39Component }, - { path: 'example40', component: Example40Component }, - { path: 'example41', component: Example41Component }, - { path: 'example42', component: Example42Component }, - { path: 'example43', component: Example43Component }, - { path: 'example44', component: Example44Component }, - { path: 'example45', component: Example45Component }, - { path: 'example46', component: Example46Component }, - { path: 'example47', component: Example47Component }, - { path: 'example48', component: Example48Component }, - { path: 'example49', component: Example49Component }, - { path: 'example50', component: Example50Component }, +export const routes: Routes = [ + { path: 'home', loadComponent: () => import('./examples/home.component').then((m) => m.HomeComponent) }, + { path: 'example01', loadComponent: () => import('./examples/example01.component').then((m) => m.Example1Component) }, + { path: 'example02', loadComponent: () => import('./examples/example02.component').then((m) => m.Example2Component) }, + { path: 'example03', loadComponent: () => import('./examples/example03.component').then((m) => m.Example3Component) }, + { path: 'example04', loadComponent: () => import('./examples/example04.component').then((m) => m.Example4Component) }, + { path: 'example05', loadComponent: () => import('./examples/example05.component').then((m) => m.Example5Component) }, + { path: 'example06', loadComponent: () => import('./examples/example06.component').then((m) => m.Example6Component) }, + { path: 'example07', loadComponent: () => import('./examples/example07.component').then((m) => m.Example7Component) }, + { path: 'example08', loadComponent: () => import('./examples/example08.component').then((m) => m.Example8Component) }, + { path: 'example09', loadComponent: () => import('./examples/example09.component').then((m) => m.Example9Component) }, + { path: 'example10', loadComponent: () => import('./examples/example10.component').then((m) => m.Example10Component) }, + { path: 'example11', loadComponent: () => import('./examples/example11.component').then((m) => m.Example11Component) }, + { path: 'example12', loadComponent: () => import('./examples/example12.component').then((m) => m.Example12Component) }, + { path: 'example13', loadComponent: () => import('./examples/example13.component').then((m) => m.Example13Component) }, + { path: 'example14', loadComponent: () => import('./examples/example14.component').then((m) => m.Example14Component) }, + { path: 'example15', loadComponent: () => import('./examples/example15.component').then((m) => m.Example15Component) }, + { path: 'example16', loadComponent: () => import('./examples/example16.component').then((m) => m.Example16Component) }, + { path: 'example17', loadComponent: () => import('./examples/example17.component').then((m) => m.Example17Component) }, + { path: 'example18', loadComponent: () => import('./examples/example18.component').then((m) => m.Example18Component) }, + { path: 'example19', loadComponent: () => import('./examples/example19.component').then((m) => m.Example19Component) }, + { path: 'example20', loadComponent: () => import('./examples/example20.component').then((m) => m.Example20Component) }, + { path: 'example21', loadComponent: () => import('./examples/example21.component').then((m) => m.Example21Component) }, + { path: 'example22', loadComponent: () => import('./examples/example22.component').then((m) => m.Example22Component) }, + { path: 'example23', loadComponent: () => import('./examples/example23.component').then((m) => m.Example23Component) }, + { path: 'example24', loadComponent: () => import('./examples/example24.component').then((m) => m.Example24Component) }, + { path: 'example25', loadComponent: () => import('./examples/example25.component').then((m) => m.Example25Component) }, + { path: 'example26', loadComponent: () => import('./examples/example26.component').then((m) => m.Example26Component) }, + { path: 'example27', loadComponent: () => import('./examples/example27.component').then((m) => m.Example27Component) }, + { path: 'example28', loadComponent: () => import('./examples/example28.component').then((m) => m.Example28Component) }, + { path: 'example29', loadComponent: () => import('./examples/example29.component').then((m) => m.Example29Component) }, + { path: 'example30', loadComponent: () => import('./examples/example30.component').then((m) => m.Example30Component) }, + { path: 'example31', loadComponent: () => import('./examples/swt-common-grid-test.component').then((m) => m.SwtCommonGridTestComponent) }, + { path: 'example32', loadComponent: () => import('./examples/example32.component').then((m) => m.Example32Component) }, + { path: 'example33', loadComponent: () => import('./examples/example33.component').then((m) => m.Example33Component) }, + { path: 'example34', loadComponent: () => import('./examples/example34.component').then((m) => m.Example34Component) }, + { path: 'example35', loadComponent: () => import('./examples/example35.component').then((m) => m.Example35Component) }, + { path: 'example36', loadComponent: () => import('./examples/example36.component').then((m) => m.Example36Component) }, + { path: 'example37', loadComponent: () => import('./examples/example37.component').then((m) => m.Example37Component) }, + { path: 'example38', loadComponent: () => import('./examples/example38.component').then((m) => m.Example38Component) }, + { path: 'example39', loadComponent: () => import('./examples/example39.component').then((m) => m.Example39Component) }, + { path: 'example40', loadComponent: () => import('./examples/example40.component').then((m) => m.Example40Component) }, + { path: 'example41', loadComponent: () => import('./examples/example41.component').then((m) => m.Example41Component) }, + { path: 'example42', loadComponent: () => import('./examples/example42.component').then((m) => m.Example42Component) }, + { path: 'example43', loadComponent: () => import('./examples/example43.component').then((m) => m.Example43Component) }, + { path: 'example44', loadComponent: () => import('./examples/example44.component').then((m) => m.Example44Component) }, + { path: 'example45', loadComponent: () => import('./examples/example45.component').then((m) => m.Example45Component) }, + { path: 'example46', loadComponent: () => import('./examples/example46.component').then((m) => m.Example46Component) }, + { path: 'example47', loadComponent: () => import('./examples/example47.component').then((m) => m.Example47Component) }, + { path: 'example48', loadComponent: () => import('./examples/example48.component').then((m) => m.Example48Component) }, + { path: 'example49', loadComponent: () => import('./examples/example49.component').then((m) => m.Example49Component) }, + { path: 'example50', loadComponent: () => import('./examples/example50.component').then((m) => m.Example50Component) }, + { path: 'example51', loadComponent: () => import('./examples/example51.component').then((m) => m.Example51Component) }, { path: '', redirectTo: '/example34', pathMatch: 'full' }, { path: '**', redirectTo: '/example34', pathMatch: 'full' }, ]; - -@NgModule({ - imports: [RouterModule.forRoot(routes, { useHash: true }), TranslateModule], - exports: [RouterModule, TranslateModule], - providers: [provideRouter(routes)], -}) -export class AppRoutingRoutingModule {} diff --git a/frameworks/angular-slickgrid/src/demos/app.component.html b/frameworks/angular-slickgrid/src/demos/app.component.html index df1f413eaf..d952e76aba 100644 --- a/frameworks/angular-slickgrid/src/demos/app.component.html +++ b/frameworks/angular-slickgrid/src/demos/app.component.html @@ -17,7 +17,7 @@ @@ -190,6 +190,9 @@ + diff --git a/frameworks/angular-slickgrid/src/demos/app.module.ts b/frameworks/angular-slickgrid/src/demos/app.initializer.ts similarity index 72% rename from frameworks/angular-slickgrid/src/demos/app.module.ts rename to frameworks/angular-slickgrid/src/demos/app.initializer.ts index dcb79c8ebf..c43f4d50c4 100644 --- a/frameworks/angular-slickgrid/src/demos/app.module.ts +++ b/frameworks/angular-slickgrid/src/demos/app.initializer.ts @@ -1,13 +1,6 @@ import { LOCATION_INITIALIZED } from '@angular/common'; -import { HttpClient } from '@angular/common/http'; import { type Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; - -// AoT requires an exported function for factories -export function createTranslateLoader(http: HttpClient) { - return new TranslateHttpLoader(http, './assets/i18n/', '.json'); -} // use an Initializer Factory as describe here: https://github.com/ngx-translate/core/issues/517#issuecomment-299637956 export function appInitializerFactory(translate: TranslateService, injector: Injector) { @@ -16,7 +9,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj const locationInitialized = injector.get(LOCATION_INITIALIZED, Promise.resolve(null)); locationInitialized.then(() => { const langToSet = 'en'; - translate.setDefaultLang('en'); + translate.setFallbackLang('en'); translate.use(langToSet).subscribe({ next: () => { // console.info(`Successfully initialized '${langToSet}' language.'`); @@ -27,5 +20,3 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj }); }); } - -// @dynamic diff --git a/frameworks/angular-slickgrid/src/demos/examples/custom-angularComponentEditor.ts b/frameworks/angular-slickgrid/src/demos/examples/custom-angularComponentEditor.ts index c7a57a6bf3..b6ecc73d0d 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/custom-angularComponentEditor.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/custom-angularComponentEditor.ts @@ -32,7 +32,7 @@ export class CustomAngularComponentEditor implements Editor { grid: SlickGrid; constructor(private args: any) { - this.grid = args && args.grid; + this.grid = args?.grid; this.init(); } diff --git a/frameworks/angular-slickgrid/src/demos/examples/custom-angularComponentFilter.ts b/frameworks/angular-slickgrid/src/demos/examples/custom-angularComponentFilter.ts index b8b9dd44b2..f718fc4e6e 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/custom-angularComponentFilter.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/custom-angularComponentFilter.ts @@ -2,7 +2,6 @@ import type { ComponentRef } from '@angular/core'; import type { Subscription } from 'rxjs'; import { AngularUtilService, - OperatorType, unsubscribeAllObservables, type Column, type ColumnFilter, @@ -10,7 +9,7 @@ import { type FilterArguments, type FilterCallback, type GridOption, - type OperatorString, + type OperatorType, type SearchTerm, type SlickGrid, } from '../../library'; @@ -27,7 +26,7 @@ export class CustomAngularComponentFilter implements Filter { searchTerms: SearchTerm[] = []; columnDef!: Column; callback!: FilterCallback; - operator: OperatorType | OperatorString = OperatorType.equal; + operator: OperatorType = 'EQ'; /** Angular Util Service (could be inside the Grid Options Params or the Filter Params ) */ get angularUtilService(): AngularUtilService { diff --git a/frameworks/angular-slickgrid/src/demos/examples/custom-inputFilter.ts b/frameworks/angular-slickgrid/src/demos/examples/custom-inputFilter.ts index 12765daa11..f708dee75e 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/custom-inputFilter.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/custom-inputFilter.ts @@ -1,14 +1,13 @@ -import { +import type { + Column, + ColumnFilter, + Filter, + FilterArguments, + FilterCallback, + GridOption, OperatorType, - type Column, - type ColumnFilter, - type Filter, - type FilterArguments, - type FilterCallback, - type GridOption, - type OperatorString, - type SearchTerm, - type SlickGrid, + SearchTerm, + SlickGrid, } from '../../library'; export class CustomInputFilter implements Filter { @@ -19,7 +18,7 @@ export class CustomInputFilter implements Filter { searchTerms: SearchTerm[] = []; columnDef!: Column; callback!: FilterCallback; - operator: OperatorType | OperatorString = OperatorType.equal; + operator: OperatorType = 'EQ'; /** Getter for the Column Filter */ get columnFilter(): ColumnFilter { diff --git a/frameworks/angular-slickgrid/src/demos/examples/editor-ng-select.component.ts b/frameworks/angular-slickgrid/src/demos/examples/editor-ng-select.component.ts index d0823d896f..2392435738 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/editor-ng-select.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/editor-ng-select.component.ts @@ -26,7 +26,7 @@ import { Subject } from 'rxjs'; export class EditorNgSelectComponent { selectedId = ''; selectedItem: any; - collection?: any[]; // this will be filled by the collection of your column definition + collection: any[] = []; // this will be filled by the collection of your column definition onItemChanged = new Subject(); // object onChange(item: any) { diff --git a/frameworks/angular-slickgrid/src/demos/examples/example01.component.html b/frameworks/angular-slickgrid/src/demos/examples/example01.component.html index b8df6f7b1b..2d239f000d 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example01.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example01.component.html @@ -1,3 +1,12 @@ +

      Example 1: Basic Grids @@ -34,6 +43,7 @@

      [columns]="columnDefinitions1" [options]="gridOptions1" [dataset]="dataset1" + [containerClasses]="grid1ContainerClasses" (onAngularGridCreated)="angularGridReady1($event.detail)" > diff --git a/frameworks/angular-slickgrid/src/demos/examples/example01.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example01.component.ts index 1244ecb268..cf0d2dff5e 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example01.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example01.component.ts @@ -1,16 +1,17 @@ import { Component, type OnDestroy, type OnInit } from '@angular/core'; -import { AngularSlickgridModule, Formatters, type AngularGridInstance, type Column, type GridOption } from '../../library'; +import { AngularSlickgridComponent, Formatters, type AngularGridInstance, type Column, type GridOption } from '../../library'; import { zeroPadding } from './utilities'; const NB_ITEMS = 995; @Component({ templateUrl: './example01.component.html', - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example1Component implements OnDestroy, OnInit { private _darkModeGrid1 = false; angularGrid1!: AngularGridInstance; + grid1ContainerClasses = ['border-bottom-dotted', 'space-bottom']; columnDefinitions1: Column[] = []; columnDefinitions2: Column[] = []; gridOptions1!: GridOption; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example02.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example02.component.ts index 2de831974e..760674f53f 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example02.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example02.component.ts @@ -1,5 +1,12 @@ import { Component, type OnInit } from '@angular/core'; -import { AngularSlickgridModule, Formatters, type AngularGridInstance, type Column, type Formatter, type GridOption } from '../../library'; +import { + AngularSlickgridComponent, + Formatters, + type AngularGridInstance, + type Column, + type Formatter, + type GridOption, +} from '../../library'; interface DataItem { id: number; @@ -32,7 +39,7 @@ const customEnableButtonFormatter: Formatter = (_row: number, _cell: n @Component({ templateUrl: './example02.component.html', - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example2Component implements OnInit { angularGrid!: AngularGridInstance; @@ -126,7 +133,7 @@ export class Example2Component implements OnInit { minWidth: 100, formatter: customEnableButtonFormatter, onCellClick: (e, args) => { - this.toggleCompletedProperty(args && args.dataContext); + this.toggleCompletedProperty(args?.dataContext); }, }, ]; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example03.component.html b/frameworks/angular-slickgrid/src/demos/examples/example03.component.html index 1310090dfa..d0c3b60146 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example03.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example03.component.html @@ -100,8 +100,12 @@

      -
      Updated Item: {{ updatedObject | json }}
      -
      Updated Item: {{ alertWarning }}
      + @if (updatedObject) { +
      Updated Item: {{ updatedObject | json }}
      + } + @if (alertWarning) { +
      Updated Item: {{ alertWarning }}
      + }
      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example03.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example03.component.ts index 8deb1fb2df..70e9768efe 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example03.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example03.component.ts @@ -1,14 +1,13 @@ -import { JsonPipe, NgIf } from '@angular/common'; +import { JsonPipe } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { Component, type OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Subject } from 'rxjs'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Editors, Filters, Formatters, - OperatorType, SlickGlobalEditorLock, SortComparers, type AngularGridInstance, @@ -38,7 +37,7 @@ const URL_COUNTRY_NAMES = 'assets/data/country_names.json'; const myCustomTitleValidator: EditorValidator = (value: any, _args?: EditorArguments) => { // 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 translate = gridOptions.i18n; const columnEditor = args?.column?.editor; @@ -64,7 +63,7 @@ const taskFormatter: Formatter = (_row, _cell, value) => { }; @Component({ templateUrl: './example03.component.html', - imports: [AngularSlickgridModule, JsonPipe, NgIf], + imports: [AngularSlickgridComponent, JsonPipe], }) export class Example3Component implements OnInit { private _commandQueue: any = []; @@ -249,7 +248,7 @@ export class Example3Component implements OnInit { collectionFilterBy: { property: 'value', value: 0, - operator: OperatorType.notEqual, + operator: '!=', }, // you could also provide a collection override to filter/sort based on the item dataContext or whatever else // collectionOverride: (updatedCollection, args) => { @@ -470,7 +469,7 @@ export class Example3Component implements OnInit { separatorBetweenTextLabels: ' ', }, model: Filters.multipleSelect, - operator: OperatorType.inContains, + operator: 'IN_CONTAINS', }, }, ]; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example04.component.html b/frameworks/angular-slickgrid/src/demos/examples/example04.component.html index 060bf0890d..f9bf569628 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example04.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example04.component.html @@ -51,10 +51,12 @@


      - - Metrics: {{ metrics.startTime | date: "yyyy-MM-dd hh:mm aaaaa'm'" }} | {{ metrics.itemCount }} of - {{ metrics.totalItemCount }} items - + @if (metrics) { + + Metrics: {{ metrics.startTime | date: "yyyy-MM-dd hh:mm aaaaa'm'" }} | {{ metrics.itemCount }} of + {{ metrics.totalItemCount }} items + + }
      -
      - Backend Error: -
      + @if (errorStatus()) { +
      + Backend Error: +
      + }
      -
      - Status: {{ status.text }} - +
      + Status: {{ status().text }} +
      @@ -78,10 +80,12 @@

      Set Sorting Dynamically
      - - Metrics: {{ metrics.endTime | date: "yyyy-MM-dd hh:mm aaaaa'm'" }} | {{ metrics.executionTime }}ms | - {{ metrics.totalItemCount }} items - + @if (metrics()) { + + Metrics: {{ metrics()?.endTime | date: "yyyy-MM-dd hh:mm aaaaa'm'" }} | {{ metrics()?.executionTime }}ms | + {{ metrics()?.totalItemCount }} items + + }

      @@ -142,7 +146,7 @@

      gridId="grid5" [columns]="columnDefinitions" [options]="gridOptions" - [paginationOptions]="paginationOptions" + [paginationOptions]="paginationOptions()" [dataset]="dataset" (onAngularGridCreated)="angularGridReady($event.detail)" (onGridStateChanged)="gridStateChanged($event.detail)" diff --git a/frameworks/angular-slickgrid/src/demos/examples/example05.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example05.component.ts index d2400ef73e..ba6284dd1a 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example05.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example05.component.ts @@ -1,11 +1,10 @@ -import { DatePipe, NgIf } from '@angular/common'; +import { DatePipe } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { ChangeDetectorRef, Component, type OnInit } from '@angular/core'; +import { Component, signal, type OnInit } from '@angular/core'; import { GridOdataService, type OdataOption, type OdataServiceApi } from '@slickgrid-universal/odata'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Filters, - OperatorType, PaginationMetadata, type AngularGridInstance, type Column, @@ -22,7 +21,7 @@ const PERCENT_HTML_ESCAPED = '%25'; @Component({ templateUrl: './example05.component.html', - imports: [AngularSlickgridModule, DatePipe, NgIf], + imports: [AngularSlickgridComponent, DatePipe], }) export class Example5Component implements OnInit { angularGrid!: AngularGridInstance; @@ -30,23 +29,20 @@ export class Example5Component implements OnInit { gridOptions!: GridOption; dataset = []; hideSubTitle = false; - metrics!: Metrics; - paginationOptions!: Pagination; + metrics = signal(undefined); + paginationOptions = signal(undefined); isCountEnabled = true; isSelectEnabled = false; isExpandEnabled = false; odataVersion = 2; odataQuery = ''; - processing = true; - errorStatus = ''; + processing = signal(true); + errorStatus = signal(''); isPageErrorTest = false; - status = { text: 'processing...', class: 'alert alert-danger' }; + status = signal({ text: 'processing...', class: 'alert alert-danger' }); - constructor( - private readonly cd: ChangeDetectorRef, - private http: HttpClient - ) {} + constructor(private http: HttpClient) {} angularGridReady(angularGrid: AngularGridInstance) { this.angularGrid = angularGrid; @@ -103,13 +99,13 @@ export class Example5Component implements OnInit { 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], @@ -118,7 +114,7 @@ export class Example5Component implements OnInit { }, 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' }, @@ -132,7 +128,7 @@ export class Example5Component implements OnInit { 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) + "$'"; @@ -144,32 +140,32 @@ export class Example5Component implements OnInit { version: this.odataVersion, // defaults to 2, the query string is slightly different between OData 2 and 4 }, onError: (error: Error) => { - this.errorStatus = error.message; + this.errorStatus.set(error.message); this.displaySpinner(false, true); }, preProcess: () => { - this.errorStatus = ''; + this.errorStatus.set(''); this.displaySpinner(true); }, process: (query) => this.getCustomerApiCall(query), postProcess: (response) => { - this.metrics = response.metrics; + this.metrics.set(response.metrics); this.displaySpinner(false); this.getCustomerCallback(response); - this.cd.detectChanges(); }, } as OdataServiceApi, }; } displaySpinner(isProcessing: boolean, isError?: boolean) { - this.processing = isProcessing; + this.processing.set(isProcessing); if (isError) { - this.status = { text: 'ERROR!!!', class: 'alert alert-danger' }; + this.status.set({ text: 'ERROR!!!', class: 'alert alert-danger' }); } else { - this.status = isProcessing ? { text: 'loading', class: 'alert alert-warning' } : { text: 'finished', class: 'alert alert-success' }; + this.status.set( + isProcessing ? { text: 'loading', class: 'alert alert-warning' } : { text: 'finished', class: 'alert alert-success' } + ); } - this.cd.detectChanges(); } getCustomerCallback(data: any) { @@ -179,12 +175,12 @@ export class Example5Component implements OnInit { if (this.isCountEnabled) { totalItemCount = this.odataVersion === 4 ? data['@odata.count'] : data['d']['__count']; } - if (this.metrics) { - this.metrics.totalItemCount = totalItemCount; + if (this.metrics()) { + this.metrics.set({ ...this.metrics()!, totalItemCount }); } // once pagination totalItems is filled, we can update the dataset - this.paginationOptions = { ...this.gridOptions.pagination, totalItems: totalItemCount } as Pagination; + this.paginationOptions.set({ ...this.gridOptions.pagination, totalItems: totalItemCount } as Pagination); this.dataset = this.odataVersion === 4 ? data.value : data.d.results; this.odataQuery = data['query']; } @@ -206,7 +202,7 @@ export class Example5Component implements OnInit { setFiltersDynamically() { // we can Set Filters Dynamically (or different filters) afterward through the FilterService this.angularGrid.filterService.updateFilters([ - // { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal }, + // { columnId: 'gender', searchTerms: ['male'], operator: '=' }, { columnId: 'name', searchTerms: ['A'], operator: 'a*' }, ]); } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example06.component.html b/frameworks/angular-slickgrid/src/demos/examples/example06.component.html index 194e763a21..51c921f563 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example06.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example06.component.html @@ -48,9 +48,9 @@

      -
      - Status: {{ status.text }} - +
      + Status: {{ status().text }} +
      @@ -94,7 +94,7 @@

      Locale: - {{ selectedLanguage + '.json' }} + {{ selectedLanguage() + '.json' }}

      @@ -125,10 +125,12 @@


      -
      - Metrics: {{ metrics.endTime | date: "yyyy-MM-dd hh:mm aaaaa'm'" }} | {{ metrics.executionTime }}ms | - {{ metrics.totalItemCount }} items -
      + @if (metrics()) { +
      + Metrics: {{ metrics()?.endTime | date: "yyyy-MM-dd hh:mm aaaaa'm'" }} | {{ metrics()?.executionTime }}ms | + {{ metrics()?.totalItemCount }} items +
      + }
      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example06.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example06.component.ts index 3149963902..492526f45b 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example06.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example06.component.ts @@ -1,5 +1,5 @@ -import { DatePipe, NgIf } from '@angular/common'; -import { ChangeDetectorRef, Component, type OnDestroy, type OnInit } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { Component, signal, type OnDestroy, type OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { addDay, format as tempoFormat } from '@formkit/tempo'; import { TranslateService } from '@ngx-translate/core'; @@ -11,10 +11,9 @@ import { } from '@slickgrid-universal/graphql'; import type { Subscription } from 'rxjs'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Filters, Formatters, - OperatorType, unsubscribeAllObservables, type AngularGridInstance, type Column, @@ -32,7 +31,7 @@ const FAKE_SERVER_DELAY = 250; @Component({ templateUrl: './example06.component.html', - imports: [AngularSlickgridModule, DatePipe, FormsModule, NgIf], + imports: [AngularSlickgridComponent, DatePipe, FormsModule], }) export class Example6Component implements OnInit, OnDestroy { private subscriptions: Subscription[] = []; @@ -40,23 +39,20 @@ export class Example6Component implements OnInit, OnDestroy { columnDefinitions!: Column[]; gridOptions!: GridOption; dataset = []; - metrics!: Metrics; + metrics = signal(undefined); hideSubTitle = false; isWithCursor = false; graphqlQuery = ''; - processing = true; - status = { text: 'processing...', class: 'alert alert-danger' }; - selectedLanguage: string; + processing = signal(true); + status = signal({ text: 'processing...', class: 'alert alert-danger' }); + selectedLanguage = signal(''); serverWaitDelay = FAKE_SERVER_DELAY; // server simulation with default of 250ms but 50ms for Cypress tests - constructor( - private readonly cd: ChangeDetectorRef, - private translate: TranslateService - ) { + constructor(private translate: TranslateService) { // always start with English for Cypress E2E tests to be consistent const defaultLang = 'en'; this.translate.use(defaultLang); - this.selectedLanguage = defaultLang; + this.selectedLanguage.set(defaultLang); } ngOnDestroy() { @@ -180,7 +176,7 @@ export class Example6Component implements OnInit, OnDestroy { gridHeight: 200, gridWidth: 900, 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' } }, }, enableFiltering: true, @@ -225,13 +221,13 @@ export class Example6Component implements OnInit, OnDestroy { ], filters: [ // 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' }, ], sorters: [ // direction can written as 'asc' (uppercase or lowercase) and/or use the SortDirection type @@ -253,7 +249,7 @@ export class Example6Component implements OnInit, OnDestroy { }, ], filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => { - 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. @@ -272,9 +268,8 @@ export class Example6Component implements OnInit, OnDestroy { preProcess: () => this.displaySpinner(true), process: (query) => this.getCustomerApiCall(query), postProcess: (result: GraphqlPaginatedResult) => { - this.metrics = result.metrics as Metrics; + this.metrics.set(result.metrics as Metrics); this.displaySpinner(false); - this.cd.detectChanges(); }, } as GraphqlServiceApi, }; @@ -285,10 +280,10 @@ export class Example6Component implements OnInit, OnDestroy { } displaySpinner(isProcessing: boolean) { - this.processing = isProcessing; - this.status = isProcessing - ? { text: 'processing...', class: 'alert alert-danger' } - : { text: 'finished', class: 'alert alert-success' }; + this.processing.set(isProcessing); + this.status.set( + isProcessing ? { text: 'processing...', class: 'alert alert-danger' } : { text: 'finished', class: 'alert alert-success' } + ); } /** @@ -386,11 +381,11 @@ export class Example6Component implements OnInit, OnDestroy { // we can Set Filters Dynamically (or different filters) afterward through the FilterService this.angularGrid.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' }, ]); } @@ -409,13 +404,13 @@ export class Example6Component implements OnInit, OnDestroy { this.angularGrid.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' }, ]); this.angularGrid.sortService.updateSorting([ // direction can written as 'asc' (uppercase or lowercase) and/or use the SortDirection type @@ -444,10 +439,10 @@ export class Example6Component implements OnInit, OnDestroy { } switchLanguage() { - const nextLanguage = this.selectedLanguage === 'en' ? 'fr' : 'en'; + const nextLanguage = this.selectedLanguage() === 'en' ? 'fr' : 'en'; this.subscriptions.push( this.translate.use(nextLanguage).subscribe(() => { - this.selectedLanguage = nextLanguage; + this.selectedLanguage.set(nextLanguage); }) ); } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example07.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example07.component.ts index 700c9da886..1be3c80c1a 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example07.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example07.component.ts @@ -1,5 +1,5 @@ import { Component, ViewEncapsulation, type OnInit } from '@angular/core'; -import { AngularSlickgridModule, type AngularGridInstance, type Column, type GridOption } from '../../library'; +import { AngularSlickgridComponent, type AngularGridInstance, type Column, type GridOption } from '../../library'; // create a custom Formatter to highlight negative values in red let columns1WithHighlightingById: any = {}; @@ -9,7 +9,7 @@ let columns2WithHighlightingById: any = {}; styleUrls: ['./example07.component.scss'], encapsulation: ViewEncapsulation.None, templateUrl: './example07.component.html', - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example7Component implements OnInit { columnDefinitions1: Column[] = []; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example08.component.html b/frameworks/angular-slickgrid/src/demos/examples/example08.component.html index 5939d78684..5e4a15fca7 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example08.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example08.component.html @@ -45,7 +45,7 @@

      Locale: - {{ selectedLanguage + '.json' }} + {{ selectedLanguage() + '.json' }}
      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example08.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example08.component.ts index 6d9752ca11..1af78b3321 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example08.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example08.component.ts @@ -1,13 +1,19 @@ -import { Component, ViewEncapsulation, type OnDestroy, type OnInit } from '@angular/core'; +import { Component, signal, ViewEncapsulation, type OnDestroy, type OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import type { Subscription } from 'rxjs'; -import { AngularSlickgridModule, unsubscribeAllObservables, type AngularGridInstance, type Column, type GridOption } from '../../library'; +import { + AngularSlickgridComponent, + unsubscribeAllObservables, + type AngularGridInstance, + type Column, + type GridOption, +} from '../../library'; @Component({ templateUrl: './example08.component.html', styleUrls: ['./example08.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example8Component implements OnInit, OnDestroy { private subscriptions: Subscription[] = []; @@ -16,13 +22,13 @@ export class Example8Component implements OnInit, OnDestroy { gridOptions!: GridOption; dataset!: any[]; hideSubTitle = false; - selectedLanguage: string; + selectedLanguage = signal(''); constructor(private translate: TranslateService) { // always start with English for Cypress E2E tests to be consistent const defaultLang = 'en'; this.translate.use(defaultLang); - this.selectedLanguage = defaultLang; + this.selectedLanguage.set(defaultLang); } angularGridReady(angularGrid: AngularGridInstance) { @@ -191,10 +197,10 @@ export class Example8Component implements OnInit, OnDestroy { } switchLanguage() { - const nextLanguage = this.selectedLanguage === 'en' ? 'fr' : 'en'; + const nextLanguage = this.selectedLanguage() === 'en' ? 'fr' : 'en'; this.subscriptions.push( this.translate.use(nextLanguage).subscribe(() => { - this.selectedLanguage = nextLanguage; + this.selectedLanguage.set(nextLanguage); }) ); } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example09.component.html b/frameworks/angular-slickgrid/src/demos/examples/example09.component.html index 3e383ac8bb..d9eeae0ba8 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example09.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example09.component.html @@ -44,7 +44,7 @@

      Locale: - {{ selectedLanguage + '.json' }} + {{ selectedLanguage() + '.json' }}
      { - 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 @@

      (single select) Selected Row: - +

      @@ -74,21 +74,23 @@

      Pagination: - -
      - - -
      -
      + @if (isGrid2WithPagination) { + +
      + + +
      +
      + }
      (multi-select) Selected Row(s): - +

      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example10.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example10.component.ts index 46deb8d707..b64f9a12db 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example10.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example10.component.ts @@ -1,7 +1,6 @@ -import { NgIf } from '@angular/common'; -import { ChangeDetectorRef, Component, type OnInit } from '@angular/core'; +import { Component, signal, type OnInit } from '@angular/core'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Filters, Formatters, type AngularGridInstance, @@ -13,7 +12,7 @@ import { @Component({ templateUrl: './example10.component.html', styles: ['.alert { padding: 8px; margin-bottom: 10px }', '.col-sm-1{ max-width: 70px }'], - imports: [AngularSlickgridModule, NgIf], + imports: [AngularSlickgridComponent], }) export class Example10Component implements OnInit { angularGrid1!: AngularGridInstance; @@ -28,11 +27,9 @@ export class Example10Component implements OnInit { gridObj2!: any; hideSubTitle = false; isGrid2WithPagination = true; - selectedTitles = ''; - selectedTitle = ''; - selectedGrid2IDs!: number[]; - - constructor(private cd: ChangeDetectorRef) {} + selectedTitles = signal(''); + selectedTitle = signal(''); + selectedGrid2IDs = signal([]); ngOnInit(): void { this.prepareGrid(); @@ -160,7 +157,7 @@ export class Example10Component implements OnInit { gridWidth: 800, enableAutoResize: false, enableCellNavigation: true, - enableRowSelection: true, + enableSelection: true, enableCheckboxSelector: true, enableFiltering: true, checkboxSelector: { @@ -175,7 +172,7 @@ export class Example10Component implements OnInit { // selectableOverride: (row: number, dataContext: any, grid: SlickGrid) => (dataContext.id % 2 === 1) }, multiSelect: false, - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: true, }, @@ -208,12 +205,12 @@ export class Example10Component implements OnInit { hideInColumnTitleRow: true, applySelectOnAllPages: true, // when clicking "Select All", should we apply it to all pages (defaults to true) }, - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false, }, enableCheckboxSelector: true, - enableRowSelection: true, + enableSelection: true, enablePagination: true, pagination: { pageSizes: [5, 10, 15, 20, 25, 50, 75, 100], @@ -289,13 +286,13 @@ export class Example10Component implements OnInit { console.log('Grid State changed:: ', gridStateChanges.change); if (gridStateChanges!.gridState!.rowSelection) { - this.selectedGrid2IDs = (gridStateChanges!.gridState!.rowSelection.filteredDataContextIds || []) as number[]; - this.selectedGrid2IDs = this.selectedGrid2IDs.sort((a, b) => a - b); // sort by ID - this.selectedTitles = this.selectedGrid2IDs.map((dataContextId) => `Task ${dataContextId}`).join(','); - if (this.selectedTitles.length > 293) { - this.selectedTitles = this.selectedTitles.substring(0, 293) + '...'; + const ids = ((gridStateChanges!.gridState!.rowSelection.filteredDataContextIds || []) as number[]).sort((a, b) => a - b); + this.selectedGrid2IDs.set(ids); + let titles = ids.map((dataContextId) => `Task ${dataContextId}`).join(','); + if (titles.length > 293) { + titles = titles.substring(0, 293) + '...'; } - this.cd.detectChanges(); + this.selectedTitles.set(titles); } } @@ -309,10 +306,11 @@ export class Example10Component implements OnInit { handleSelectedRowsChanged1(e: Event, args: any) { if (Array.isArray(args.rows) && this.gridObj1) { - this.selectedTitle = args.rows.map((idx: number) => { + const title = args.rows.map((idx: number) => { const item = this.gridObj1.getDataItem(idx); return item.title || ''; }); + this.selectedTitle.set(title); } } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example11.component.html b/frameworks/angular-slickgrid/src/demos/examples/example11.component.html index 55b2468d08..bfa6da668e 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example11.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example11.component.html @@ -26,7 +26,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/frameworks/angular-slickgrid/src/demos/examples/example11.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example11.component.ts index 9c6c8fc63f..36d61bcc0c 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example11.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example11.component.ts @@ -1,6 +1,6 @@ import { Component, ViewEncapsulation, type OnInit } from '@angular/core'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Editors, Formatters, type AngularGridInstance, @@ -16,7 +16,7 @@ import { styles: ['.duration-bg { background-color: #e9d4f1 !important }'], encapsulation: ViewEncapsulation.None, templateUrl: './example11.component.html', - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example11Component implements OnInit { angularGrid!: AngularGridInstance; @@ -141,7 +141,7 @@ export class Example11Component implements OnInit { editable: true, enableColumnPicker: true, enableCellNavigation: true, - enableRowSelection: true, + enableSelection: true, }; } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example12.component.html b/frameworks/angular-slickgrid/src/demos/examples/example12.component.html index 27106eedf3..0c9238ea98 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example12.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example12.component.html @@ -65,7 +65,7 @@

      Switch Language - Locale: {{ selectedLanguage + '.json' }} + Locale: {{ selectedLanguage() + '.json' }}

    • -

      Grid 1 (with Header Grouping & Colspan)

      - +

      + Grid 1 (with Header Grouping & Colspan) + +

      + + +
      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example14.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example14.component.ts index 69f39b0ae5..eadff553dc 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example14.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example14.component.ts @@ -1,14 +1,15 @@ import { Component, type OnInit } from '@angular/core'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { PdfExportService } from '@slickgrid-universal/pdf-export'; -import { AngularSlickgridModule, type AngularGridInstance, type Column, type GridOption, type ItemMetadata } from '../../library'; +import { AngularSlickgridComponent, type AngularGridInstance, type Column, type GridOption, type ItemMetadata } from '../../library'; @Component({ templateUrl: './example14.component.html', styleUrls: ['./example14.component.scss'], - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example14Component implements OnInit { + angularGrid1!: AngularGridInstance; angularGrid2!: AngularGridInstance; gridObj2: any; columnDefinitions1!: Column[]; @@ -18,12 +19,17 @@ export class Example14Component implements OnInit { dataset1: any[] = []; dataset2: any[] = []; hideSubTitle = false; + isColspanSpreading = false; ngOnInit(): void { this.prepareGrid1(); this.prepareGrid2(); } + angularGridReady1(angularGrid: AngularGridInstance) { + this.angularGrid1 = angularGrid; + } + angularGridReady2(angularGrid: AngularGridInstance) { this.angularGrid2 = angularGrid; this.gridObj2 = angularGrid.slickGrid; @@ -63,6 +69,7 @@ export class Example14Component implements OnInit { exportWithFormatter: false, }, externalResources: [new ExcelExportService(), new PdfExportService()], + spreadHiddenColspan: this.isColspanSpreading, }; this.dataset1 = this.getData(500); @@ -157,6 +164,13 @@ export class Example14Component implements OnInit { }; } + spreadColspan() { + this.isColspanSpreading = !this.isColspanSpreading; + this.angularGrid1.slickGrid?.setOptions({ spreadHiddenColspan: this.isColspanSpreading }); + this.angularGrid1.slickGrid?.resetActiveCell(); + this.angularGrid1.slickGrid?.invalidate(); + } + toggleSubTitle() { this.hideSubTitle = !this.hideSubTitle; const action = this.hideSubTitle ? 'add' : 'remove'; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example15.component.html b/frameworks/angular-slickgrid/src/demos/examples/example15.component.html index d83593fa57..6d458bfb1f 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example15.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example15.component.html @@ -44,7 +44,7 @@

      Locale: - {{ selectedLanguage + '.json' }} + {{ selectedLanguage() + '.json' }} { - this.selectedLanguage = nextLanguage; + this.selectedLanguage.set(nextLanguage); }) ); } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example16.component.html b/frameworks/angular-slickgrid/src/demos/examples/example16.component.html index 0bd23fc5e3..a15409f5b3 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example16.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example16.component.html @@ -72,7 +72,7 @@

      gridId="grid16" [(columns)]="columnDefinitions" [options]="gridOptions" - [dataset]="dataset" + [dataset]="dataset()" (onAngularGridCreated)="angularGridReady($event.detail)" > diff --git a/frameworks/angular-slickgrid/src/demos/examples/example16.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example16.component.ts index a5ce836a9a..56e29c8383 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example16.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example16.component.ts @@ -1,7 +1,6 @@ -import { Component, type OnInit } from '@angular/core'; +import { Component, signal, type OnInit } from '@angular/core'; import { - AngularSlickgridModule, - ExtensionName, + AngularSlickgridComponent, Filters, Formatters, type AngularGridInstance, @@ -12,23 +11,19 @@ import { @Component({ templateUrl: './example16.component.html', - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example16Component implements OnInit { angularGrid!: AngularGridInstance; columnDefinitions!: Column[]; gridOptions!: GridOption; - dataset!: any[]; + dataset = signal([]); hideSubTitle = false; angularGridReady(angularGrid: AngularGridInstance) { this.angularGrid = angularGrid; } - get rowMoveInstance() { - return this.angularGrid?.extensionService?.getExtensionInstanceByName(ExtensionName.rowMoveManager) ?? {}; - } - ngOnInit(): void { this.columnDefinitions = [ { id: 'title', name: 'Title', field: 'title', filterable: true }, @@ -84,8 +79,8 @@ export class Example16Component implements OnInit { hideInFilterHeaderRow: false, hideInColumnTitleRow: true, }, - enableRowSelection: true, - rowSelectionOptions: { + enableSelection: true, + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false, }, @@ -100,7 +95,7 @@ export class Example16Component implements OnInit { cancelEditOnDrag: true, hideRowMoveShadow: false, width: 30, - onBeforeMoveRows: this.onBeforeMoveRow.bind(this), + onBeforeMoveRows: this.onBeforeMoveRows.bind(this), onMoveRows: this.onMoveRows.bind(this), // you can change the move icon position of any extension (RowMove, RowDetail or RowSelector icon) @@ -140,10 +135,10 @@ export class Example16Component implements OnInit { effortDriven: i % 5 === 0, }; } - this.dataset = mockDataset; + this.dataset.set(mockDataset); } - onBeforeMoveRow(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { + onBeforeMoveRows(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { for (const rowIdx of data.rows) { // no point in moving before or after itself if ( @@ -208,7 +203,8 @@ export class Example16Component implements OnInit { // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order const finalDataset = left.concat(extractedRows.concat(right)); - this.dataset = finalDataset; // update dataset and re-render the grid + this.angularGrid.slickGrid?.invalidate(); + this.dataset.set(finalDataset); // assign new array reference to trigger Angular input change detection } hideDurationColumnDynamically() { @@ -246,7 +242,7 @@ export class Example16Component implements OnInit { params: { iconCssClass: 'mdi mdi-pencil pointer' }, minWidth: 30, maxWidth: 30, - onCellClick: (clickEvent: Event, args: OnEventArgs) => { + onCellClick: (_clickEvent: Event, args: OnEventArgs) => { alert(`Technically we should Edit "Task ${args.dataContext.id}"`); }, }, @@ -260,7 +256,7 @@ export class Example16Component implements OnInit { params: { iconCssClass: 'mdi mdi-trash-can pointer' }, minWidth: 30, maxWidth: 30, - onCellClick: (e: Event, args: OnEventArgs) => { + onCellClick: (_e: Event, args: OnEventArgs) => { if (confirm('Are you sure?')) { this.angularGrid.gridService.deleteItemById(args.dataContext.id); } @@ -268,8 +264,12 @@ export class Example16Component implements OnInit { }, ]; - this.columnDefinitions.splice(0, 0, newCols[0], newCols[1]); - this.columnDefinitions = this.columnDefinitions.slice(); // or use spread operator [...cols] to trigger change + // NOTE if you use an Extensions (Checkbox Selector, Row Detail, ...) that modifies the column definitions in any way + // you MUST use "getAllColumnDefinitions()" from the GridService, using this will be ALL columns including the 1st column that is created internally + // for example if you use the Checkbox Selector (row selection), you MUST use the code below + const allColumns = this.angularGrid.gridService.getAllColumnDefinitions(); + allColumns.unshift(newCols[0], newCols[1]); + this.columnDefinitions = [...allColumns]; // (or use slice) reassign to column definitions for Aurelia to do dirty checking } } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example17.component.html b/frameworks/angular-slickgrid/src/demos/examples/example17.component.html index 49e405c08b..01ba8ff170 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example17.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example17.component.html @@ -45,13 +45,14 @@


      - - + @if (gridCreated) { + + + }

      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example17.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example17.component.ts index b95ab2f95e..0188aeae49 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example17.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example17.component.ts @@ -1,8 +1,7 @@ -import { NgIf } from '@angular/common'; -import { ChangeDetectorRef, Component, ViewEncapsulation } from '@angular/core'; +import { Component, ViewEncapsulation } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; -import { AngularSlickgridModule, toCamelCase, type AngularGridInstance, type Column, type GridOption } from '../../library'; +import { AngularSlickgridComponent, toCamelCase, type AngularGridInstance, type Column, type GridOption } from '../../library'; const sampleDataRoot = 'assets/data'; @@ -10,7 +9,7 @@ const sampleDataRoot = 'assets/data'; styles: ['.file-upload { max-width: 300px; }'], encapsulation: ViewEncapsulation.None, templateUrl: './example17.component.html', - imports: [AngularSlickgridModule, FormsModule, NgIf], + imports: [AngularSlickgridComponent, FormsModule], }) export class Example17Component { angularGrid!: AngularGridInstance; @@ -22,8 +21,6 @@ export class Example17Component { uploadFileRef = ''; templateUrl = `${sampleDataRoot}/users.csv`; - constructor(private readonly cd: ChangeDetectorRef) {} - angularGridReady(angularGrid: AngularGridInstance) { this.angularGrid = angularGrid; } @@ -53,7 +50,6 @@ export class Example17Component { dynamicallyCreateGrid(csvContent: string) { // dispose of any previous grid before creating a new one this.gridCreated = false; - this.cd.detectChanges(); const dataRows = csvContent?.split('\n'); const colDefs: Column[] = []; @@ -105,7 +101,6 @@ export class Example17Component { this.dataset = outputData; this.columnDefinitions = colDefs; this.gridCreated = true; - this.cd.detectChanges(); } toggleSubTitle() { diff --git a/frameworks/angular-slickgrid/src/demos/examples/example18.component.html b/frameworks/angular-slickgrid/src/demos/examples/example18.component.html index cdac6420f0..78c509a6df 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example18.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example18.component.html @@ -104,20 +104,21 @@

      -
      - -
      + @for (groupField of selectedGroupingFields(); track selectTrackByFn(i, groupField); let i = $index) { +
      + +
      + }

      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example18.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example18.component.ts index 6a49a6dabb..ea89a719bb 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example18.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example18.component.ts @@ -1,13 +1,11 @@ -import { NgFor } from '@angular/common'; -import { ChangeDetectorRef, Component, type AfterViewInit, type OnDestroy, type OnInit } from '@angular/core'; +import { Component, signal, type OnDestroy, type OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { PdfExportService } from '@slickgrid-universal/pdf-export'; import { TextExportService } from '@slickgrid-universal/text-export'; import { Aggregators, - AngularSlickgridModule, - DelimiterType, + AngularSlickgridComponent, Editors, Filters, Formatters, @@ -25,9 +23,9 @@ const NB_ITEMS = 10_000; @Component({ templateUrl: './example18.component.html', - imports: [AngularSlickgridModule, FormsModule, NgFor], + imports: [AngularSlickgridComponent, FormsModule], }) -export class Example18Component implements AfterViewInit, OnInit, OnDestroy { +export class Example18Component implements OnInit, OnDestroy { private _darkMode = false; angularGrid!: AngularGridInstance; columnDefinitions!: Column[]; @@ -39,12 +37,12 @@ export class Example18Component implements AfterViewInit, OnInit, OnDestroy { gridOptions!: GridOption; hideSubTitle = false; processing = false; - selectedGroupingFields: Array = ['', '', '']; + selectedGroupingFields = signal>(['', '', '']); excelExportService = new ExcelExportService(); pdfExportService = new PdfExportService(); textExportService = new TextExportService(); - constructor(private changeDetectorRef: ChangeDetectorRef) { + constructor() { // define the grid options & columns and then create the grid itself this.loadData(NB_ITEMS); this.defineGrid(); @@ -56,12 +54,11 @@ export class Example18Component implements AfterViewInit, OnInit, OnDestroy { } ngOnDestroy() { - document.querySelector('.panel-wm-content')!.classList.remove('dark-mode'); - document.querySelector('#demo-container')!.dataset.bsTheme = 'light'; - } - - ngAfterViewInit() { - this.changeDetectorRef.detectChanges(); + document.querySelector('.panel-wm-content')?.classList.remove('dark-mode'); + const demoContainer = document.querySelector('#demo-container'); + if (demoContainer?.dataset) { + demoContainer.dataset.bsTheme = 'light'; + } } angularGridReady(angularGrid: AngularGridInstance) { @@ -283,11 +280,12 @@ export class Example18Component implements AfterViewInit, OnInit, OnDestroy { initialGroupBy: ['duration'], }, darkMode: this._darkMode, - enableTextExport: true, - enableExcelExport: true, excelExportOptions: { 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', @@ -336,7 +334,7 @@ export class Example18Component implements AfterViewInit, OnInit, OnDestroy { } clearGroupingSelects() { - this.selectedGroupingFields.forEach((g, i) => (this.selectedGroupingFields[i] = '')); + this.selectedGroupingFields.set(['', '', '']); } collapseAllGroups() { @@ -362,7 +360,7 @@ export class Example18Component implements AfterViewInit, OnInit, OnDestroy { exportToCsv(type = 'csv') { this.textExportService.exportToFile({ - delimiter: type === 'csv' ? DelimiterType.comma : DelimiterType.tab, + delimiter: type === 'csv' ? ',' : '\t', filename: 'myExport', format: type === 'csv' ? 'csv' : 'txt', }); @@ -394,9 +392,9 @@ export class Example18Component implements AfterViewInit, OnInit, OnDestroy { groupByFieldName(_fieldName: string, _index: number) { this.clearGrouping(); - if (this.draggableGroupingPlugin && this.draggableGroupingPlugin.setDroppedGroups) { + if (this.draggableGroupingPlugin?.setDroppedGroups) { // get the field names from Group By select(s) dropdown, but filter out any empty fields - const groupedFields = this.selectedGroupingFields.filter((g) => g !== ''); + const groupedFields = this.selectedGroupingFields().filter((g) => g !== ''); this.showPreHeader(); this.draggableGroupingPlugin.setDroppedGroups(groupedFields); @@ -405,13 +403,14 @@ export class Example18Component implements AfterViewInit, OnInit, OnDestroy { } onGroupChanged(change: { caller?: string; groupColumns: Grouping[] }) { - // the "caller" property might not be in the SlickGrid core lib yet, reference PR https://github.com/6pac/SlickGrid/pull/303 - const caller = (change && change.caller) || []; - const groups = (change && change.groupColumns) || []; - - if (Array.isArray(this.selectedGroupingFields) && Array.isArray(groups) && groups.length > 0) { - // update all Group By select dropdown - this.selectedGroupingFields.forEach((g, i) => (this.selectedGroupingFields[i] = (groups[i] && groups[i].getter) || '')); + const caller = change?.caller || []; + const groups = change?.groupColumns || []; + + if (Array.isArray(groups) && groups.length > 0) { + // assign a new array reference to trigger Angular change detection + const newFields = groups.map((g) => g?.getter ?? ''); + while (newFields.length < 3) newFields.push(''); + this.selectedGroupingFields.set(newFields); } else if (groups.length === 0 && caller === 'remove-group') { this.clearGroupingSelects(); } @@ -422,6 +421,14 @@ export class Example18Component implements AfterViewInit, OnInit, OnDestroy { this.angularGrid.dataView?.refresh(); } + // helper for updating a single dropdown value in the signal array + onDropdownChange(value: string, index: number) { + const arr = [...this.selectedGroupingFields()]; + arr[index] = value; + this.selectedGroupingFields.set(arr); + this.groupByFieldName(value, index); + } + showPreHeader() { this.gridObj.setPreHeaderPanelVisibility(true); } @@ -457,19 +464,27 @@ export class Example18Component implements AfterViewInit, OnInit, OnDestroy { } toggleBodyBackground() { + const panelContentElm = document.querySelector('.panel-wm-content'); + const demoContainer = document.querySelector('#demo-container'); + let theme = 'light'; + if (this._darkMode) { - document.querySelector('.panel-wm-content')!.classList.add('dark-mode'); - document.querySelector('#demo-container')!.dataset.bsTheme = 'dark'; + panelContentElm?.classList.add('dark-mode'); + theme = 'dark'; } else { - document.querySelector('.panel-wm-content')!.classList.remove('dark-mode'); - document.querySelector('#demo-container')!.dataset.bsTheme = 'light'; + panelContentElm?.classList.remove('dark-mode'); + theme = 'light'; + } + + if (demoContainer?.dataset) { + demoContainer.dataset.bsTheme = theme; } } toggleSubTitle() { this.hideSubTitle = !this.hideSubTitle; const action = this.hideSubTitle ? 'add' : 'remove'; - document.querySelector('.subtitle')?.classList[action]('hidden'); - this.angularGrid.resizerService.resizeGrid(0); + document.querySelector('.subtitle')?.classList?.[action]('hidden'); + this.angularGrid.resizerService?.resizeGrid(0); } } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example19.component.html b/frameworks/angular-slickgrid/src/demos/examples/example19.component.html index 94e4aee80f..766c01cdf8 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example19.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example19.component.html @@ -62,9 +62,11 @@

      />

      -
      - {{ message }} -
      + @if (message) { +
      + {{ message }} +
      + }

      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example19.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example19.component.ts index 7599ae7958..8c37f9ce18 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example19.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example19.component.ts @@ -1,15 +1,14 @@ -import { NgIf } from '@angular/common'; import { Component, type OnDestroy, type OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { AngularSlickRowDetailView } from '@slickgrid-universal/angular-row-detail-plugin'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Editors, Filters, Formatters, type AngularGridInstance, type Column, type GridOption, - type SlickRowDetailView, } from '../../library'; import { Example19RowDetailComponent } from './example19-rowdetail.component'; import { RowDetailPreloadComponent } from './rowdetail-preload.component'; @@ -19,7 +18,7 @@ const NB_ITEMS = 1000; @Component({ templateUrl: './example19.component.html', - imports: [AngularSlickgridModule, FormsModule, NgIf], + imports: [AngularSlickgridComponent, FormsModule], }) export class Example19Component implements OnDestroy, OnInit { private _darkMode = false; @@ -37,14 +36,14 @@ export class Example19Component implements OnDestroy, OnInit { this.angularGrid = angularGrid; } - get rowDetailInstance(): SlickRowDetailView { + get rowDetailInstance(): AngularSlickRowDetailView { // you can get the SlickGrid RowDetail plugin (addon) instance via 2 ways // option 1 return this.angularGrid.extensions.rowDetailView?.instance || {}; // OR option 2 - // return this.angularGrid?.extensionService.getExtensionInstanceByName(ExtensionName.rowDetailView) || {}; + // return this.angularGrid?.extensionService.getExtensionInstanceByName('rowDetailView') || {}; } ngOnInit(): void { @@ -145,6 +144,7 @@ export class Example19Component implements OnDestroy, OnInit { 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: [AngularSlickRowDetailView], rowDetailView: { // optionally change the column index position of the icon (defaults to 0) // columnIndexPosition: 1, @@ -192,14 +192,14 @@ export class Example19Component implements OnDestroy, OnInit { return true; }, }, - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: true, }, // 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/frameworks/angular-slickgrid/src/demos/examples/example20.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example20.component.ts index da3a86889b..d4baeba09b 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example20.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example20.component.ts @@ -1,7 +1,7 @@ import { Component, ViewEncapsulation, type OnDestroy, type OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Editors, Filters, formatNumber, @@ -17,7 +17,7 @@ import { templateUrl: './example20.component.html', styleUrls: ['./example20.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [AngularSlickgridModule, FormsModule], + imports: [AngularSlickgridComponent, FormsModule], }) export class Example20Component implements OnInit, OnDestroy { angularGrid!: AngularGridInstance; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example21.component.html b/frameworks/angular-slickgrid/src/demos/examples/example21.component.html index a6ba6170e8..3aa7dd693c 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example21.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example21.component.html @@ -39,7 +39,9 @@

      [(ngModel)]="selectedColumn" (ngModelChange)="updateFilter()" > - + @for (field of columnDefinitions; track field) { + + }

      @@ -50,7 +52,9 @@

      [(ngModel)]="selectedOperator" (ngModelChange)="updateFilter()" > - + @for (operator of operatorList; track operator) { + + }

      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example21.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example21.component.ts index fcdd073086..4a79c5135f 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example21.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example21.component.ts @@ -1,13 +1,12 @@ -import { NgFor } from '@angular/common'; import { Component, ViewEncapsulation, type OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Formatters, type AngularGridInstance, type Column, type GridOption, - type OperatorString, + type OperatorType, type SlickDataView, type SlickGrid, } from '../../library'; @@ -16,7 +15,7 @@ import { encapsulation: ViewEncapsulation.None, styleUrls: ['./example21.component.scss'], templateUrl: './example21.component.html', - imports: [AngularSlickgridModule, FormsModule, NgFor], + imports: [AngularSlickgridComponent, FormsModule], }) export class Example21Component implements OnInit { angularGrid!: AngularGridInstance; @@ -26,7 +25,7 @@ export class Example21Component implements OnInit { gridOptions!: GridOption; dataset!: any[]; hideSubTitle = false; - operatorList: OperatorString[] = ['=', '<', '<=', '>', '>=', '<>', 'StartsWith', 'EndsWith']; + operatorList: OperatorType[] = ['=', '<', '<=', '>', '>=', '<>', 'StartsWith', 'EndsWith']; selectedOperator = '='; searchValue = ''; selectedColumn?: Column; @@ -108,7 +107,7 @@ export class Example21Component implements OnInit { alwaysShowVerticalScroll: false, enableColumnPicker: true, enableCellNavigation: true, - enableRowSelection: true, + enableSelection: true, }; // mock a dataset @@ -145,7 +144,7 @@ export class Example21Component implements OnInit { updateFilter() { this.angularGrid.filterService.updateSingleFilter({ columnId: `${this.selectedColumn!.id || ''}`, - operator: this.selectedOperator as OperatorString, + operator: this.selectedOperator as OperatorType, searchTerms: [this.searchValue || ''], }); } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example22.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example22.component.ts index 923de3faff..d7c9cc9748 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example22.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example22.component.ts @@ -1,13 +1,13 @@ import { HttpClient } from '@angular/common/http'; import { Component, type OnInit } from '@angular/core'; import { TabDirective, TabsetComponent } from 'ngx-bootstrap/tabs'; -import { AngularSlickgridModule, Filters, type AngularGridInstance, type Column, type GridOption } from '../../library'; +import { AngularSlickgridComponent, Filters, type AngularGridInstance, type Column, type GridOption } from '../../library'; const URL_CUSTOMERS = 'assets/data/customers_100.json'; @Component({ templateUrl: './example22.component.html', - imports: [TabsetComponent, TabDirective, AngularSlickgridModule], + imports: [TabsetComponent, TabDirective, AngularSlickgridComponent], }) export class Example22Component implements OnInit { angularGrid2!: AngularGridInstance; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example23.component.html b/frameworks/angular-slickgrid/src/demos/examples/example23.component.html index 9eb9054be3..d97abbecea 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example23.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example23.component.html @@ -51,10 +51,12 @@


      - - Metrics: {{ metrics.startTime | date: "yyyy-MM-dd hh:mm aaaaa'm'" }} | {{ metrics.itemCount }} of - {{ metrics.totalItemCount }} items - + @if (metrics) { + + Metrics: {{ metrics.startTime | date: "yyyy-MM-dd hh:mm aaaaa'm'" }} | {{ metrics.itemCount }} of + {{ metrics.totalItemCount }} items + + }
      @@ -86,7 +88,9 @@

      data-test="select-dynamic-filter" (ngModelChange)="usePredefinedFilter($event)" > - + @for (filter of filterList; track filter) { + + }

      @@ -97,7 +101,7 @@

      Switch Language - Locale: {{ selectedLanguage + '.json' }} + Locale: {{ selectedLanguage() + '.json' }} diff --git a/frameworks/angular-slickgrid/src/demos/examples/example23.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example23.component.ts index be6a5103bc..56f0d7e98c 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example23.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example23.component.ts @@ -1,5 +1,5 @@ -import { DatePipe, NgFor, NgIf } from '@angular/common'; -import { Component, type OnDestroy, type OnInit } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { Component, signal, type OnDestroy, type OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { addDay, format } from '@formkit/tempo'; import { TranslateService } from '@ngx-translate/core'; @@ -7,10 +7,9 @@ import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import type { Subscription } from 'rxjs'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Filters, Formatters, - OperatorType, unsubscribeAllObservables, type AngularGridInstance, type Column, @@ -39,7 +38,7 @@ const taskTranslateFormatter: Formatter = (row, cell, value, columnDef, dataCont @Component({ templateUrl: './example23.component.html', - imports: [NgIf, FormsModule, NgFor, AngularSlickgridModule, DatePipe], + imports: [AngularSlickgridComponent, DatePipe, FormsModule], }) export class Example23Component implements OnInit, OnDestroy { private subscriptions: Subscription[] = []; @@ -48,7 +47,7 @@ export class Example23Component implements OnInit, OnDestroy { gridOptions!: GridOption; dataset!: any[]; hideSubTitle = false; - selectedLanguage: string; + selectedLanguage = signal(''); metrics!: Metrics; filterList = [ { value: '', label: '' }, @@ -61,7 +60,7 @@ export class Example23Component implements OnInit, OnDestroy { // always start with English for Cypress E2E tests to be consistent const defaultLang = 'en'; this.translate.use(defaultLang); - this.selectedLanguage = defaultLang; + this.selectedLanguage.set(defaultLang); } ngOnDestroy() { @@ -108,7 +107,7 @@ export class Example23Component implements OnInit, OnDestroy { 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, @@ -156,7 +155,7 @@ export class Example23Component implements OnInit, OnDestroy { filterable: true, filter: { model: Filters.input, - operator: OperatorType.rangeExclusive, // defaults to inclusive + operator: 'RangeExclusive', // defaults to inclusive }, }, { @@ -181,9 +180,8 @@ export class Example23Component implements OnInit, OnDestroy { }, ]; - const today = new Date(); const presetLowestDay = format(addDay(new Date(), -2), 'YYYY-MM-DD'); - const presetHighestDay = format(addDay(new Date(), today.getDate() < 14 ? 28 : 25), 'YYYY-MM-DD'); + const presetHighestDay = format(addDay(new Date(), 25), 'YYYY-MM-DD'); this.gridOptions = { autoResize: { @@ -267,11 +265,11 @@ export class Example23Component implements OnInit, OnDestroy { } 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, }; }); @@ -305,8 +303,8 @@ export class Example23Component implements OnInit, OnDestroy { switch (filterValue) { 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': @@ -317,10 +315,10 @@ export class Example23Component implements OnInit, OnDestroy { } switchLanguage() { - const nextLanguage = this.selectedLanguage === 'en' ? 'fr' : 'en'; + const nextLanguage = this.selectedLanguage() === 'en' ? 'fr' : 'en'; this.subscriptions.push( this.translate.use(nextLanguage).subscribe(() => { - this.selectedLanguage = nextLanguage; + this.selectedLanguage.set(nextLanguage); }) ); } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example24.component.html b/frameworks/angular-slickgrid/src/demos/examples/example24.component.html index 34c858bd72..62a5905f20 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example24.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example24.component.html @@ -106,7 +106,7 @@

      Locale: - {{ selectedLanguage + '.json' }} + {{ selectedLanguage() + '.json' }} diff --git a/frameworks/angular-slickgrid/src/demos/examples/example24.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example24.component.ts index d47ee06c9b..df7c922056 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example24.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example24.component.ts @@ -1,10 +1,9 @@ -import { Component, ViewEncapsulation, type OnDestroy, type OnInit } from '@angular/core'; +import { Component, signal, ViewEncapsulation, type OnDestroy, type OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import type { Subscription } from 'rxjs'; import { - AngularSlickgridModule, - ExtensionName, + AngularSlickgridComponent, Filters, Formatters, unsubscribeAllObservables, @@ -61,7 +60,7 @@ const taskTranslateFormatter: Formatter = (row, cell, value, columnDef, dataCont templateUrl: './example24.component.html', styleUrls: ['./example24.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example24Component implements OnInit, OnDestroy { private _darkModeGrid = false; @@ -71,13 +70,13 @@ export class Example24Component implements OnInit, OnDestroy { gridOptions!: GridOption; dataset!: any[]; hideSubTitle = false; - selectedLanguage: string; + selectedLanguage = signal(''); constructor(private translate: TranslateService) { // always start with English for Cypress E2E tests to be consistent const defaultLang = 'en'; this.translate.use(defaultLang); - this.selectedLanguage = defaultLang; + this.selectedLanguage.set(defaultLang); } angularGridReady(angularGrid: AngularGridInstance) { @@ -85,11 +84,11 @@ export class Example24Component implements OnInit, OnDestroy { } get cellMenuInstance() { - return this.angularGrid?.extensionService?.getExtensionInstanceByName(ExtensionName.cellMenu); + return this.angularGrid?.extensionService?.getExtensionInstanceByName('cellMenu'); } get contextMenuInstance() { - return this.angularGrid?.extensionService?.getExtensionInstanceByName(ExtensionName.contextMenu); + return this.angularGrid?.extensionService?.getExtensionInstanceByName('contextMenu'); } ngOnInit() { @@ -359,7 +358,7 @@ export class Example24Component implements OnInit, OnDestroy { 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 && 'completed' in dataContext) { dataContext.completed = args.item.option; this.angularGrid.gridService.updateItem(dataContext); @@ -438,7 +437,7 @@ export class Example24Component implements OnInit, OnDestroy { // 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 @@ -469,7 +468,7 @@ export class Example24Component implements OnInit, OnDestroy { }, // 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; }, }, @@ -535,7 +534,7 @@ export class Example24Component implements OnInit, OnDestroy { 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 @@ -557,7 +556,7 @@ export class Example24Component implements OnInit, OnDestroy { disabled: true, // only shown when the task is Not Completed itemVisibilityOverride: (args) => { - const dataContext = args && args.dataContext; + const dataContext = args?.dataContext; return !dataContext.completed; }, }, @@ -588,7 +587,7 @@ export class Example24Component implements OnInit, OnDestroy { // 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 ('priority' in dataContext) { dataContext.priority = args.item.option; this.angularGrid.gridService.updateItem(dataContext); @@ -619,11 +618,10 @@ export class Example24Component implements OnInit, OnDestroy { } switchLanguage() { - const nextLanguage = this.selectedLanguage === 'en' ? 'fr' : 'en'; - + const nextLanguage = this.selectedLanguage() === 'en' ? 'fr' : 'en'; this.subscriptions.push( this.translate.use(nextLanguage).subscribe(() => { - this.selectedLanguage = nextLanguage; + this.selectedLanguage.set(nextLanguage); }) ); } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example25.component.html b/frameworks/angular-slickgrid/src/demos/examples/example25.component.html index 37776c3ae1..1d1efcd71b 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example25.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example25.component.html @@ -51,9 +51,9 @@

      -
      - Status: {{ status.text }} - +
      + Status: {{ status()?.text }} +
      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example25.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example25.component.ts index 8c087e75af..0f5d1f362a 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example25.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example25.component.ts @@ -1,12 +1,11 @@ import { HttpClient } from '@angular/common/http'; -import { Component, ViewEncapsulation, type OnInit } from '@angular/core'; +import { Component, signal, ViewEncapsulation, type OnInit } from '@angular/core'; import { GraphqlService, type GraphqlResult, type GraphqlServiceApi } from '@slickgrid-universal/graphql'; import type { Observable } from 'rxjs'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Filters, Formatters, - OperatorType, type AngularGridInstance, type Column, type GridOption, @@ -34,7 +33,7 @@ export interface Country { templateUrl: './example25.component.html', styleUrls: ['./example25.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example25Component implements OnInit { angularGrid!: AngularGridInstance; @@ -42,12 +41,11 @@ export class Example25Component implements OnInit { gridOptions!: GridOption; dataset = []; hideSubTitle = false; - metrics!: Metrics; - + metrics = signal(undefined); graphqlQuery = ''; - processing = true; - status = { text: 'processing...', class: 'alert alert-danger' }; - isDataLoaded = false; + processing = signal(true); + status = signal({ text: 'processing...', class: 'alert alert-danger' }); + isDataLoaded = signal(false); constructor(private http: HttpClient) {} @@ -90,7 +88,7 @@ export class Example25Component implements OnInit { 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 +116,7 @@ export class Example25Component implements OnInit { 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 @@ -205,22 +203,22 @@ export class Example25Component implements OnInit { datasetName: 'countries', // the only REQUIRED property }, // you can define the onInit callback OR enable the "executeProcessCommandOnInit" flag in the service init - preProcess: () => (!this.isDataLoaded ? this.displaySpinner(true) : ''), + preProcess: () => (!this.isDataLoaded() ? this.displaySpinner(true) : ''), process: (query: string) => this.getCountries(query), postProcess: (result: GraphqlResult) => { - this.metrics = result.metrics as Metrics; + this.metrics.set(result.metrics as Metrics); this.displaySpinner(false); - this.isDataLoaded = true; + this.isDataLoaded.set(true); }, } as GraphqlServiceApi, }; } displaySpinner(isProcessing: boolean) { - this.processing = isProcessing; - this.status = isProcessing - ? { text: 'processing...', class: 'alert alert-danger' } - : { text: 'finished', class: 'alert alert-success' }; + this.processing.set(isProcessing); + this.status.set( + isProcessing ? { text: 'processing...', class: 'alert alert-danger' } : { text: 'finished', class: 'alert alert-success' } + ); } // -- diff --git a/frameworks/angular-slickgrid/src/demos/examples/example26.component.html b/frameworks/angular-slickgrid/src/demos/examples/example26.component.html index 195c2c9b20..bc7561b2fa 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example26.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example26.component.html @@ -82,8 +82,12 @@

      Filters, Editors, AsyncPostRender with Angular Components

      -
      Updated Item: {{ updatedObject | json }}
      -
      Updated Item: {{ alertWarning }}
      + @if (updatedObject) { +
      Updated Item: {{ updatedObject | json }}
      + } + @if (alertWarning) { +
      Updated Item: {{ alertWarning }}
      + }
      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example26.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example26.component.ts index 760d4935f1..ac099912b9 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example26.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example26.component.ts @@ -1,9 +1,9 @@ -import { JsonPipe, NgIf } from '@angular/common'; +import { JsonPipe } from '@angular/common'; import { Component, ViewEncapsulation, type OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; import { - AngularSlickgridModule, + AngularSlickgridComponent, AngularUtilService, Editors, Filters, @@ -30,7 +30,7 @@ const NB_ITEMS = 100; styleUrls: ['./example26.component.scss'], encapsulation: ViewEncapsulation.None, providers: [AngularUtilService], - imports: [AngularSlickgridModule, JsonPipe, NgIf], + imports: [AngularSlickgridComponent, JsonPipe], }) export class Example26Component implements OnInit { private _commandQueue: any[] = []; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example27.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example27.component.ts index e181eff647..e70d907fd8 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example27.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example27.component.ts @@ -1,7 +1,7 @@ -import { ChangeDetectorRef, Component, ViewEncapsulation, type OnInit } from '@angular/core'; +import { Component, ViewEncapsulation, type OnInit } from '@angular/core'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Filters, Formatters, type AngularGridInstance, @@ -18,7 +18,7 @@ const NB_ITEMS = 500; templateUrl: './example27.component.html', styleUrls: ['example27.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example27Component implements OnInit { angularGrid!: AngularGridInstance; @@ -33,8 +33,6 @@ export class Example27Component implements OnInit { hasNoExpandCollapseChanged = true; treeToggleItems: TreeToggledItem[] = []; - constructor(private cdref: ChangeDetectorRef) {} - ngOnInit(): void { // define the grid options & columns and then create the grid itself this.defineGrid(); @@ -329,14 +327,12 @@ export class Example27Component implements OnInit { /** Whenever a parent is being toggled, we'll keep a reference of all of these changes so that we can reapply them whenever we want */ handleOnTreeItemToggled(treeToggleExecution: TreeToggleStateChange) { this.hasNoExpandCollapseChanged = false; - this.cdref.detectChanges(); this.treeToggleItems = treeToggleExecution.toggledItems as TreeToggledItem[]; console.log('Tree Data changes', treeToggleExecution); } handleOnGridStateChanged(gridStateChange: GridStateChange) { this.hasNoExpandCollapseChanged = false; - this.cdref.detectChanges(); if (gridStateChange?.change?.type === 'treeData') { console.log('Tree Data gridStateChange', gridStateChange?.gridState?.treeData); diff --git a/frameworks/angular-slickgrid/src/demos/examples/example28.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example28.component.ts index f4215c57a2..9ea5a984a7 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example28.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example28.component.ts @@ -4,7 +4,7 @@ import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { addWhiteSpaces, Aggregators, - AngularSlickgridModule, + AngularSlickgridComponent, decimalFormatted, Filters, findItemInTreeStructure, @@ -21,7 +21,7 @@ import { templateUrl: './example28.component.html', styleUrls: ['example28.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [AngularSlickgridModule, FormsModule], + imports: [AngularSlickgridComponent, FormsModule], }) export class Example28Component implements OnInit { angularGrid!: AngularGridInstance; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example29.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example29.component.ts index efb49104bd..ebea00f149 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example29.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example29.component.ts @@ -1,14 +1,15 @@ -import { NgIf } from '@angular/common'; import { Component, type OnInit } from '@angular/core'; -import { AngularSlickgridModule, Formatters, type AngularGridInstance, type Column, type GridOption } from '../../library'; +import { AngularSlickgridComponent, Formatters, type AngularGridInstance, type Column, type GridOption } from '../../library'; const NB_ITEMS = 995; @Component({ template: ` -
      You've clicked me {{ clickedTimes }} time(s)
      `, + @if (clickedTimes) { +
      You've clicked me {{ clickedTimes }} time(s)
      + }`, selector: 'custom-footer', - imports: [NgIf], + imports: [], }) export class CustomFooterComponent { clickedTimes = 0; @@ -20,7 +21,7 @@ export class CustomFooterComponent { @Component({ templateUrl: './example29.component.html', - imports: [AngularSlickgridModule, CustomFooterComponent], + imports: [AngularSlickgridComponent, CustomFooterComponent], }) export class Example29Component implements OnInit { angularGrid!: AngularGridInstance; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example30.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example30.component.ts index 8002196f31..9209cef6b2 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example30.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example30.component.ts @@ -4,7 +4,7 @@ import { SlickCompositeEditor, SlickCompositeEditorComponent } from '@slickgrid- import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Editors, Filters, formatNumber, @@ -87,7 +87,7 @@ const myCustomTitleValidator = (value: any, args: any) => { templateUrl: './example30.component.html', styleUrls: ['./example30.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example30Component implements OnDestroy, OnInit { private _darkMode = false; @@ -493,7 +493,7 @@ export class Example30Component implements OnDestroy, OnInit { }, externalResources: [new ExcelExportService(), new SlickCustomTooltip(), this.compositeEditorInstance], enableFiltering: true, - rowSelectionOptions: { + selectionOptions: { // True (Single Selection), False (Multiple Selections) selectActiveRow: false, }, @@ -501,7 +501,7 @@ export class Example30Component implements OnDestroy, OnInit { showPreHeaderPanel: true, preHeaderPanelHeight: 28, enableCheckboxSelector: true, - enableRowSelection: true, + enableSelection: true, multiSelect: false, checkboxSelector: { hideInFilterHeaderRow: false, diff --git a/frameworks/angular-slickgrid/src/demos/examples/example32.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example32.component.ts index 7505f72418..8f2cb60b97 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example32.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example32.component.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Component, ViewEncapsulation, type OnInit } from '@angular/core'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Editors, Filters, formatNumber, @@ -44,7 +44,7 @@ const myCustomTitleValidator = (value: any) => { templateUrl: './example32.component.html', styleUrls: ['./example32.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example32Component implements OnInit { angularGrid!: AngularGridInstance; @@ -495,13 +495,13 @@ export class Example32Component implements OnInit { }, 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, }, @@ -586,7 +586,7 @@ export class Example32Component implements OnInit { // just for demo purposes, set it back to its original width const columns = this.angularGrid.slickGrid.getColumns() as Column[]; columns.forEach((col) => (col.width = col.originalWidth)); - this.angularGrid.slickGrid.setColumns(columns); + this.angularGrid.slickGrid.updateColumns(); this.angularGrid.slickGrid.autosizeColumns(); this.isUsingDefaultResize = true; } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example33.component.html b/frameworks/angular-slickgrid/src/demos/examples/example33.component.html index 338472056a..de469b2d66 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example33.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example33.component.html @@ -34,8 +34,26 @@

      + +
      -
      +
      Lazy loading collection...
      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example33.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example33.component.ts index c1fcf142e8..90c287c5f7 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example33.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example33.component.ts @@ -1,14 +1,13 @@ -import { NgClass } from '@angular/common'; -import { Component, ViewEncapsulation, type OnInit } from '@angular/core'; +import { Component, signal, ViewEncapsulation, type OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { - AngularSlickgridModule, + AngularSlickgridComponent, + createDomElement, Editors, Filters, Formatters, - OperatorType, type AngularGridInstance, type Column, type EditCommand, @@ -26,7 +25,7 @@ const NB_ITEMS = 1000; templateUrl: './example33.component.html', styleUrls: ['./example33.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [FormsModule, NgClass, AngularSlickgridModule], + imports: [FormsModule, AngularSlickgridComponent], }) export class Example33Component implements OnInit { angularGrid!: AngularGridInstance; @@ -36,7 +35,7 @@ export class Example33Component implements OnInit { dataset!: any[]; hideSubTitle = false; serverApiDelay = 500; - showLazyLoading = false; + showLazyLoading = signal(false); ngOnInit(): void { this.initializeGrid(); @@ -146,6 +145,31 @@ export class Example33Component implements OnInit { // 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', @@ -307,11 +331,11 @@ export class Example33Component implements OnInit { // }); // }), collectionLazy: () => { - this.showLazyLoading = true; + this.showLazyLoading.set(true); return new Promise((resolve) => { setTimeout(() => { - this.showLazyLoading = false; + this.showLazyLoading.set(false); resolve(Array.from(Array((this.dataset || []).length).keys()).map((k) => ({ value: k, label: `Task ${k}` }))); }, this.serverApiDelay); }); @@ -327,7 +351,7 @@ export class Example33Component implements OnInit { }, model: Filters.multipleSelect, options: { minHeight: 70 } as MultipleSelectOption, - operator: OperatorType.inContains, + operator: 'IN_CONTAINS', }, }, { @@ -409,19 +433,21 @@ export class Example33Component implements OnInit { 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, @@ -438,7 +464,7 @@ export class Example33Component implements OnInit { 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 && 'completed' in dataContext) { dataContext.completed = args.item.option; this.angularGrid.gridService.updateItem(dataContext); @@ -463,7 +489,7 @@ export class Example33Component implements OnInit { 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 @@ -571,4 +597,18 @@ export class Example33Component implements OnInit { document.querySelector('.subtitle')?.classList[action]('hidden'); this.angularGrid.resizerService.resizeGrid(2); } + + setFiltersDynamically(operator: string) { + const operatorType = operator === '=' ? '=' : '!='; + this.angularGrid.filterService.updateFilters( + [ + { + columnId: 'desc', + operator: operatorType, + searchTerms: [''], + }, + ], + true + ); + } } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example34.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example34.component.ts index cc428d790b..75074409f3 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example34.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example34.component.ts @@ -4,7 +4,7 @@ import { faker } from '@faker-js/faker'; import sparkline from '@fnando/sparkline'; import { Aggregators, - AngularSlickgridModule, + AngularSlickgridComponent, createDomElement, deepCopy, Filters, @@ -80,7 +80,7 @@ const historicSparklineFormatter: Formatter = (_row, _cell, _value: string, _col templateUrl: './example34.component.html', styleUrls: ['./example34.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [AngularSlickgridModule, FormsModule], + imports: [AngularSlickgridComponent, FormsModule], }) export class Example34Component implements OnDestroy, OnInit { private _darkMode = false; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example35.component.html b/frameworks/angular-slickgrid/src/demos/examples/example35.component.html index 909b450a6e..16de1f8c56 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example35.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example35.component.html @@ -60,12 +60,12 @@

      Switch Language - Locale: {{ selectedLanguage + '.json' }} + Locale: {{ selectedLanguage() + '.json' }}

      -
      +
      Status: - +
      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example35.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example35.component.ts index a36db153f3..dd43b5e35f 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example35.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example35.component.ts @@ -1,8 +1,8 @@ -import { Component, ViewEncapsulation, type OnInit } from '@angular/core'; +import { Component, signal, ViewEncapsulation, type OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; import type { Subscription } from 'rxjs'; -import { AngularSlickgridModule, Editors, Formatters, type AngularGridInstance, type Column, type GridOption } from '../../library'; +import { AngularSlickgridComponent, Editors, Formatters, type AngularGridInstance, type Column, type GridOption } from '../../library'; const NB_ITEMS = 20; @@ -10,7 +10,7 @@ const NB_ITEMS = 20; encapsulation: ViewEncapsulation.None, styleUrls: ['./example35.component.scss'], templateUrl: './example35.component.html', - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example35Component implements OnInit { private subscriptions: Subscription[] = []; @@ -19,17 +19,17 @@ export class Example35Component implements OnInit { columnDefinitions!: Column[]; dataset!: any[]; hideSubTitle = false; - selectedLanguage = ''; + selectedLanguage = signal(''); selectedLanguageFile = ''; - fetchResult = ''; - statusClass = 'alert alert-light'; - statusStyle = 'display: none'; + fetchResult = signal(''); + statusClass = signal('alert alert-light'); + statusStyle = signal('display: none'); constructor(private translate: TranslateService) { // always start with English for Cypress E2E tests to be consistent const defaultLang = 'en'; this.translate.use(defaultLang); - this.selectedLanguage = defaultLang; + this.selectedLanguage.set(defaultLang); } ngOnInit() { @@ -148,7 +148,7 @@ export class Example35Component implements OnInit { }) .then((response: any) => { if (response === false) { - this.statusClass = 'alert alert-danger'; + this.statusClass.set('alert alert-danger'); return false; } if (typeof response === 'object') { @@ -156,9 +156,9 @@ export class Example35Component implements OnInit { } }) .then((json) => { - this.statusStyle = 'display: block'; - this.statusClass = 'alert alert-success'; - this.fetchResult = json.message; + this.statusStyle.set('display: block'); + this.statusClass.set('alert alert-success'); + this.fetchResult.set(json.message); return true; }); }, @@ -222,9 +222,9 @@ export class Example35Component implements OnInit { } clearStatus() { - this.statusClass = 'alert alert-light'; - this.statusStyle = 'display: none'; - this.fetchResult = ''; + this.statusClass.set('alert alert-light'); + this.statusStyle.set('display: none'); + this.fetchResult.set(''); } toggleSingleMultiRowEdit() { @@ -242,10 +242,10 @@ export class Example35Component implements OnInit { } switchLanguage() { - const nextLanguage = this.selectedLanguage === 'en' ? 'fr' : 'en'; + const nextLanguage = this.selectedLanguage() === 'en' ? 'fr' : 'en'; this.subscriptions.push( this.translate.use(nextLanguage).subscribe(() => { - this.selectedLanguage = nextLanguage; + this.selectedLanguage.set(nextLanguage); }) ); } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example36.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example36.component.ts index 5a416ea828..ec6b917ecb 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example36.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example36.component.ts @@ -3,7 +3,7 @@ import { FormsModule } from '@angular/forms'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { Aggregators, - AngularSlickgridModule, + AngularSlickgridComponent, Editors, Formatters, GroupTotalFormatters, @@ -99,7 +99,7 @@ export class CustomSumAggregator implements Aggregator { templateUrl: './example36.component.html', styleUrls: ['./example36.component.scss'], encapsulation: ViewEncapsulation.None, - imports: [AngularSlickgridModule, FormsModule], + imports: [AngularSlickgridComponent, FormsModule], }) export class Example36Component implements OnInit { columnDefinitions: Column[] = []; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example37.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example37.component.ts index fb46ec08c1..e0142b6248 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example37.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example37.component.ts @@ -1,6 +1,6 @@ import { Component, type OnDestroy, type OnInit } from '@angular/core'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Editors, type AngularGridInstance, type Column, @@ -12,7 +12,7 @@ const NB_ITEMS = 100; @Component({ templateUrl: './example37.component.html', - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example37Component implements OnDestroy, OnInit { private _darkMode = false; diff --git a/frameworks/angular-slickgrid/src/demos/examples/example38.component.html b/frameworks/angular-slickgrid/src/demos/examples/example38.component.html index b0d17bd033..effb7f68ce 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example38.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example38.component.html @@ -40,24 +40,26 @@
      -
      - Backend Error: -
      + @if (errorStatus()) { +
      + Backend Error: +
      + }
      -
      - Status: {{ status.text }} - +
      + Status: {{ status()?.text }} +
      - OData Query: {{ odataQuery }} + OData Query: {{ odataQuery() }}
      @@ -83,17 +85,19 @@
      Group by Gender -
      - Metrics: - - {{ metrics.endTime | date: 'dd MMM, h:mm:ssa' }} — - {{ metrics.itemCount }} - of - {{ metrics.totalItemCount }} - items - - All Data Loaded!!! -
      + @if (metrics()) { +
      + Metrics: + + {{ metrics()?.endTime | date: 'dd MMM, h:mm:ssa' }} — + {{ metrics()?.itemCount }} + of + {{ metrics()?.totalItemCount }} + items + + All Data Loaded!!! +
      + }
      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example38.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example38.component.ts index 3d443cf1ec..cb5f46fb12 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example38.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example38.component.ts @@ -1,10 +1,10 @@ -import { DatePipe, NgIf } from '@angular/common'; +import { DatePipe } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { ChangeDetectorRef, Component, ViewEncapsulation, type OnInit } from '@angular/core'; +import { Component, signal, ViewEncapsulation, type OnInit } from '@angular/core'; import { GridOdataService, type OdataServiceApi } from '@slickgrid-universal/odata'; import { Aggregators, - AngularSlickgridModule, + AngularSlickgridComponent, Filters, SortComparers, type AngularGridInstance, @@ -23,7 +23,7 @@ const PERCENT_HTML_ESCAPED = '%25'; styleUrls: ['./example38.component.scss'], encapsulation: ViewEncapsulation.None, templateUrl: './example38.component.html', - imports: [AngularSlickgridModule, DatePipe, NgIf], + imports: [AngularSlickgridComponent, DatePipe], }) export class Example38Component implements OnInit { angularGrid!: AngularGridInstance; @@ -33,18 +33,15 @@ export class Example38Component implements OnInit { dataset: any[] = []; hideSubTitle = false; isPageErrorTest = false; - metrics!: Partial; - tagDataClass = ''; - odataQuery = ''; - processing = false; - errorStatus = ''; - errorStatusClass = 'hidden'; - status = { text: 'processing...', class: 'alert alert-danger' }; - - constructor( - private readonly cd: ChangeDetectorRef, - private http: HttpClient - ) { + metrics = signal | undefined>(undefined); + tagDataClass = signal(''); + odataQuery = signal(''); + processing = signal(false); + errorStatus = signal(''); + errorStatusClass = signal('hidden'); + status = signal({ text: 'processing...', class: 'alert alert-danger' }); + + constructor(private http: HttpClient) { this.backendService = new GridOdataService(); } @@ -106,7 +103,7 @@ export class Example38Component implements OnInit { enableCellNavigation: true, enableFiltering: true, enableCheckboxSelector: true, - enableRowSelection: true, + enableSelection: true, enableGrouping: true, headerMenu: { hideFreezeColumnsCommand: false, @@ -124,21 +121,20 @@ export class Example38Component implements OnInit { version: 4, }, onError: (error: Error) => { - this.errorStatus = error.message; - this.errorStatusClass = 'visible notification is-light is-danger is-small is-narrow'; + this.errorStatus.set(error.message); + this.errorStatusClass.set('visible notification is-light is-danger is-small is-narrow'); this.displaySpinner(false, true); }, preProcess: () => { - this.errorStatus = ''; - this.errorStatusClass = 'hidden'; + this.errorStatus.set(''); + this.errorStatusClass.set('hidden'); this.displaySpinner(true); }, process: (query) => this.getCustomerApiCall(query), postProcess: (response) => { - this.metrics = response.metrics; + this.metrics.set(response.metrics); this.displaySpinner(false); this.getCustomerCallback(response); - this.cd.detectChanges(); }, // we could use local in-memory Filtering (please note that it only filters against what is currently loaded) // that is when we want to avoid reloading the entire dataset every time @@ -148,20 +144,23 @@ export class Example38Component implements OnInit { } displaySpinner(isProcessing: boolean, isError?: boolean) { - this.processing = isProcessing; + this.processing.set(isProcessing); if (isError) { - this.status = { text: 'ERROR!!!', class: 'alert alert-danger' }; + this.status.set({ text: 'ERROR!!!', class: 'alert alert-danger' }); } else { - this.status = isProcessing ? { text: 'loading', class: 'alert alert-warning' } : { text: 'finished', class: 'alert alert-success' }; + this.status.set( + isProcessing ? { text: 'loading', class: 'alert alert-warning' } : { text: 'finished', class: 'alert alert-success' } + ); } - this.cd.detectChanges(); } getCustomerCallback(data: { '@odata.count': number; infiniteScrollBottomHit: boolean; metrics: Metrics; query: string; value: any[] }) { // totalItems property needs to be filled for pagination to work correctly // however we need to force a dirty check, doing a clone object will do just that const totalItemCount: number = data['@odata.count']; - this.metrics.totalItemCount = totalItemCount; + const metrics = { ...this.metrics() }; + metrics.totalItemCount = totalItemCount; + this.metrics.set(metrics); // even if we're not showing pagination, it is still used behind the scene to fetch next set of data (next page basically) // once pagination totalItems is filled, we can update the dataset @@ -172,13 +171,15 @@ export class Example38Component implements OnInit { // initial load not scroll hit yet, full dataset assignment this.angularGrid.slickGrid?.scrollTo(0); // scroll back to top to avoid unwanted onScroll end triggered this.dataset = data.value; - this.metrics.itemCount = data.value.length; + const updatedMetrics = { ...this.metrics() }; + updatedMetrics.itemCount = data.value.length; + this.metrics.set(updatedMetrics); } else { // scroll hit, for better perf we can simply use the DataView directly for better perf (which is better compare to replacing the entire dataset) this.angularGrid.dataView?.addItems(data.value); } - this.odataQuery = data['query']; + this.odataQuery.set(data['query']); // NOTE: you can get currently loaded item count via the `onRowCountChanged`slick event, see `refreshMetrics()` below // OR you could also calculate it yourself or get it via: `this.sgb.dataView.getItemCount() === totalItemCount` @@ -196,7 +197,7 @@ export class Example38Component implements OnInit { * in your case the getCustomer() should be a WebAPI function returning a Promise */ getCustomerDataApiMock(query: string): Promise { - this.errorStatusClass = 'hidden'; + this.errorStatusClass.set('hidden'); // the mock is returning a Promise, just like a WebAPI typically does return new Promise((resolve) => { @@ -412,8 +413,10 @@ export class Example38Component implements OnInit { refreshMetrics(args: OnRowCountChangedEventArgs) { if (args?.current >= 0) { - this.metrics.itemCount = this.angularGrid.dataView?.getFilteredItemCount() || 0; - this.tagDataClass = this.metrics.itemCount === this.metrics.totalItemCount ? 'fully-loaded' : 'partial-load'; + const metrics = { ...this.metrics() }; + metrics.itemCount = this.angularGrid.dataView?.getFilteredItemCount() || 0; + this.metrics.set(metrics); + this.tagDataClass.set(metrics.itemCount === metrics.totalItemCount ? 'fully-loaded' : 'partial-load'); } } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example39.component.html b/frameworks/angular-slickgrid/src/demos/examples/example39.component.html index 74f51509bd..8135ce32f1 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example39.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example39.component.html @@ -40,9 +40,9 @@
      -
      - Status: {{ status.text }} - +
      + Status: {{ status()?.text }} +
      @@ -77,22 +77,24 @@
      Locale: - {{ selectedLanguage + '.json' }} + {{ selectedLanguage() + '.json' }}

      -
      - Metrics: - - {{ metrics.endTime | date: 'dd MMM, h:mm:ssa' }} — - {{ metrics.itemCount }} - of - {{ metrics.totalItemCount }} - items - - All Data Loaded!!! -
      + @if (metrics()) { +
      + Metrics: + + {{ metrics()?.endTime | date: 'dd MMM, h:mm:ssa' }} — + {{ metrics()?.itemCount }} + of + {{ metrics()?.totalItemCount }} + items + + All Data Loaded!!! +
      + }
      diff --git a/frameworks/angular-slickgrid/src/demos/examples/example39.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example39.component.ts index 1c90dd1b39..01a4bfb9fc 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example39.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example39.component.ts @@ -1,12 +1,12 @@ -import { DatePipe, NgIf } from '@angular/common'; +import { DatePipe } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { ChangeDetectorRef, Component, ViewEncapsulation, type OnDestroy, type OnInit } from '@angular/core'; +import { Component, signal, ViewEncapsulation, type OnDestroy, type OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { GraphqlService, type GraphqlPaginatedResult, type GraphqlServiceApi } from '@slickgrid-universal/graphql'; import { type Subscription } from 'rxjs'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Filters, unsubscribeAllObservables, type AngularGridInstance, @@ -29,7 +29,7 @@ function unescapeAndLowerCase(val: string) { styleUrls: ['./example39.component.scss'], encapsulation: ViewEncapsulation.None, templateUrl: './example39.component.html', - imports: [AngularSlickgridModule, DatePipe, FormsModule, NgIf], + imports: [AngularSlickgridComponent, DatePipe, FormsModule], }) export class Example39Component implements OnInit, OnDestroy { private subscriptions: Subscription[] = []; @@ -39,16 +39,15 @@ export class Example39Component implements OnInit, OnDestroy { gridOptions!: GridOption; dataset: any[] = []; hideSubTitle = false; - metrics!: Partial; - tagDataClass = ''; + metrics = signal | undefined>(undefined); + tagDataClass = signal(''); graphqlQuery = '...'; - processing = false; - selectedLanguage: string; - status = { text: 'processing...', class: 'alert alert-danger' }; + processing = signal(false); + selectedLanguage = signal(''); + status = signal({ text: 'processing...', class: 'alert alert-danger' }); serverWaitDelay = FAKE_SERVER_DELAY; // server simulation with default of 250ms but 50ms for Cypress tests constructor( - private readonly cd: ChangeDetectorRef, private http: HttpClient, private translate: TranslateService ) { @@ -56,7 +55,7 @@ export class Example39Component implements OnInit, OnDestroy { // always start with English for Cypress E2E tests to be consistent const defaultLang = 'en'; this.translate.use(defaultLang); - this.selectedLanguage = defaultLang; + this.selectedLanguage.set(defaultLang); } ngOnDestroy() { @@ -166,13 +165,12 @@ export class Example39Component implements OnInit, OnDestroy { preProcess: () => this.displaySpinner(true), process: (query) => this.getCustomerApiCall(query), postProcess: (result: GraphqlPaginatedResult) => { - this.metrics = { + this.metrics.set({ endTime: new Date(), totalItemCount: result.data[GRAPHQL_QUERY_DATASET_NAME].totalCount || 0, - }; + }); this.displaySpinner(false); this.getCustomerCallback(result); - this.cd.detectChanges(); }, } as GraphqlServiceApi, }; @@ -185,35 +183,26 @@ export class Example39Component implements OnInit, OnDestroy { } displaySpinner(isProcessing: boolean) { - this.processing = isProcessing; - this.status = isProcessing - ? { text: 'processing...', class: 'alert alert-danger' } - : { text: 'finished', class: 'alert alert-success' }; + this.processing.set(isProcessing); + this.status.set( + isProcessing ? { text: 'processing...', class: 'alert alert-danger' } : { text: 'finished', class: 'alert alert-success' } + ); } getCustomerCallback(result: any) { const { nodes, totalCount } = result.data[GRAPHQL_QUERY_DATASET_NAME]; if (this.angularGrid) { - this.metrics.totalItemCount = totalCount; - - // even if we're not showing pagination, it is still used behind the scene to fetch next set of data (next page basically) - // once pagination totalItems is filled, we can update the dataset - - // infinite scroll has an extra data property to determine if we hit an infinite scroll and there's still more data (in that case we need append data) - // or if we're on first data fetching (no scroll bottom ever occured yet) + const metrics = { ...(this.metrics() ?? {}) }; + metrics.totalItemCount = totalCount; if (!result.infiniteScrollBottomHit) { - // initial load not scroll hit yet, full dataset assignment - this.angularGrid.slickGrid?.scrollTo(0); // scroll back to top to avoid unwanted onScroll end triggered + this.angularGrid.slickGrid?.scrollTo(0); this.dataset = nodes; - this.metrics.itemCount = nodes.length; + metrics.itemCount = nodes.length; + this.metrics.set(metrics); } else { - // scroll hit, for better perf we can simply use the DataView directly for better perf (which is better compare to replacing the entire dataset) this.angularGrid.dataView?.addItems(nodes); } - // NOTE: you can get currently loaded item count via the `onRowCountChanged`slick event, see `refreshMetrics()` below - // OR you could also calculate it yourself or get it via: `this.angularGrid?.dataView.getItemCount() === totalItemCount` - // console.log('is data fully loaded: ', this.angularGrid?.dataView?.getItemCount() === totalItemCount); } } @@ -356,16 +345,18 @@ export class Example39Component implements OnInit, OnDestroy { refreshMetrics(args: OnRowCountChangedEventArgs) { if (args?.current >= 0) { - this.metrics.itemCount = this.angularGrid.dataView?.getFilteredItemCount() || 0; - this.tagDataClass = this.metrics.itemCount === this.metrics.totalItemCount ? 'fully-loaded' : 'partial-load'; + const metrics = { ...(this.metrics() ?? {}) }; + metrics.itemCount = this.angularGrid.dataView?.getFilteredItemCount() || 0; + this.metrics.set(metrics); + this.tagDataClass.set(metrics.itemCount === metrics.totalItemCount ? 'fully-loaded' : 'partial-load'); } } switchLanguage() { - const nextLanguage = this.selectedLanguage === 'en' ? 'fr' : 'en'; + const nextLanguage = this.selectedLanguage() === 'en' ? 'fr' : 'en'; this.subscriptions.push( this.translate.use(nextLanguage).subscribe(() => { - this.selectedLanguage = nextLanguage; + this.selectedLanguage.set(nextLanguage); }) ); // we could also force a query since we provide a Locale and it changed diff --git a/frameworks/angular-slickgrid/src/demos/examples/example40.component.html b/frameworks/angular-slickgrid/src/demos/examples/example40.component.html index 7c6f358b67..32c5508134 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example40.component.html +++ b/frameworks/angular-slickgrid/src/demos/examples/example40.component.html @@ -55,14 +55,16 @@

      -
      - Metrics: - - {{ metrics.endTime | date: 'dd MMM, h:mm:ssa' }} — - {{ metrics.totalItemCount }} - items - -
      + @if (metrics) { +
      + Metrics: + + {{ metrics.endTime | date: 'dd MMM, h:mm:ssa' }} — + {{ metrics.totalItemCount }} + items + +
      + } 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.
        Master Grid

        Detail Grid - Orders for: - {{ selectedName }} + {{ selectedName() }}
        diff --git a/frameworks/angular-slickgrid/src/demos/examples/example50.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example50.component.ts index cae58f7364..cb4de92fb6 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example50.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example50.component.ts @@ -1,6 +1,6 @@ -import { Component, type OnInit } from '@angular/core'; +import { Component, signal, type OnInit } from '@angular/core'; import { - AngularSlickgridModule, + AngularSlickgridComponent, Formatters, type AngularGridInstance, type Column, @@ -27,7 +27,7 @@ export interface OrderData { @Component({ templateUrl: './example50.component.html', - imports: [AngularSlickgridModule], + imports: [AngularSlickgridComponent], }) export class Example50Component implements OnInit { angularGrid1!: AngularGridInstance; @@ -37,7 +37,7 @@ export class Example50Component implements OnInit { gridOptions2!: GridOption; dataset1!: Customer[]; dataset2!: OrderData[]; - selectedName = ''; + selectedName = signal(''); ngOnInit(): void { this.prepareGrid(); @@ -47,7 +47,7 @@ export class Example50Component implements OnInit { angularGridReady1(angularGrid: AngularGridInstance) { this.angularGrid1 = angularGrid; this.angularGrid1.slickGrid?.setSelectedRows([0]); - this.selectedName = `${this.dataset1[0].name} - ${this.dataset1[0].company}`; + this.selectedName.set(`${this.dataset1[0].name} - ${this.dataset1[0].company}`); this.dataset2 = this.mockDetailData(this.dataset1[0]); } @@ -64,8 +64,8 @@ export class Example50Component implements OnInit { gridHeight: 225, gridWidth: 800, rowHeight: 33, - enableHybridSelection: true, - rowSelectionOptions: { + enableSelection: true, + selectionOptions: { selectionType: 'row', }, }; @@ -97,7 +97,7 @@ export class Example50Component implements OnInit { if (item) { this.angularGrid1?.slickGrid?.setSelectedRows([args.row]); this.dataset2 = this.mockDetailData(item); - this.selectedName = `${item.name} - ${item.company}`; + this.selectedName.set(`${item.name} - ${item.company}`); } } diff --git a/frameworks/angular-slickgrid/src/demos/examples/example51.component.html b/frameworks/angular-slickgrid/src/demos/examples/example51.component.html new file mode 100644 index 0000000000..6d57c9cbcf --- /dev/null +++ b/frameworks/angular-slickgrid/src/demos/examples/example51.component.html @@ -0,0 +1,62 @@ +

        + 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. + +

        +
        + +
        +
        + + + + +
        +
        + + + diff --git a/frameworks/angular-slickgrid/src/demos/examples/example51.component.scss b/frameworks/angular-slickgrid/src/demos/examples/example51.component.scss new file mode 100644 index 0000000000..539ad2ecd2 --- /dev/null +++ b/frameworks/angular-slickgrid/src/demos/examples/example51.component.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/frameworks/angular-slickgrid/src/demos/examples/example51.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example51.component.ts new file mode 100644 index 0000000000..6d3d2dc823 --- /dev/null +++ b/frameworks/angular-slickgrid/src/demos/examples/example51.component.ts @@ -0,0 +1,614 @@ +import { Component, ViewEncapsulation, type OnInit } from '@angular/core'; +import { format as tempoFormat } from '@formkit/tempo'; +import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; +import { + Aggregators, + AngularSlickgridComponent, + createDomElement, + Filters, + Formatters, + SortComparers, + SortDirectionNumber, + type AngularGridInstance, + type Column, + type GridOption, + type Grouping, + type MenuCommandItem, +} from '../../library'; + +const NB_ITEMS = 2000; + +interface ReportItem { + id: number; + title: string; + duration: number; + cost: number; + percentComplete: number; + start: Date; + finish: Date; + action?: string; +} + +@Component({ + templateUrl: './example51.component.html', + styleUrls: ['example51.component.scss'], + imports: [AngularSlickgridComponent], + encapsulation: ViewEncapsulation.None, +}) +export class Example51Component implements OnInit { + angularGrid!: AngularGridInstance; + columnDefinitions: Column[] = []; + gridOptions!: GridOption; + dataset: ReportItem[] = []; + hideSubTitle = false; + + angularGridReady(angularGrid: AngularGridInstance) { + this.angularGrid = angularGrid; + } + + ngOnInit(): void { + this.prepareGrid(); + // mock some data (different in each dataset) + this.dataset = this.loadData(NB_ITEMS); + } + + prepareGrid() { + this.columnDefinitions = [ + { + 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) => ` + + `, + }, + { + command: 'sort-desc', + title: 'Sort Descending', + positionOrder: 51, + // Slot renderer using native DOM elements + slotRenderer: () => { + const menuItemElm = createDomElement('div', { className: 'menu-item' }); + const iconElm = createDomElement('i', { className: 'mdi mdi-sort-descending menu-item-icon' }); + const menuItemLabelElm = createDomElement('span', { className: 'menu-item-label', textContent: 'Sort Descending' }); + const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'Alt+↓' }); + menuItemElm.appendChild(iconElm); + menuItemElm.appendChild(menuItemLabelElm); + menuItemElm.appendChild(kbdElm); + return menuItemElm; + }, + }, + ], + }, + }, + }, + { + id: 'duration', + name: 'Duration', + field: 'duration', + sortable: true, + filterable: true, + minWidth: 100, + // Demo: Header Menu with Slot - showing badge and status dot + header: { + menu: { + commandItems: [ + { + command: 'column-resize-by-content', + title: 'Resize by Content', + positionOrder: 47, + // Slot renderer with badge + slotRenderer: () => ` + + `, + }, + { divider: true, command: '', positionOrder: 48 }, + { + command: 'sort-asc', + title: 'Sort Ascending', + iconCssClass: 'mdi mdi-sort-ascending', + positionOrder: 50, + }, + { + command: 'sort-desc', + title: 'Sort Descending', + iconCssClass: 'mdi mdi-sort-descending', + positionOrder: 51, + }, + { divider: true, command: '', positionOrder: 52 }, + { + command: 'clear-sort', + title: 'Remove Sort', + positionOrder: 58, + // Slot renderer with status indicator + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'start', + name: 'Start', + field: 'start', + sortable: true, + formatter: Formatters.dateIso, + filterable: true, + filter: { model: Filters.compoundDate }, + minWidth: 100, + }, + { + id: 'finish', + name: 'Finish', + field: 'finish', + sortable: true, + formatter: Formatters.dateIso, + filterable: true, + filter: { model: Filters.dateRange }, + minWidth: 100, + }, + { + id: 'cost', + name: 'Cost', + field: 'cost', + width: 90, + sortable: true, + filterable: true, + formatter: Formatters.dollar, + // Demo: Header Menu with Slot - showing slotRenderer with callback (item, args) + header: { + menu: { + commandItems: [ + { + command: 'custom-action', + title: 'Advanced Export', + // Demo: Native HTMLElement with event listeners using slotRenderer (full DOM control) + slotRenderer: (cmdItem) => { + // you can use `createDomElement()` from Slickgrid for easier DOM element creation + const containerDiv = createDomElement('div', { className: 'menu-item' }); + const iconDiv = createDomElement('div', { className: 'advanced-export-icon', textContent: '📊' }); + const textSpan = createDomElement('span', { textContent: cmdItem.title || '', style: { flex: '1' } }); + const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'Ctrl+E' }); + containerDiv.appendChild(iconDiv); + containerDiv.appendChild(textSpan); + containerDiv.appendChild(kbdElm); + + // Add native event listeners for hover effects + containerDiv.addEventListener('mouseover', () => { + iconDiv.style.transform = 'scale(1.15)'; + iconDiv.style.background = 'linear-gradient(135deg, #d8dcef 0%, #ffffff 100%)'; + containerDiv.parentElement!.style.backgroundColor = '#854685'; + containerDiv.parentElement!.title = `📈 Export timestamp: ${tempoFormat(new Date(), 'YYYY-MM-DD hh:mm:ss a')}`; + containerDiv.style.color = 'white'; + containerDiv.querySelector('.key-hint')!.style.color = 'black'; + }); + containerDiv.addEventListener('mouseout', () => { + iconDiv.style.transform = 'scale(1)'; + iconDiv.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; + containerDiv.parentElement!.style.backgroundColor = 'white'; + containerDiv.style.color = 'black'; + document.querySelector('.export-timestamp')?.remove(); + }); + + return containerDiv; + }, + action: () => { + alert('Custom export action triggered!'); + }, + }, + { divider: true, command: '' }, + { + command: 'filter-column', + title: 'Filter Column', + // Slot renderer with status indicator and beta badge + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'percentComplete', + name: '% Complete', + field: 'percentComplete', + sortable: true, + filterable: true, + type: 'number', + filter: { model: Filters.slider, operator: '>=' }, + // Demo: Header Menu with Slot - showing interactive element (checkbox) + header: { + menu: { + commandItems: [ + { + command: 'recalc', + title: 'Recalculate', + iconCssClass: 'mdi mdi-refresh', + slotRenderer: () => ` + + `, + }, + ], + }, + }, + }, + { + id: 'action', + name: 'Action', + field: 'action', + width: 70, + minWidth: 70, + maxWidth: 70, + cssClass: 'justify-center flex', + formatter: () => + `
        `, + excludeFromExport: true, + // Demo: Cell Menu with slot examples (demonstrating defaultMenuItemRenderer at menu level) + cellMenu: { + hideCloseButton: false, + commandTitle: 'Cell Actions', + // Demo: Menu-level default renderer that applies to all items unless overridden + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + commandItems: [ + { + command: 'copy-cell', + title: 'Copy Cell Value', + iconCssClass: 'mdi mdi-content-copy', + action: (_e, args) => { + console.log('Copy cell value:', args.dataContext[args.column.field]); + alert(`Copied: ${args.dataContext[args.column.field]}`); + }, + }, + 'divider', + { + command: 'export-row', + title: 'Export Row', + iconCssClass: 'mdi mdi-download', + action: (_e, args) => { + console.log('Export row:', args.dataContext); + alert(`Export row #${args.dataContext.id}`); + }, + }, + { + command: 'export', + title: 'Export', + iconCssClass: 'mdi mdi-download', + commandItems: [ + { + command: 'export-excel', + title: 'Export as Excel', + iconCssClass: 'mdi mdi-file-excel-outline text-success', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to Excel`); + }, + }, + { + command: 'export-csv', + title: 'Export as CSV', + iconCssClass: 'mdi mdi-file-document-outline', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to CSV`); + }, + }, + { + command: 'export-pdf', + title: 'Export as PDF', + iconCssClass: 'mdi mdi-file-pdf-outline text-red', + action: (_e, args) => { + alert(`Export row #${args.dataContext.id} to PDF`); + }, + }, + ], + }, + { divider: true, command: '' }, + { + command: 'edit-row', + title: 'Edit Row', + // Individual slotRenderer overrides the defaultMenuItemRenderer + slotRenderer: (_item, args) => ` + + `, + action: (_e, args) => { + console.log('Edit row:', args.dataContext); + alert(`Edit row #${args.dataContext.id}`); + }, + }, + { + command: 'delete-row', + title: 'Delete Row', + iconCssClass: 'mdi mdi-delete text-danger', + action: (_event, args) => { + const dataContext = args.dataContext; + if (confirm(`Do you really want to delete row (${args.row! + 1}) with "${dataContext.title}"`)) { + this.angularGrid?.gridService.deleteItemById(dataContext.id); + } + }, + }, + ], + }, + }, + ]; + + this.gridOptions = { + autoResize: { + container: '#demo-container', + }, + enableAutoResize: true, + enableCellNavigation: true, + enableFiltering: true, + enableSorting: true, + enableGrouping: true, + + // Header Menu with slots (already configured in columns above) + enableHeaderMenu: true, + headerMenu: { + // hideCommands: ['column-resize-by-content', 'clear-sort'], + + // Demo: Menu-level default renderer for all header menu items + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + }, + + // Cell Menu with slots (configured in the Action column above) + enableCellMenu: true, + + // Context Menu with slot examples + enableContextMenu: true, + contextMenu: { + // hideCommands: ['clear-grouping', 'copy'], + + // build your command items list + // spread built-in commands and optionally filter/sort them however you want + commandListBuilder: (builtInItems) => { + // commandItems.sort((a, b) => (a === 'divider' || b === 'divider' ? 0 : a.title! > b.title! ? -1 : 1)); + return [ + // filter commands if you want + // ...builtInItems.filter((x) => x !== 'divider' && x.command !== 'copy' && x.command !== 'clear-grouping'), + { + command: 'edit-cell', + title: 'Edit Cell', + // Demo: Individual slotRenderer overrides the menu's defaultMenuItemRenderer + slotRenderer: (cmdItem) => { + // you can use `createDomElement()` from Slickgrid for easier DOM element creation + const containerDiv = createDomElement('div', { className: 'menu-item' }); + const iconDiv = createDomElement('div', { className: 'edit-cell-icon', textContent: '✎' }); + const textSpan = createDomElement('span', { textContent: cmdItem.title || '', style: { flex: '1' } }); + const kbdElm = createDomElement('kbd', { className: 'edit-cell', textContent: 'F2' }); + containerDiv.appendChild(iconDiv); + containerDiv.appendChild(textSpan); + containerDiv.appendChild(kbdElm); + + // Native event listeners for interactive effects + containerDiv.addEventListener('mouseover', () => { + iconDiv.style.transform = 'rotate(15deg) scale(1.1)'; + iconDiv.style.boxShadow = '0 2px 8px rgba(0,200,83,0.4)'; + }); + containerDiv.addEventListener('mouseout', () => { + iconDiv.style.transform = 'rotate(0deg) scale(1)'; + iconDiv.style.boxShadow = 'none'; + }); + + return containerDiv; + }, + action: () => alert('Edit cell'), + }, + ...builtInItems, + { divider: true, command: '' }, + { + command: 'export', + title: 'Export', + iconCssClass: 'mdi mdi-download', + commandItems: [ + { + command: 'export-excel', + title: 'Export as Excel', + iconCssClass: 'mdi mdi-file-excel-outline text-success', + action: () => alert('Export to Excel'), + }, + { + command: 'export-csv', + title: 'Export as CSV', + iconCssClass: 'mdi mdi-file-document-outline', + action: () => alert('Export to CSV'), + }, + { + command: 'export-pdf', + title: 'Export as PDF', + iconCssClass: 'mdi mdi-file-pdf-outline text-danger', + action: () => alert('Export to PDF'), + }, + ], + }, + { divider: true, command: '' }, + { + command: 'delete-row', + title: 'Delete Row', + iconCssClass: 'mdi mdi-delete text-danger', + action: () => alert('Delete row'), + }, + ] as Array; + }, + // Demo: Menu-level default renderer for context menu items + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + }, + + // Grid Menu with slot examples (demonstrating defaultMenuItemRenderer at menu level) + enableGridMenu: true, + gridMenu: { + // hideCommands: ['toggle-preheader', 'toggle-filter'], + + // Demo: Menu-level default renderer that applies to all items (can be overridden per item with slotRenderer) + defaultMenuItemRenderer: (cmdItem) => { + return ` + + `; + }, + commandListBuilder: (builtInItems) => { + return [ + ...builtInItems, + { divider: true, command: '' }, + { + command: 'export-excel', + title: 'Export to Excel', + iconCssClass: 'mdi mdi-file-excel-outline', + action: () => alert('Export to Excel'), + }, + { + command: 'export-csv', + title: 'Export to CSV', + iconCssClass: 'mdi mdi-download', + // Individual slotRenderer overrides the defaultMenuItemRenderer for this item + slotRenderer: (cmdItem) => ` + + `, + 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.angularGrid?.dataView?.setGrouping([]); + } + + collapseAllGroups() { + this.angularGrid?.dataView?.collapseAllGroups(); + } + + expandAllGroups() { + this.angularGrid?.dataView?.expandAllGroups(); + } + + groupByDuration() { + // you need to manually add the sort icon(s) in UI + this.angularGrid?.slickGrid?.setSortColumns([{ columnId: 'duration', sortAsc: true }]); + this.angularGrid?.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.angularGrid?.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.hideSubTitle = !this.hideSubTitle; + const action = this.hideSubTitle ? 'add' : 'remove'; + document.querySelector('.subtitle')?.classList[action]('hidden'); + this.angularGrid.resizerService.resizeGrid(0); + } +} diff --git a/frameworks/angular-slickgrid/src/demos/examples/filter-ng-select.component.ts b/frameworks/angular-slickgrid/src/demos/examples/filter-ng-select.component.ts index 53dccdec33..e177474abe 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/filter-ng-select.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/filter-ng-select.component.ts @@ -27,7 +27,7 @@ import { Subject } from 'rxjs'; export class FilterNgSelectComponent { selectedIds = signal([]); selectedItems = signal([]); - collection?: any[]; // this will be filled by the collection of your column definition + collection: any[] = []; // this will be filled by the collection of your column definition onItemChanged = new Subject(); // object onChange(items: any) { diff --git a/frameworks/angular-slickgrid/src/demos/examples/swt-common-grid-pagination.component.ts b/frameworks/angular-slickgrid/src/demos/examples/swt-common-grid-pagination.component.ts index 8815472943..d48225c6bb 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/swt-common-grid-pagination.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/swt-common-grid-pagination.component.ts @@ -1,6 +1,5 @@ -import { NgClass } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { Component, Input, type OnInit } from '@angular/core'; +import { Component, Input, signal, type OnInit } from '@angular/core'; import { TranslateDirective } from '@ngx-translate/core'; import { type GridOption } from '../../library'; import { type SwtCommonGridComponent } from './swt-common-grid.component'; @@ -19,10 +18,10 @@ import { Logger } from './swt-logger.service';