From a9db2cca965adc7871d7e4d050ae8f3653c84bb4 Mon Sep 17 00:00:00 2001 From: Ghislain B Date: Sun, 16 Oct 2022 12:57:42 -0400 Subject: [PATCH] feat(common): BREAKING CHANGE replace jQueryUI with SortableJS in common & DraggableGrouping (#772) * feat(common): replace jQueryUI Autocomplete with Kradeen Autocomplete (#752) * feat(common)!: remove & replace jQueryUI with SortableJS in common & SlickDraggableGrouping (#756) --- CHANGELOG.md | 13 + README.md | 3 +- .../webpack-demo-vanilla-bundle/CHANGELOG.md | 11 + .../assets/i18n/en.json | 1 + .../assets/i18n/fr.json | 1 + .../webpack-demo-vanilla-bundle/package.json | 2 +- .../webpack-demo-vanilla-bundle/src/app.ts | 4 +- .../src/examples/example02.ts | 3 - .../src/examples/example03.ts | 17 +- .../src/examples/example04.ts | 64 +- .../src/examples/example05.ts | 2 +- .../src/examples/example11.ts | 23 +- .../src/examples/example12.ts | 89 +- .../src/examples/example14.ts | 23 +- lerna.json | 2 +- package.json | 2 + packages/binding/CHANGELOG.md | 5 + packages/binding/package.json | 2 +- packages/common/CHANGELOG.md | 12 + packages/common/package.json | 8 +- .../commonEditorFilterUtils.ts | 30 + .../common/src/commonEditorFilter/index.ts | 1 + ...or.spec.ts => autocompleterEditor.spec.ts} | 478 +++++----- .../editors/__tests__/checkboxEditor.spec.ts | 4 +- .../src/editors/__tests__/inputEditor.spec.ts | 4 +- .../__tests__/inputPasswordEditor.spec.ts | 4 +- .../editors/__tests__/longTextEditor.spec.ts | 36 +- .../editors/__tests__/selectEditor.spec.ts | 4 +- ...mpleteEditor.ts => autocompleterEditor.ts} | 425 ++++----- packages/common/src/editors/editors.index.ts | 6 +- packages/common/src/editors/index.ts | 2 +- .../src/enums/columnReorderFunction.type.ts | 2 +- .../__tests__/slickContextMenu.spec.ts | 4 +- .../__tests__/slickDraggableGrouping.spec.ts | 357 +++++--- .../common/src/extensions/menuBaseClass.ts | 10 +- .../src/extensions/menuFromCellBaseClass.ts | 25 +- .../extensions/slickCellExcelCopyManager.ts | 5 +- .../common/src/extensions/slickCellMenu.ts | 4 +- .../src/extensions/slickCellRangeSelector.ts | 28 +- .../extensions/slickCheckboxSelectColumn.ts | 8 +- .../src/extensions/slickColumnPicker.ts | 15 +- .../common/src/extensions/slickContextMenu.ts | 10 +- .../src/extensions/slickDraggableGrouping.ts | 319 ++++--- .../common/src/extensions/slickGridMenu.ts | 9 +- .../src/extensions/slickRowMoveManager.ts | 4 +- ...er.spec.ts => autocompleterFilter.spec.ts} | 404 +++++---- .../filters/__tests__/filterFactory.spec.ts | 22 +- .../filters/__tests__/selectFilter.spec.ts | 20 +- .../__tests__/sliderRangeFilter.spec.ts | 281 ------ ...mpleteFilter.ts => autocompleterFilter.ts} | 391 +++++---- .../common/src/filters/compoundDateFilter.ts | 2 +- packages/common/src/filters/filters.index.ts | 10 +- packages/common/src/filters/index.ts | 3 +- packages/common/src/filters/selectFilter.ts | 6 +- .../common/src/filters/sliderRangeFilter.ts | 280 ------ .../autocompleteOption.interface.ts | 129 --- .../autocompleterOption.interface.ts | 44 + .../src/interfaces/columnEditor.interface.ts | 19 +- .../src/interfaces/columnFilter.interface.ts | 9 - .../src/interfaces/domEvent.interface.ts | 2 +- .../draggableGroupingOption.interface.ts | 2 +- .../src/interfaces/gridOption.interface.ts | 12 +- packages/common/src/interfaces/index.ts | 4 +- .../jQueryUiSliderOption.interface.ts | 46 - .../jQueryUiSliderResponse.interface.ts | 13 - .../common/src/interfaces/locale.interface.ts | 3 + .../interfaces/slickEventData.interface.ts | 2 +- .../__tests__/extension.service.spec.ts | 2 + .../common/src/services/extension.service.ts | 2 +- packages/common/src/styles/_variables.scss | 51 +- packages/common/src/styles/jquery-ui.scss | 375 -------- ...ocomplete.scss => slick-autocomplete.scss} | 111 +-- packages/common/src/styles/slick-grid.scss | 10 +- packages/common/src/styles/slick-plugins.scss | 27 +- .../src/styles/slickgrid-theme-bootstrap.scss | 2 +- .../styles/slickgrid-theme-material.bare.scss | 2 +- .../styles/slickgrid-theme-material.lite.scss | 2 +- .../src/styles/slickgrid-theme-material.scss | 2 +- .../slickgrid-theme-salesforce.bare.scss | 2 +- .../slickgrid-theme-salesforce.lite.scss | 2 +- .../styles/slickgrid-theme-salesforce.scss | 3 +- .../composite-editor-component/CHANGELOG.md | 5 + .../composite-editor-component/package.json | 2 +- packages/custom-footer-component/CHANGELOG.md | 5 + packages/custom-footer-component/package.json | 2 +- packages/custom-tooltip-plugin/CHANGELOG.md | 5 + packages/custom-tooltip-plugin/package.json | 2 +- packages/empty-warning-component/CHANGELOG.md | 5 + packages/empty-warning-component/package.json | 2 +- packages/event-pub-sub/CHANGELOG.md | 5 + packages/event-pub-sub/package.json | 2 +- packages/excel-export/CHANGELOG.md | 5 + packages/excel-export/package.json | 2 +- packages/graphql/CHANGELOG.md | 5 + packages/graphql/package.json | 2 +- packages/odata/CHANGELOG.md | 5 + packages/odata/package.json | 2 +- packages/pagination-component/CHANGELOG.md | 5 + packages/pagination-component/package.json | 2 +- packages/row-detail-view-plugin/CHANGELOG.md | 5 + packages/row-detail-view-plugin/package.json | 2 +- .../src/slickRowDetailView.ts | 4 +- packages/rxjs-observable/CHANGELOG.md | 5 + packages/rxjs-observable/package.json | 2 +- packages/text-export/CHANGELOG.md | 5 + packages/text-export/README.md | 4 +- packages/text-export/package.json | 2 +- .../text-export/src/textExport.service.ts | 2 +- packages/tsconfig.base.json | 1 + packages/tsconfig.bundle.json | 1 + packages/utils/CHANGELOG.md | 5 + packages/utils/package.json | 2 +- packages/utils/src/utils.spec.ts | 33 + packages/utils/src/utils.ts | 9 + packages/vanilla-bundle/CHANGELOG.md | 7 + packages/vanilla-bundle/package.json | 7 +- .../__tests__/slick-vanilla-grid.spec.ts | 6 +- .../components/slick-vanilla-grid-bundle.ts | 16 +- packages/vanilla-force-bundle/CHANGELOG.md | 5 + packages/vanilla-force-bundle/package.json | 2 +- pnpm-lock.yaml | 813 ++++++++++-------- test/cypress/e2e/example03.cy.js | 12 +- test/cypress/e2e/example04.cy.js | 42 + test/cypress/e2e/example07.cy.js | 20 +- test/cypress/e2e/example12.cy.js | 12 +- test/cypress/e2e/example17.cy.js | 10 +- test/cypress/e2e/example18.cy.js | 10 +- test/cypress/jsconfig.json | 1 + test/cypress/support/commands.js | 2 +- test/cypress/support/drag.js | 11 +- test/cypress/support/index.js | 2 +- test/httpClientStub.ts | 2 +- test/jest-pretest.ts | 5 +- test/tsconfig.spec.json | 1 + 134 files changed, 2460 insertions(+), 3049 deletions(-) create mode 100644 packages/common/src/commonEditorFilter/commonEditorFilterUtils.ts create mode 100644 packages/common/src/commonEditorFilter/index.ts rename packages/common/src/editors/__tests__/{autoCompleteEditor.spec.ts => autocompleterEditor.spec.ts} (72%) rename packages/common/src/editors/{autoCompleteEditor.ts => autocompleterEditor.ts} (59%) rename packages/common/src/filters/__tests__/{autoCompleteFilter.spec.ts => autocompleterFilter.spec.ts} (67%) delete mode 100644 packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts rename packages/common/src/filters/{autoCompleteFilter.ts => autocompleterFilter.ts} (52%) delete mode 100644 packages/common/src/filters/sliderRangeFilter.ts delete mode 100644 packages/common/src/interfaces/autocompleteOption.interface.ts create mode 100644 packages/common/src/interfaces/autocompleterOption.interface.ts delete mode 100644 packages/common/src/interfaces/jQueryUiSliderOption.interface.ts delete mode 100644 packages/common/src/interfaces/jQueryUiSliderResponse.interface.ts delete mode 100644 packages/common/src/styles/jquery-ui.scss rename packages/common/src/styles/{ui-autocomplete.scss => slick-autocomplete.scss} (76%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ad09b5f..861b39ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +### Bug Fixes + +* **deps:** update all non-major dependencies ([#769](https://github.com/ghiscoding/slickgrid-universal/issues/769)) ([4e05a4b](https://github.com/ghiscoding/slickgrid-universal/commit/4e05a4b977c760511fc90903c0f62673859bd65f)) - by @renovate-bot +* **styling:** fix some styling issues with input groups and Firefox ([#750](https://github.com/ghiscoding/slickgrid-universal/issues/750)) ([1aa849e](https://github.com/ghiscoding/slickgrid-universal/commit/1aa849ea81461dc9bbd7b3bc05a092bb14c88be2)) - by @ghiscoding + +## ⚠️ Breaking Change +### - Features + +* **common:** replace jQueryUI Autocomplete with Kradeen Autocomplete ([#752](https://github.com/ghiscoding/slickgrid-universal/issues/752)) ([991d29c](https://github.com/ghiscoding/slickgrid-universal/commit/991d29c4c8c85d800d69c4ba16d608d7a20d2a90)) - by @ghiscoding +* **common:** remove & replace jQueryUI with SortableJS in common & SlickDraggableGrouping ([#756](https://github.com/ghiscoding/slickgrid-universal/issues/756)) ([b1c5a84](https://github.com/ghiscoding/slickgrid-universal/commit/b1c5a84bb9a10ff805dfd13996ecf60dae3ab609)) - by @ghiscoding + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) ### Bug Fixes diff --git a/README.md b/README.md index faabd8e3c..1c23e63fa 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ You might be wondering why was this monorepo created? Here are a few reasons: The Vanilla Implementation (which is not associated to any framework) was built with [WebPack](https://webpack.js.org/) and is also used to run and test all the UI functionalities [Cypress](https://www.cypress.io/) (E2E tests). The [Vanilla-force-bundle](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/vanilla-bundle), which extends the `vanilla-bundle` package is what we use in our SalesForce implementation (with Lightning Web Component), hence the creation of this monorepo library. ### Fully Tested with [Jest](https://jestjs.io/) (Unit Tests) - [Cypress](https://www.cypress.io/) (E2E Tests) -Slickgrid-Universal has **100%** Unit Test Coverage, we are talking about +15,000 lines of code (+3,700 unit tests) that are fully tested with [Jest](https://jestjs.io/). There are also +400 Cypress E2E tests to cover all [Examples](https://ghiscoding.github.io/slickgrid-universal/) and most UI functionalities (there's also an additional +500 tests in Aurelia-Slickgrid) +Slickgrid-Universal has **100%** Unit Test Coverage, we are talking about +15,000 lines of code (+3,700 unit tests) that are fully tested with [Jest](https://jestjs.io/). There are also +450 Cypress E2E tests to cover all [Examples](https://ghiscoding.github.io/slickgrid-universal/) and most UI functionalities (there's also an additional +500 tests in Aurelia-Slickgrid) ### Available Demos @@ -73,6 +73,7 @@ Slickgrid-Universal has **100%** Unit Test Coverage, we are talking about +15,00 | [@slickgrid-universal/odata](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/odata) | [![npm](https://img.shields.io/npm/v/@slickgrid-universal/odata.svg?color=forest)](https://www.npmjs.com/package/@slickgrid-universal/odata) | OData Query Service (Filter/Sort/Paging) | [changelog](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/odata/CHANGELOG.md) | | [@slickgrid-universal/row-detail-view-plugin](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/row-detail-view-plugin) | [![npm](https://img.shields.io/npm/v/@slickgrid-universal/row-detail-view-plugin.svg?color=forest)](https://www.npmjs.com/package/@slickgrid-universal/row-detail-view-plugin) | Row Detail View (plugin) | [changelog](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/row-detail-view-plugin/CHANGELOG.md) | | [@slickgrid-universal/rxjs-observable](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/rxjs-observable) | [![npm](https://img.shields.io/npm/v/@slickgrid-universal/rxjs-observable.svg?color=forest)](https://www.npmjs.com/package/@slickgrid-universal/rxjs-observable) | RxJS Observable Service Wrapper | [changelog](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/rxjs-observable/CHANGELOG.md) | +| [@slickgrid-universal/utils](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/utils) | [![npm](https://img.shields.io/npm/v/@slickgrid-universal/utils.svg?color=forest)](https://www.npmjs.com/package/@slickgrid-universal/utils) | Vanilla TypeScript/ES6 implementation | [changelog](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/utils/CHANGELOG.md) | [@slickgrid-universal/vanilla-bundle](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/vanilla-bundle) | [![npm](https://img.shields.io/npm/v/@slickgrid-universal/vanilla-bundle.svg?color=forest)](https://www.npmjs.com/package/@slickgrid-universal/vanilla-bundle) | Vanilla TypeScript/ES6 implementation | [changelog](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/vanilla-bundle/CHANGELOG.md) | [@slickgrid-universal/vanilla-force-bundle](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/vanilla-force-bundle) | [![npm](https://img.shields.io/npm/v/@slickgrid-universal/vanilla-force-bundle.svg?color=forest)](https://www.npmjs.com/package/@slickgrid-universal/vanilla-force-bundle) | Vanilla TypeScript/ES6 for Salesforce implementation | [changelog](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/vanilla-force-bundle/CHANGELOG.md) diff --git a/examples/webpack-demo-vanilla-bundle/CHANGELOG.md b/examples/webpack-demo-vanilla-bundle/CHANGELOG.md index 32c55116f..7eb11232a 100644 --- a/examples/webpack-demo-vanilla-bundle/CHANGELOG.md +++ b/examples/webpack-demo-vanilla-bundle/CHANGELOG.md @@ -1,8 +1,19 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +### Bug Fixes + +* **deps:** update all non-major dependencies ([#769](https://github.com/ghiscoding/slickgrid-universal/issues/769)) ([4e05a4b](https://github.com/ghiscoding/slickgrid-universal/commit/4e05a4b977c760511fc90903c0f62673859bd65f)) - by @renovate-bot + +### Features + +* **common:** replace jQueryUI Autocomplete with Kradeen Autocomplete ([#752](https://github.com/ghiscoding/slickgrid-universal/issues/752)) ([991d29c](https://github.com/ghiscoding/slickgrid-universal/commit/991d29c4c8c85d800d69c4ba16d608d7a20d2a90)) - by @ghiscoding + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) ### Bug Fixes diff --git a/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json b/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json index bff4917cf..ea07f296b 100644 --- a/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json +++ b/examples/webpack-demo-vanilla-bundle/assets/i18n/en.json @@ -40,6 +40,7 @@ "LAST_UPDATE": "Last Update", "LESS_THAN": "Less than", "LESS_THAN_OR_EQUAL_TO": "Less than or equal to", + "NO_ELEMENTS_FOUND": "No elements found", "NOT_CONTAINS": "Not contains", "NOT_EQUAL_TO": "Not equal to", "NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Search items not in a collection, must be separated by a comma (a,b)", diff --git a/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json b/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json index e02cdab89..dd1ff9407 100644 --- a/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json +++ b/examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json @@ -40,6 +40,7 @@ "LAST_UPDATE": "Dernière mise à jour", "LESS_THAN": "Plus petit que", "LESS_THAN_OR_EQUAL_TO": "Plus petit ou égal à", + "NO_ELEMENTS_FOUND": "Aucun élément trouvé", "NOT_CONTAINS": "Ne contient pas", "NOT_EQUAL_TO": "Non égal à", "NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Recherche excluant certain éléments d'une collection, doit être séparé par une virgule (a,b)", diff --git a/examples/webpack-demo-vanilla-bundle/package.json b/examples/webpack-demo-vanilla-bundle/package.json index 6ff7e8a04..3f644125d 100644 --- a/examples/webpack-demo-vanilla-bundle/package.json +++ b/examples/webpack-demo-vanilla-bundle/package.json @@ -1,6 +1,6 @@ { "name": "webpack-demo-vanilla-bundle", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "private": true, "description": "SlickGrid-Universal demo", "directories": { diff --git a/examples/webpack-demo-vanilla-bundle/src/app.ts b/examples/webpack-demo-vanilla-bundle/src/app.ts index 8d3e58aff..2b87309af 100644 --- a/examples/webpack-demo-vanilla-bundle/src/app.ts +++ b/examples/webpack-demo-vanilla-bundle/src/app.ts @@ -21,7 +21,7 @@ export class App { routerConfig: RouterConfig = { pushState: false, routes: [] - } + }; constructor() { this.appRouting = new AppRouting(this.routerConfig); @@ -157,7 +157,7 @@ export class App { handleNavbarHamburgerToggle() { // Get all "navbar-burger" elements - const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); + const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger') || [], 0); // Check if there are any navbar burgers if ($navbarBurgers.length > 0) { diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts index e95a8e400..97ba6ebbd 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts @@ -160,9 +160,6 @@ export class Example2 { enableTextExport: true, enableFiltering: true, enableGrouping: true, - exportOptions: { - sanitizeDataExport: true - }, columnPicker: { onColumnsChanged: (e, args) => console.log(e, args) }, diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts index a0bca3af1..b749fe89e 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example03.ts @@ -302,6 +302,7 @@ export class Example3 { rowHeight: 33, headerRowHeight: 35, enableDraggableGrouping: true, + // frozenColumn: 2, draggableGrouping: { dropPlaceHolderText: 'Drop a column header here to group by the column', // groupIconCssClass: 'fa fa-outdent', @@ -430,22 +431,6 @@ export class Example3 { } } - groupByFieldName(_fieldName, _index) { - this.clearGrouping(); - if (this.draggableGroupingPlugin?.setDroppedGroups) { - this.showPreHeader(); - - // get the field names from Group By select(s) dropdown, but filter out any empty fields - const groupedFields = this.selectedGroupingFields.filter((g) => g !== ''); - if (groupedFields.length === 0) { - this.clearGrouping(); - } else { - this.draggableGroupingPlugin.setDroppedGroups(groupedFields); - } - this.sgb?.slickGrid.invalidate(); // invalidate all rows and re-render - } - } - showPreHeader() { this.sgb?.slickGrid.setPreHeaderPanelVisibility(true); } diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example04.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example04.ts index a8e07cba0..ed58b3c6b 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example04.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example04.ts @@ -1,5 +1,5 @@ import { - AutocompleteOption, + AutocompleterOption, BindingEventService, Column, ColumnEditorDualInput, @@ -14,11 +14,16 @@ import { } from '@slickgrid-universal/common'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; +import { fetch } from 'whatwg-fetch'; + import { ExampleGridOptions } from './example-grid-options'; -import './example02.scss'; +import './example04.scss'; + +const URL_COUNTRIES_COLLECTION = 'assets/data/countries.json'; +const URL_COUNTRY_NAMES_COLLECTION = 'assets/data/country_names.json'; // you can create custom validator to pass to an inline editor -const myCustomTitleValidator = (value, _args) => { +const myCustomTitleValidator = (value) => { if (value === null || value === undefined || !value.length) { return { valid: false, msg: 'This is a required field' }; } else if (!/^Task\s\d+$/.test(value)) { @@ -221,8 +226,20 @@ export class Example4 { filterable: true, sortable: true, minWidth: 100, + // formatter: (_, __, val) => typeof val === 'string' ? val : val.name, + // editor: { + // model: Editors.autocompleter, + // // collectionAsync: fetch(URL_COUNTRY_NAMES_COLLECTION), + // placeholder: '🔎︎ search country', + // customStructure: { label: 'name', value: 'code' }, + // // collectionAsync: fetch(URL_COUNTRIES_COLLECTION), + + // enableRenderHtml: true, + // collection: [{ code: true, name: 'True', labelPrefix: ` ` }, { code: false, name: 'False', labelSuffix: '' }], + // editorOptions: { minLength: 1 } + // }, editor: { - model: Editors.autoComplete, + model: Editors.autocompleter, placeholder: '🔎︎ search city', // We can use the autocomplete through 3 ways "collection", "collectionAsync" or with your own autocomplete options @@ -230,45 +247,58 @@ export class Example4 { // here we use $.ajax just because I'm not sure how to configure HttpClient with JSONP and CORS editorOptions: { minLength: 3, - forceUserInput: true, - source: (request, response) => { + fetch: (searchText, updateCallback) => { $.ajax({ url: 'http://gd.geobytes.com/AutoCompleteCity', dataType: 'jsonp', data: { - q: request.term + q: searchText }, success: (data) => { - response(data); + const finalData = (data.length === 1 && data[0] === '') ? [] : data; // invalid result should be [] instead of ['' + updateCallback(finalData); } }); - } - } as AutocompleteOption, + }, + } as AutocompleterOption, }, + // filter: { + // model: Filters.autocompleter, + // // collectionAsync: fetch(URL_COUNTRY_NAMES_COLLECTION), + // placeholder: '🔎︎ search country', + // customStructure: { label: 'name', value: 'code' }, + // collectionAsync: fetch(URL_COUNTRIES_COLLECTION), + + // // enableRenderHtml: true, + // // collection: [{ code: true, name: 'True', labelPrefix: ` ` }, { code: false, name: 'False', labelSuffix: '' }], + // // filterOptions: { minLength: 1 } + // }, filter: { - model: Filters.autoComplete, + model: Filters.autocompleter, // placeholder: '🔎︎ search city', + // customStructure: { label: 'name', value: 'code' }, // We can use the autocomplete through 3 ways "collection", "collectionAsync" or with your own autocomplete options - // collectionAsync: this.httpFetch.fetch(URL_COUNTRIES_COLLECTION), + // collectionAsync: fetch(URL_COUNTRIES_COLLECTION), // OR use your own autocomplete options, instead of $.ajax, use HttpClient or FetchClient // here we use $.ajax just because I'm not sure how to configure HttpClient with JSONP and CORS filterOptions: { minLength: 3, - source: (request, response) => { + fetch: (searchText, updateCallback) => { $.ajax({ url: 'http://gd.geobytes.com/AutoCompleteCity', dataType: 'jsonp', data: { - q: request.term + q: searchText }, success: (data) => { - response(data); + const finalData = (data.length === 1 && data[0] === '') ? [] : data; // invalid result should be [] instead of [''] + updateCallback(finalData); } }); - } - } as AutocompleteOption, + }, + } as AutocompleterOption, } }, { diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts index ba402dca0..09bd90639 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example05.ts @@ -213,7 +213,7 @@ export class Example5 { enableAutoSizeColumns: true, enableAutoResize: true, enableExcelExport: true, - exportOptions: { exportWithFormatter: true }, + textExportOptions: { exportWithFormatter: true }, excelExportOptions: { exportWithFormatter: true }, registerExternalResources: [new ExcelExportService()], enableFiltering: true, diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts index d6b0d09bc..bb9486d57 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example11.ts @@ -1,5 +1,5 @@ import { - AutocompleteOption, + AutocompleterOption, BindingEventService, DOMEvent, Column, @@ -196,17 +196,16 @@ export class Example11 { type: FieldType.object, sortComparer: SortComparers.objectString, editor: { - model: Editors.autoComplete, + model: Editors.autocompleter, alwaysSaveOnEnterKey: true, massUpdate: true, // example with a Remote API call editorOptions: { - openSearchListOnFocus: true, + showOnFocus: true, minLength: 1, - source: (request, response) => { - // const items = require('c://TEMP/items.json'); + fetch: (searchText, updateCallback) => { const products = this.mockProducts(); - response(products.filter(product => product.itemName.toLowerCase().includes(request.term.toLowerCase()))); + updateCallback(products.filter(product => product.itemName.toLowerCase().includes(searchText.toLowerCase()))); }, renderItem: { // layout: 'twoRows', @@ -215,7 +214,7 @@ export class Example11 { layout: 'fourCorners', templateCallback: (item: any) => this.renderItemCallbackWith4Corners(item), }, - } as AutocompleteOption, + } as AutocompleterOption, }, filter: { model: Filters.inputText, @@ -236,17 +235,17 @@ export class Example11 { sortable: true, minWidth: 100, editor: { - model: Editors.autoComplete, + model: Editors.autocompleter, alwaysSaveOnEnterKey: true, massUpdate: true, editorOptions: { minLength: 1, - source: (request, response) => { + fetch: (searchText, updateCallback) => { const countries: any[] = require('./data/countries.json'); - const foundCountries = countries.filter((country) => country.name.toLowerCase().includes(request.term.toLowerCase())); - response(foundCountries.map(item => ({ label: item.name, value: item.code, }))); + const foundCountries = countries.filter((country) => country.name.toLowerCase().includes(searchText.toLowerCase())); + updateCallback(foundCountries.map(item => ({ label: item.name, value: item.code, }))); }, - }, + } as AutocompleterOption, }, filter: { model: Filters.inputText, diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts index 0f013c4ed..1c72b0ff9 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts @@ -1,6 +1,6 @@ // import { Instance as FlatpickrInstance } from 'flatpickr/dist/types/instance'; import { - AutocompleteOption, + AutocompleterOption, BindingEventService, Column, CompositeEditorModalType, @@ -291,17 +291,16 @@ export class Example12 { type: FieldType.object, sortComparer: SortComparers.objectString, editor: { - model: Editors.autoComplete, + model: Editors.autocompleter, alwaysSaveOnEnterKey: true, massUpdate: true, // example with a Remote API call editorOptions: { minLength: 1, - source: (request, response) => { - // const items = require('c://TEMP/items.json'); + fetch: (searchTerm, callback) => { const products = this.mockProducts(); - response(products.filter(product => product.itemName.toLowerCase().includes(request.term.toLowerCase()))); + callback(products.filter(product => product.itemName.toLowerCase().includes(searchTerm.toLowerCase()))); }, renderItem: { // layout: 'twoRows', @@ -310,11 +309,11 @@ export class Example12 { layout: 'fourCorners', templateCallback: (item: any) => this.renderItemCallbackWith4Corners(item), }, - } as AutocompleteOption, + } as AutocompleterOption, }, filter: { model: Filters.inputText, - // placeholder: '🔎︎ search city', + // placeholder: '🔎︎ search product', type: FieldType.string, queryField: 'product.itemName', } @@ -331,19 +330,18 @@ export class Example12 { sortable: true, minWidth: 100, editor: { - model: Editors.autoComplete, + model: Editors.autocompleter, alwaysSaveOnEnterKey: true, massUpdate: true, editorOptions: { minLength: 0, - openSearchListOnFocus: false, - // onSelect: (e, ui, row, cell, column, dataContext) => console.log(ui, column, dataContext), - source: (request, response) => { + showOnFocus: false, + fetch: (searchText, updateCallback) => { const countries: any[] = require('./data/countries.json'); - const foundCountries = countries.filter((country) => country.name.toLowerCase().includes(request.term.toLowerCase())); - response(foundCountries.map(item => ({ label: item.name, value: item.code, }))); + const foundCountries = countries.filter((country) => country.name.toLowerCase().includes(searchText.toLowerCase())); + updateCallback(foundCountries.map(item => ({ label: item.name, value: item.code, }))); }, - }, + } as AutocompleterOption, }, filter: { model: Filters.inputText, @@ -398,13 +396,13 @@ export class Example12 { autoFixResizeRequiredGoodCount: 1, datasetIdPropertyName: 'id', eventNamingStyle: EventNamingStyle.lowerCase, - editable: true, autoAddCustomEditorFormatter: customEditableInputFormatter, enableAddRow: true, // <-- this flag is required to work with the (create & clone) modal types enableCellNavigation: true, asyncEditorLoading: false, autoEdit: true, autoCommitEdit: true, + editable: true, autoResize: { container: '.demo-container', }, @@ -445,7 +443,7 @@ export class Example12 { const serializedValues = Array.isArray(editCommand.serializedValue) ? editCommand.serializedValue : [editCommand.serializedValue]; const editorColumns = this.columnDefinitions.filter((col) => col.editor !== undefined); - const modifiedColumns = []; + const modifiedColumns: Column[] = []; prevSerializedValues.forEach((_val, index) => { const prevSerializedValue = prevSerializedValues[index]; const serializedValue = serializedValues[index]; @@ -605,7 +603,6 @@ export class Example12 { /* if (columnDef.id === 'completed') { this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select.js, show filter in dropdown - this.compositeEditorInstance.changeFormEditorOption('product', 'minLength', 3); // autocomplete, change minLength char to type this.compositeEditorInstance.changeFormEditorOption('finish', 'minDate', 'today'); // flatpickr, change minDate to today } */ @@ -742,7 +739,7 @@ export class Example12 { listPrice: 2100.23, itemTypeName: 'I', image: 'http://i.stack.imgur.com/pC1Tv.jpg', - icon: `mdi ${this.getRandomIcon(0)}`, + icon: this.getRandomIcon(0) }, { id: 1, @@ -751,7 +748,7 @@ export class Example12 { listPrice: 3200.12, itemTypeName: 'I', image: 'https://i.imgur.com/Fnm7j6h.jpg', - icon: `mdi ${this.getRandomIcon(1)}`, + icon: this.getRandomIcon(1) }, { id: 2, @@ -760,7 +757,7 @@ export class Example12 { listPrice: 15.00, itemTypeName: 'I', image: 'https://i.imgur.com/RaVJuLr.jpg', - icon: `mdi ${this.getRandomIcon(2)}`, + icon: this.getRandomIcon(2) }, { id: 3, @@ -769,7 +766,7 @@ export class Example12 { listPrice: 25.76, itemTypeName: 'I', image: 'http://i.stack.imgur.com/pC1Tv.jpg', - icon: `mdi ${this.getRandomIcon(3)}`, + icon: this.getRandomIcon(3) }, { id: 4, @@ -778,7 +775,7 @@ export class Example12 { listPrice: 13.35, itemTypeName: 'I', image: 'https://i.imgur.com/Fnm7j6h.jpg', - icon: `mdi ${this.getRandomIcon(4)}`, + icon: this.getRandomIcon(4) }, { id: 5, @@ -787,7 +784,7 @@ export class Example12 { listPrice: 23.33, itemTypeName: 'I', image: 'https://i.imgur.com/RaVJuLr.jpg', - icon: `mdi ${this.getRandomIcon(5)}`, + icon: this.getRandomIcon(5) }, { id: 6, @@ -796,7 +793,7 @@ export class Example12 { listPrice: 71.21, itemTypeName: 'I', image: 'http://i.stack.imgur.com/pC1Tv.jpg', - icon: `mdi ${this.getRandomIcon(6)}`, + icon: this.getRandomIcon(6) }, { id: 7, @@ -805,7 +802,7 @@ export class Example12 { listPrice: 2.43, itemTypeName: 'I', image: 'https://i.imgur.com/Fnm7j6h.jpg', - icon: `mdi ${this.getRandomIcon(7)}`, + icon: this.getRandomIcon(7) }, { id: 8, @@ -814,7 +811,7 @@ export class Example12 { listPrice: 31288.39, itemTypeName: 'I', image: 'https://i.imgur.com/RaVJuLr.jpg', - icon: `mdi ${this.getRandomIcon(8)}`, + icon: this.getRandomIcon(8) }, ]; } @@ -889,33 +886,33 @@ export class Example12 { ${item.itemName} +
+
+
+
${item.itemNameTranslated}
+
`; + } + + renderItemCallbackWith4Corners(item: any): string { + return `
+
+ + +
+
+ + + ${item.itemName} + + ${formatNumber(item.listPrice, 2, 2, false, '$')}
${item.itemNameTranslated}
+ Type: ${item.itemTypeName === 'I' ? 'Item' : item.itemTypeName === 'C' ? 'PdCat' : 'Cat'}
`; } - renderItemCallbackWith4Corners(item: any): string { - return `
-
- - -
-
- - - ${item.itemName} - - ${formatNumber(item.listPrice, 2, 2, false, '$')} -
-
-
-
${item.itemNameTranslated}
- Type: ${item.itemTypeName === 'I' ? 'Item' : item.itemTypeName === 'C' ? 'PdCat' : 'Cat'} -
`; - } - openCompositeModal(modalType: CompositeEditorModalType, openDelay = 0) { // open the editor modal and we can also provide a header title with optional parsing pulled from the dataContext, via template {{ }} // for example {{title}} => display the item title, or even complex object works {{product.itemName}} => display item product name diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts index 866b2fec9..6b265a981 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example14.ts @@ -1,5 +1,5 @@ import { - AutocompleteOption, + AutocompleterOption, BindingEventService, Column, Editors, @@ -249,16 +249,15 @@ export class Example14 { type: FieldType.object, sortComparer: SortComparers.objectString, editor: { - model: Editors.autoComplete, + model: Editors.autocompleter, alwaysSaveOnEnterKey: true, // example with a Remote API call editorOptions: { minLength: 1, - source: (request, response) => { - // const items = require('c://TEMP/items.json'); + fetch: (searchText, updateCallback) => { const products = this.mockProducts(); - response(products.filter(product => product.itemName.toLowerCase().includes(request.term.toLowerCase()))); + updateCallback(products.filter(product => product.itemName.toLowerCase().includes(searchText.toLowerCase()))); }, renderItem: { // layout: 'twoRows', @@ -267,7 +266,7 @@ export class Example14 { layout: 'fourCorners', templateCallback: (item: any) => this.renderItemCallbackWith4Corners(item), }, - } as AutocompleteOption, + } as AutocompleterOption, }, filter: { model: Filters.inputText, @@ -288,17 +287,17 @@ export class Example14 { sortable: true, minWidth: 100, editor: { - model: Editors.autoComplete, + model: Editors.autocompleter, alwaysSaveOnEnterKey: true, editorOptions: { minLength: 0, - openSearchListOnFocus: false, - source: (request, response) => { + showOnFocus: false, + fetch: (searchText, updateCallback) => { const countries: any[] = require('./data/countries.json'); - const foundCountries = countries.filter((country) => country.name.toLowerCase().includes(request.term.toLowerCase())); - response(foundCountries.map(item => ({ label: item.name, value: item.code, }))); + const foundCountries = countries.filter((country) => country.name.toLowerCase().includes(searchText.toLowerCase())); + updateCallback(foundCountries.map(item => ({ label: item.name, value: item.code, }))); }, - }, + } as AutocompleterOption, }, filter: { model: Filters.inputText, diff --git a/lerna.json b/lerna.json index 4fb9719bf..1df8c89e9 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/@lerna-lite/cli/schemas/lerna-schema.json", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "npmClient": "pnpm", "loglevel": "info", "command": { diff --git a/package.json b/package.json index 50a222780..74690bd0f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "preview:publish": "lerna publish from-package --dry-run", "preview:version": "lerna version --dry-run", "preview:roll-new-release": "pnpm bundle && pnpm new-version --dry-run && pnpm new-publish --dry-run", + "preview:alpha-release": "lerna publish 2.0.0-alpha.0 --dist-tag next --dry-run", "new-version": "lerna version", "new-publish": "lerna publish from-package", "roll-new-release": "pnpm bundle && pnpm new-version && pnpm new-publish", @@ -39,6 +40,7 @@ "new-version": "To create a new version with lerna, run the following scripts (1) 'bundle', (2) 'new-version' and (3) 'new-publish' to send it over to NPM." }, "devDependencies": { + "@4tw/cypress-drag-drop": "^2.2.1", "@jest/types": "^29.1.2", "@lerna-lite/cli": "^1.12.0", "@lerna-lite/run": "^1.12.0", diff --git a/packages/binding/CHANGELOG.md b/packages/binding/CHANGELOG.md index 7c51576d7..ad8916e79 100644 --- a/packages/binding/CHANGELOG.md +++ b/packages/binding/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/binding + ## [1.3.5](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.4...v1.3.5) (2022-07-28) ### Bug Fixes diff --git a/packages/binding/package.json b/packages/binding/package.json index de5be271d..2da2c8427 100644 --- a/packages/binding/package.json +++ b/packages/binding/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/binding", - "version": "1.3.5", + "version": "2.0.0-alpha.0", "description": "Simple Vanilla Implementation of a Binding Engine & Helper to add properties/events 2 way bindings", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index 1d17fef6b..c344d78d0 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -1,8 +1,20 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +### Bug Fixes + +* **deps:** update all non-major dependencies ([#769](https://github.com/ghiscoding/slickgrid-universal/issues/769)) ([4e05a4b](https://github.com/ghiscoding/slickgrid-universal/commit/4e05a4b977c760511fc90903c0f62673859bd65f)) - by @renovate-bot +* **styling:** fix some styling issues with input groups and Firefox ([#750](https://github.com/ghiscoding/slickgrid-universal/issues/750)) ([1aa849e](https://github.com/ghiscoding/slickgrid-universal/commit/1aa849ea81461dc9bbd7b3bc05a092bb14c88be2)) - by @ghiscoding + +### Features + +* **common:** replace jQueryUI Autocomplete with Kradeen Autocomplete ([#752](https://github.com/ghiscoding/slickgrid-universal/issues/752)) ([991d29c](https://github.com/ghiscoding/slickgrid-universal/commit/991d29c4c8c85d800d69c4ba16d608d7a20d2a90)) - by @ghiscoding + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) ### Bug Fixes diff --git a/packages/common/package.json b/packages/common/package.json index 499af350a..72a9bce30 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/common", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "SlickGrid-Universal Common Code", "main": "dist/commonjs/index.js", "browser": "src/index.ts", @@ -71,20 +71,22 @@ "dependencies": { "@slickgrid-universal/event-pub-sub": "workspace:~", "@slickgrid-universal/utils": "workspace:~", + "autocompleter": "^6.1.3", "dequal": "^2.0.3", "dompurify": "^2.4.0", "flatpickr": "^4.6.13", "jquery": "^3.6.1", - "jquery-ui": "^1.13.2", "moment-mini": "^2.29.4", "multiple-select-modified": "^1.3.17", - "slickgrid": "^2.4.45", + "slickgrid": "^3.0.0", + "sortablejs": "^1.15.0", "un-flatten-tree": "^2.0.12" }, "devDependencies": { "@types/dompurify": "^2.3.4", "@types/jquery": "^3.5.14", "@types/moment": "^2.13.0", + "@types/sortablejs": "^1.13.0", "autoprefixer": "^10.4.12", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/packages/common/src/commonEditorFilter/commonEditorFilterUtils.ts b/packages/common/src/commonEditorFilter/commonEditorFilterUtils.ts new file mode 100644 index 000000000..66ad28214 --- /dev/null +++ b/packages/common/src/commonEditorFilter/commonEditorFilterUtils.ts @@ -0,0 +1,30 @@ +import { AutocompleteItem } from 'autocompleter'; + +import { AutocompleterOption } from '../interfaces'; + +/** + * add loading class ".slick-autocomplete-loading" to the Kraaden Autocomplete input element + * by overriding the original user's fetch method. + * We will add the loading class when the fetch starts and later remove it when the update callback is being called. + * @param inputElm - autocomplete input element + * @param autocompleterOptions - autocomplete settings + */ +export function addAutocompleteLoadingByOverridingFetch(inputElm: HTMLInputElement, autocompleterOptions: Partial>) { + const previousFetch = autocompleterOptions.fetch; + + if (previousFetch) { + autocompleterOptions.fetch = (searchTerm, updateCallback, trigger, cursorPos) => { + // add loading class + inputElm.classList.add('slick-autocomplete-loading'); + + const previousCallback = updateCallback; + const newUpdateCallback = (items: T[] | false) => { + previousCallback(items); + // we're done, time to remove loading class + inputElm.classList.remove('slick-autocomplete-loading'); + }; + // call original fetch implementation + previousFetch!(searchTerm, newUpdateCallback, trigger, cursorPos); + }; + } +} \ No newline at end of file diff --git a/packages/common/src/commonEditorFilter/index.ts b/packages/common/src/commonEditorFilter/index.ts new file mode 100644 index 000000000..c30220474 --- /dev/null +++ b/packages/common/src/commonEditorFilter/index.ts @@ -0,0 +1 @@ +export * from './commonEditorFilterUtils'; \ No newline at end of file diff --git a/packages/common/src/editors/__tests__/autoCompleteEditor.spec.ts b/packages/common/src/editors/__tests__/autocompleterEditor.spec.ts similarity index 72% rename from packages/common/src/editors/__tests__/autoCompleteEditor.spec.ts rename to packages/common/src/editors/__tests__/autocompleterEditor.spec.ts index 4ff83dca0..41cfbfe6a 100644 --- a/packages/common/src/editors/__tests__/autoCompleteEditor.spec.ts +++ b/packages/common/src/editors/__tests__/autocompleterEditor.spec.ts @@ -1,7 +1,9 @@ +import 'jest-extended'; import { Editors } from '../index'; -import { AutoCompleteEditor } from '../autoCompleteEditor'; +import { AutocompleterEditor } from '../autocompleterEditor'; import { KeyCode, FieldType } from '../../enums/index'; -import { AutocompleteOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; +import { AutocompleterOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; +import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; declare const Slick: SlickNamespace; const KEY_CHAR_A = 97; @@ -36,19 +38,21 @@ const gridStub = { onCompositeEditorChange: new Slick.Event(), } as unknown as SlickGrid; -describe('AutoCompleteEditor', () => { +describe('AutocompleterEditor', () => { let divContainer: HTMLDivElement; - let editor: AutoCompleteEditor; + let editor: AutocompleterEditor; let editorArguments: EditorArguments; let mockColumn: Column; let mockItemData: any; + let translateService: TranslateServiceStub; beforeEach(() => { + translateService = new TranslateServiceStub(); divContainer = document.createElement('div'); divContainer.innerHTML = template; document.body.appendChild(divContainer); - mockColumn = { id: 'gender', field: 'gender', editable: true, editor: { model: Editors.autoComplete }, internalColumnEditor: {} } as Column; + mockColumn = { id: 'gender', field: 'gender', editable: true, editor: { model: Editors.autocompleter }, internalColumnEditor: {} } as Column; editorArguments = { grid: gridStub, @@ -68,28 +72,18 @@ describe('AutoCompleteEditor', () => { describe('with invalid Editor instance', () => { it('should throw an error when trying to call init without any arguments', (done) => { try { - editor = new AutoCompleteEditor(null as any); + editor = new AutocompleterEditor(null as any); } catch (e) { expect(e.toString()).toContain(`[Slickgrid-Universal] Something is wrong with this grid, an Editor must always have valid arguments.`); done(); } }); - - it('should throw an error when collection is not a valid array', (done) => { - try { - (mockColumn.internalColumnEditor as ColumnEditor).collection = { hello: 'world' } as any; - editor = new AutoCompleteEditor(editorArguments); - } catch (e) { - expect(e.toString()).toContain(`The "collection" passed to the Autocomplete Editor is not a valid array.`); - done(); - } - }); }); describe('with valid Editor instance', () => { beforeEach(() => { mockItemData = { id: 123, gender: 'male', isActive: true }; - mockColumn = { id: 'gender', field: 'gender', editable: true, editor: { model: Editors.autoComplete }, internalColumnEditor: {} } as Column; + mockColumn = { id: 'gender', field: 'gender', editable: true, editor: { model: Editors.autocompleter }, internalColumnEditor: {} } as Column; (mockColumn.internalColumnEditor as ColumnEditor).collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; editorArguments.column = mockColumn; @@ -101,22 +95,22 @@ describe('AutoCompleteEditor', () => { }); it('should initialize the editor', () => { - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const editorCount = divContainer.querySelectorAll('input.editor-text.editor-gender').length; - const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); expect(editor.instance).toBeTruthy(); - expect(autocompleteUlElms.length).toBe(1); expect(editorCount).toBe(1); }); it('should initialize the editor with element being disabled in the DOM when passing a collectionAsync and an empty collection property', () => { + gridOptionMock.translater = translateService; + gridOptionMock.enableTranslate = true; const mockCollection = ['male', 'female']; const promise = new Promise(resolve => resolve(mockCollection)); (mockColumn.internalColumnEditor as ColumnEditor).collection = null as any; (mockColumn.internalColumnEditor as ColumnEditor).collectionAsync = promise; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const disableSpy = jest.spyOn(editor, 'disable'); editor.destroy(); editor.init(); @@ -127,12 +121,10 @@ describe('AutoCompleteEditor', () => { }); it('should initialize the editor even when user define his own editor options', () => { - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleteOption; - editor = new AutoCompleteEditor(editorArguments); + (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleterOption; + editor = new AutocompleterEditor(editorArguments); const editorCount = divContainer.querySelectorAll('input.editor-text.editor-gender').length; - const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); - expect(autocompleteUlElms.length).toBe(1); expect(editorCount).toBe(1); }); @@ -140,7 +132,7 @@ describe('AutoCompleteEditor', () => { const testValue = 'test placeholder'; (mockColumn.internalColumnEditor as ColumnEditor).placeholder = testValue; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const editorElm = divContainer.querySelector('input.editor-text.editor-gender') as HTMLInputElement; expect(editorElm.placeholder).toBe(testValue); @@ -150,14 +142,14 @@ describe('AutoCompleteEditor', () => { const testValue = 'test title'; (mockColumn.internalColumnEditor as ColumnEditor).title = testValue; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const editorElm = divContainer.querySelector('input.editor-text.editor-gender') as HTMLInputElement; expect(editorElm.title).toBe(testValue); }); it('should call "setValue" and expect the DOM element to have the same value when calling "getValue"', () => { - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.setValue('male'); expect(editor.getValue()).toBe('male'); @@ -165,7 +157,7 @@ describe('AutoCompleteEditor', () => { it('should call "setValue" with value & apply value flag and expect the DOM element to have same value and also expect the value to be applied to the item object', () => { mockColumn.type = FieldType.object; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.setValue({ value: 'male', label: 'male' }, true); expect(editor.getValue()).toBe('male'); @@ -173,7 +165,7 @@ describe('AutoCompleteEditor', () => { }); it('should define an item datacontext containing a string as cell value and expect this value to be loaded in the editor when calling "loadValue"', () => { - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue(mockItemData); expect(editor.getValue()).toBe('male'); @@ -182,7 +174,7 @@ describe('AutoCompleteEditor', () => { it('should define an item datacontext containing a complex object as cell value and expect this value to be loaded in the editor when calling "loadValue"', () => { mockItemData = { id: 123, gender: { value: 'male', label: 'Male' }, isActive: true }; mockColumn.field = 'gender.value'; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue(mockItemData); expect(editor.getValue()).toBe('male'); @@ -192,7 +184,7 @@ describe('AutoCompleteEditor', () => { const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: KeyCode.LEFT, bubbles: true, cancelable: true }); const spyEvent = jest.spyOn(event, 'stopImmediatePropagation'); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; editorElm.focus(); @@ -205,7 +197,7 @@ describe('AutoCompleteEditor', () => { const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: KeyCode.RIGHT, bubbles: true, cancelable: true }); const spyEvent = jest.spyOn(event, 'stopImmediatePropagation'); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; editorElm.focus(); @@ -219,7 +211,7 @@ describe('AutoCompleteEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).customStructure = { value: 'option', label: 'text' }; const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: 109, bubbles: true, cancelable: true }); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; editorElm.focus(); @@ -231,7 +223,7 @@ describe('AutoCompleteEditor', () => { it('should return True when calling "isValueChanged()" method with previously dispatched keyboard event being char "a"', () => { const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: KEY_CHAR_A, bubbles: true, cancelable: true }); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.setValue('z'); const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; @@ -244,7 +236,7 @@ describe('AutoCompleteEditor', () => { it('should return False when calling "isValueChanged()" method with previously dispatched keyboard event is same char as current value', () => { const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: KEY_CHAR_A, bubbles: true, cancelable: true }); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; editor.loadValue({ id: 123, gender: 'a', isActive: true }); @@ -258,7 +250,7 @@ describe('AutoCompleteEditor', () => { const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: KeyCode.ENTER, bubbles: true, cancelable: true }); (mockColumn.internalColumnEditor as ColumnEditor).alwaysSaveOnEnterKey = true; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; editorElm.focus(); @@ -268,7 +260,7 @@ describe('AutoCompleteEditor', () => { }); it('should call "focus()" method and expect the DOM element to be focused and selected', () => { - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const editorElm = editor.editorDomElement; const spy = jest.spyOn(editorElm, 'focus'); editor.focus(); @@ -276,14 +268,6 @@ describe('AutoCompleteEditor', () => { expect(spy).toHaveBeenCalled(); }); - it('should call the "changeEditorOption" method and expect new option to be merged with the previous Editor options and also expect to call AutoComplete "option" setter method', () => { - editor = new AutoCompleteEditor(editorArguments); - const autoCompleteSpy = jest.spyOn(editor.editorDomElement, 'autocomplete'); - editor.changeEditorOption('delay', 500); - - expect(autoCompleteSpy).toHaveBeenCalledWith('option', 'delay', 500); - }); - describe('collectionOverride callback option', () => { it('should create the editor and expect a different collection outputed when using the override', () => { mockColumn.internalColumnEditor = { @@ -291,7 +275,7 @@ describe('AutoCompleteEditor', () => { collectionOverride: (inputCollection) => inputCollection.filter(item => item.value !== 'other') }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.destroy(); editor.init(); const editorCount = divContainer.querySelectorAll('input.editor-text.editor-gender').length; @@ -306,7 +290,7 @@ describe('AutoCompleteEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).validator = null as any; mockItemData = { id: 123, gender: 'female', isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.applyValue(mockItemData, { value: 'female', label: 'female' }); expect(mockItemData).toEqual({ id: 123, gender: { value: 'female', label: 'female' }, isActive: true }); @@ -317,7 +301,7 @@ describe('AutoCompleteEditor', () => { mockColumn.field = 'user.gender'; mockItemData = { id: 1, user: { gender: 'female' }, isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.applyValue(mockItemData, { value: 'female', label: 'female' }); expect(mockItemData).toEqual({ id: 1, user: { gender: { value: 'female', label: 'female' } }, isActive: true }); @@ -328,7 +312,7 @@ describe('AutoCompleteEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).collection = ['male', 'female']; mockItemData = { id: 123, gender: 'female', isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.applyValue(mockItemData, 'female'); expect(mockItemData).toEqual({ id: 123, gender: 'female', isActive: true }); @@ -343,7 +327,7 @@ describe('AutoCompleteEditor', () => { }; mockItemData = { id: 123, gender: 'female', isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.applyValue(mockItemData, 'female'); expect(mockItemData).toEqual({ id: 123, gender: '', isActive: true }); @@ -355,7 +339,7 @@ describe('AutoCompleteEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { forceUserInput: true, }; mockItemData = { id: 123, gender: { value: 'male', label: 'Male' }, isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue(mockItemData); editor.setValue('Female'); const output = editor.serializeValue(); @@ -364,10 +348,10 @@ describe('AutoCompleteEditor', () => { }); it('should return DOM element value when "forceUserInput" is enabled and loaded value length is greater then custom minLength defined when calling "serializeValue"', () => { - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { forceUserInput: true, minLength: 2 } as AutocompleteOption; + (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { forceUserInput: true, minLength: 2 } as AutocompleterOption; mockItemData = { id: 123, gender: { value: 'male', label: 'Male' }, isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue(mockItemData); editor.setValue('Female'); const output = editor.serializeValue(); @@ -376,10 +360,10 @@ describe('AutoCompleteEditor', () => { }); it('should return loaded value when "forceUserInput" is enabled and loaded value length is lower than minLength defined when calling "serializeValue"', () => { - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { forceUserInput: true, } as AutocompleteOption; + (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { forceUserInput: true, } as AutocompleterOption; mockItemData = { id: 123, gender: { value: 'male', label: 'Male' }, isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue(mockItemData); editor.setValue('F'); const output = editor.serializeValue(); @@ -388,50 +372,13 @@ describe('AutoCompleteEditor', () => { }); }); - describe('openSearchListOnFocus flag', () => { - it('should open the search list by calling the AutoComplete "search" event with an empty string when there are no search term provided', () => { - (mockColumn.internalColumnEditor as ColumnEditor).collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }]; - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { openSearchListOnFocus: true, } as AutocompleteOption; - - const event = new (window.window as any).KeyboardEvent('click', { keyCode: KeyCode.LEFT, bubbles: true, cancelable: true }); - - editor = new AutoCompleteEditor(editorArguments); - const autoCompleteSpy = jest.spyOn(editor.editorDomElement, 'autocomplete'); - - const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; - editorElm.focus(); - editorElm.dispatchEvent(event); - - expect(autoCompleteSpy).toHaveBeenCalledWith('search', ''); - }); - - it('should open the search list by calling the AutoComplete "search" event with the same search term string that was provided', () => { - (mockColumn.internalColumnEditor as ColumnEditor).collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }]; - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { openSearchListOnFocus: true, } as AutocompleteOption; - mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true }; - - const event = new (window.window as any).KeyboardEvent('click', { keyCode: KeyCode.LEFT, bubbles: true, cancelable: true }); - - editor = new AutoCompleteEditor(editorArguments); - const autoCompleteSpy = jest.spyOn(editor.editorDomElement, 'autocomplete'); - editor.loadValue(mockItemData); - editor.setValue('Female'); - - const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; - editorElm.focus(); - editorElm.dispatchEvent(event); - - expect(autoCompleteSpy).toHaveBeenCalledWith('search', 'Female'); - }); - }); - describe('serializeValue method', () => { it('should return correct object value even when defining a "customStructure" when calling "serializeValue"', () => { (mockColumn.internalColumnEditor as ColumnEditor).collection = [{ option: 'male', text: 'Male' }, { option: 'female', text: 'Female' }]; (mockColumn.internalColumnEditor as ColumnEditor).customStructure = { value: 'option', label: 'text' }; mockItemData = { id: 123, gender: { option: 'female', text: 'Female' }, isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue(mockItemData); const output = editor.serializeValue(); @@ -443,7 +390,7 @@ describe('AutoCompleteEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }]; mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue(mockItemData); const output = editor.serializeValue(); @@ -457,7 +404,7 @@ describe('AutoCompleteEditor', () => { mockColumn.labelKey = 'name'; mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue(mockItemData); const output = editor.serializeValue(); @@ -470,7 +417,7 @@ describe('AutoCompleteEditor', () => { gridOptionMock.autoCommitEdit = true; const spy = jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit'); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.setValue('a'); editor.save(); @@ -481,7 +428,7 @@ describe('AutoCompleteEditor', () => { gridOptionMock.autoCommitEdit = false; const spy = jest.spyOn(editorArguments, 'commitChanges'); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.setValue('a'); editor.save(); @@ -492,7 +439,7 @@ describe('AutoCompleteEditor', () => { describe('validate method', () => { it('should return False when field is required and field is empty', () => { (mockColumn.internalColumnEditor as ColumnEditor).required = true; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, ''); expect(validation).toEqual({ valid: false, msg: 'Field is required' }); @@ -500,7 +447,7 @@ describe('AutoCompleteEditor', () => { it('should return True when field is required and input is a valid input value', () => { (mockColumn.internalColumnEditor as ColumnEditor).required = true; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text'); expect(validation).toEqual({ valid: true, msg: '' }); @@ -508,7 +455,7 @@ describe('AutoCompleteEditor', () => { it('should return False when field is lower than a minLength defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 5; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is at least 5 character(s)' }); @@ -517,7 +464,7 @@ describe('AutoCompleteEditor', () => { it('should return False when field is lower than a minLength defined using exclusive operator', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 5; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'exclusive'; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is more than 5 character(s)' }); @@ -525,7 +472,7 @@ describe('AutoCompleteEditor', () => { it('should return True when field is equal to the minLength defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 4; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text'); expect(validation).toEqual({ valid: true, msg: '' }); @@ -533,7 +480,7 @@ describe('AutoCompleteEditor', () => { it('should return False when field is greater than a maxLength defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 10; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text is 16 chars'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is less than or equal to 10 characters' }); @@ -542,7 +489,7 @@ describe('AutoCompleteEditor', () => { it('should return False when field is greater than a maxLength defined using exclusive operator', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 10; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'exclusive'; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text is 16 chars'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is less than 10 characters' }); @@ -550,7 +497,7 @@ describe('AutoCompleteEditor', () => { it('should return True when field is equal to the maxLength defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 16; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text is 16 chars'); expect(validation).toEqual({ valid: true, msg: '' }); @@ -559,7 +506,7 @@ describe('AutoCompleteEditor', () => { it('should return True when field is equal to the maxLength defined and "operatorType" is set to "inclusive"', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 16; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'inclusive'; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text is 16 chars'); expect(validation).toEqual({ valid: true, msg: '' }); @@ -568,7 +515,7 @@ describe('AutoCompleteEditor', () => { it('should return False when field is equal to the maxLength defined but "operatorType" is set to "exclusive"', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 16; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'exclusive'; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text is 16 chars'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is less than 16 characters' }); @@ -577,7 +524,7 @@ describe('AutoCompleteEditor', () => { it('should return False when field is not between minLength & maxLength defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 0; (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 10; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text is 16 chars'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text length is between 0 and 10 characters' }); @@ -586,7 +533,7 @@ describe('AutoCompleteEditor', () => { it('should return True when field is is equal to maxLength defined when both min/max values are defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 0; (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 16; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text is 16 chars'); expect(validation).toEqual({ valid: true, msg: '' }); @@ -596,7 +543,7 @@ describe('AutoCompleteEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 4; (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 15; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'inclusive'; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'text'); expect(validation).toEqual({ valid: true, msg: '' }); @@ -606,7 +553,7 @@ describe('AutoCompleteEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 4; (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 16; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'exclusive'; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation1 = editor.validate(null, 'text is 16 chars'); const validation2 = editor.validate(null, 'text'); @@ -616,7 +563,7 @@ describe('AutoCompleteEditor', () => { it('should return False when field is greater than a maxValue defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 10; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const validation = editor.validate(null, 'Task is longer than 10 chars'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is less than or equal to 10 characters' }); @@ -628,71 +575,71 @@ describe('AutoCompleteEditor', () => { jest.clearAllMocks(); }); - it('should expect "setValue" to have been called but not "autoCommitEdit" when "autoCommitEdit" is disabled', () => { - const commitEditSpy = jest.spyOn(gridStub, 'getEditorLock'); - gridOptionMock.autoCommitEdit = false; - (mockColumn.internalColumnEditor as ColumnEditor).collection = ['male', 'female']; - mockItemData = { id: 123, gender: 'female', isActive: true }; - - editor = new AutoCompleteEditor(editorArguments); - const spySetValue = jest.spyOn(editor, 'setValue'); - const output = editor.handleSelect(null as any, { item: mockItemData.gender }); - - expect(output).toBe(false); - expect(commitEditSpy).not.toHaveBeenCalled(); - expect(spySetValue).toHaveBeenCalledWith('female'); - expect(editor.isValueTouched()).toBe(true); - }); - - it('should expect "setValue" and "autoCommitEdit" to have been called with a string when item provided is a string', () => { - const commitEditSpy = jest.spyOn(gridStub, 'getEditorLock'); - gridOptionMock.autoCommitEdit = true; - (mockColumn.internalColumnEditor as ColumnEditor).collection = ['male', 'female']; - mockItemData = { id: 123, gender: 'female', isActive: true }; - - editor = new AutoCompleteEditor(editorArguments); - const spySetValue = jest.spyOn(editor, 'setValue'); - const output = editor.handleSelect(null as any, { item: mockItemData.gender }); - - // HOW DO WE TRIGGER the jQuery UI autocomplete select event? The following works only on "autocompleteselect" - // but that doesn't trigger the "select" (handleSelect) directly - // const editorElm = editor.editorDomElement; - // editorElm.on('autocompleteselect', (event, ui) => console.log(ui)); - // editorElm[0].dispatchEvent(new (window.window as any).CustomEvent('autocompleteselect', { detail: { item: 'female' }, bubbles: true, cancelable: true })); - jest.runAllTimers(); // fast-forward timer - - expect(output).toBe(false); - expect(commitEditSpy).toHaveBeenCalled(); - expect(spySetValue).toHaveBeenCalledWith('female'); - expect(editor.isValueTouched()).toBe(true); - }); - - it('should expect "setValue" and "autoCommitEdit" to have been called with the string label when item provided is an object', () => { - const commitEditSpy = jest.spyOn(gridStub, 'getEditorLock'); - gridOptionMock.autoCommitEdit = true; - (mockColumn.internalColumnEditor as ColumnEditor).collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }]; - mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true }; - - editor = new AutoCompleteEditor(editorArguments); - const spySetValue = jest.spyOn(editor, 'setValue'); - const output = editor.handleSelect(null as any, { item: mockItemData.gender }); - - expect(output).toBe(false); - expect(commitEditSpy).toHaveBeenCalled(); - expect(spySetValue).toHaveBeenCalledWith('Female'); - expect(editor.isValueTouched()).toBe(true); - }); + // xit('should expect "setValue" to have been called but not "autoCommitEdit" when "autoCommitEdit" is disabled', () => { + // const commitEditSpy = jest.spyOn(gridStub, 'getEditorLock'); + // gridOptionMock.autoCommitEdit = false; + // (mockColumn.internalColumnEditor as ColumnEditor).collection = ['male', 'female']; + // mockItemData = { id: 123, gender: 'female', isActive: true }; + + // editor = new AutocompleterEditor(editorArguments); + // const spySetValue = jest.spyOn(editor, 'setValue'); + // const output = editor.handleSelect({ item: mockItemData.gender }); + + // expect(output).toBe(false); + // expect(commitEditSpy).not.toHaveBeenCalled(); + // expect(spySetValue).toHaveBeenCalledWith('female'); + // expect(editor.isValueTouched()).toBe(true); + // }); + + // xit('should expect "setValue" and "autoCommitEdit" to have been called with a string when item provided is a string', () => { + // const commitEditSpy = jest.spyOn(gridStub, 'getEditorLock'); + // gridOptionMock.autoCommitEdit = true; + // (mockColumn.internalColumnEditor as ColumnEditor).collection = ['male', 'female']; + // mockItemData = { id: 123, gender: 'female', isActive: true }; + + // editor = new AutocompleterEditor(editorArguments); + // const spySetValue = jest.spyOn(editor, 'setValue'); + // const output = editor.handleSelect({ item: mockItemData.gender }); + + // // HOW DO WE TRIGGER the jQuery UI autocomplete select event? The following works only on "autocompleteselect" + // // but that doesn't trigger the "select" (handleSelect) directly + // // const editorElm = editor.editorDomElement; + // // editorElm.on('autocompleteselect', (event, ui) => console.log(ui)); + // // editorElm[0].dispatchEvent(new (window.window as any).CustomEvent('autocompleteselect', { detail: { item: 'female' }, bubbles: true, cancelable: true })); + // jest.runAllTimers(); // fast-forward timer + + // expect(output).toBe(false); + // expect(commitEditSpy).toHaveBeenCalled(); + // expect(spySetValue).toHaveBeenCalledWith('female'); + // expect(editor.isValueTouched()).toBe(true); + // }); + + // xit('should expect "setValue" and "autoCommitEdit" to have been called with the string label when item provided is an object', () => { + // const commitEditSpy = jest.spyOn(gridStub, 'getEditorLock'); + // gridOptionMock.autoCommitEdit = true; + // (mockColumn.internalColumnEditor as ColumnEditor).collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }]; + // mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true }; + + // editor = new AutocompleterEditor(editorArguments); + // const spySetValue = jest.spyOn(editor, 'setValue'); + // const output = editor.handleSelect({ item: mockItemData.gender }); + + // expect(output).toBe(false); + // expect(commitEditSpy).toHaveBeenCalled(); + // expect(spySetValue).toHaveBeenCalledWith('Female'); + // expect(editor.isValueTouched()).toBe(true); + // }); it('should expect the "handleSelect" method to be called when the callback method is triggered when user provide his own filterOptions', () => { - gridOptionMock.autoCommitEdit = true; - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { source: [], minLength: 3 } as AutocompleteOption; - - const event = new CustomEvent('change'); - editor = new AutoCompleteEditor(editorArguments); - const spy = jest.spyOn(editor, 'handleSelect'); - editor.autoCompleteOptions.select!(event, { item: 'fem' }); - - expect(spy).toHaveBeenCalledWith(event, { item: 'fem' }); + editor = new AutocompleterEditor(editorArguments); + const selectSpy = jest.spyOn(editor, 'handleSelect'); + const setValueSpy = jest.spyOn(editor, 'setValue'); + const saveSpy = jest.spyOn(editor, 'save'); + editor.autocompleterOptions.onSelect!({ item: 'fem' }, editor.editorDomElement); + + expect(setValueSpy).toHaveBeenCalled(); + expect(saveSpy).toHaveBeenCalled(); + // expect(spy).toHaveBeenCalledWith({ item: 'fem' }); expect(editor.isValueTouched()).toBe(true); }); @@ -701,29 +648,31 @@ describe('AutoCompleteEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }]; mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true }; - const event = new CustomEvent('change'); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const spy = jest.spyOn(editor, 'handleSelect'); - editor.autoCompleteOptions.select!(event, { item: 'fem' }); + const saveSpy = jest.spyOn(editor, 'save'); + editor.autocompleterOptions.onSelect!({ item: 'fem' }, editor.editorDomElement); - expect(spy).toHaveBeenCalledWith(event, { item: 'fem' }); + // expect(spy).toHaveBeenCalledWith(event, { item: 'fem' }); + expect(saveSpy).toHaveBeenCalled(); expect(editor.isValueTouched()).toBe(true); }); it('should initialize the editor with editorOptions and expect the "handleSelect" method to be called when the callback method is triggered', () => { gridOptionMock.autoCommitEdit = true; (mockColumn.internalColumnEditor as ColumnEditor).collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }]; - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleteOption; + (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleterOption; mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true }; - const event = new CustomEvent('change'); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const handleSelectSpy = jest.spyOn(editor, 'handleSelect'); const focusSpy = jest.spyOn(editor, 'focus'); - editor.autoCompleteOptions.select!(event, { item: 'fem' }); + const saveSpy = jest.spyOn(editor, 'save'); + editor.autocompleterOptions.onSelect!({ item: 'fem' }, editor.editorDomElement); jest.runAllTimers(); // fast-forward timer - expect(handleSelectSpy).toHaveBeenCalledWith(event, { item: 'fem' }); + // expect(handleSelectSpy).toHaveBeenCalledWith({ item: 'fem' }); + expect(saveSpy).toHaveBeenCalled(); expect(focusSpy).toHaveBeenCalled(); expect(editor.isValueTouched()).toBe(true); }); @@ -733,83 +682,124 @@ describe('AutoCompleteEditor', () => { const mockOnSelect = jest.fn(); const activeCellMock = { row: 1, cell: 0 }; jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(activeCellMock); - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { source: [], minLength: 3, onSelect: mockOnSelect } as AutocompleteOption; + (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3, onSelectItem: mockOnSelect } as AutocompleterOption; const event = new CustomEvent('change'); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); + const saveSpy = jest.spyOn(editor, 'save'); const handleSelectSpy = jest.spyOn(editor, 'handleSelect'); - editor.autoCompleteOptions.select!(event, { item: 'fem' }); + editor.autocompleterOptions.onSelect!({ item: 'fem' }, editor.editorDomElement); - expect(handleSelectSpy).toHaveBeenCalledWith(event, { item: 'fem' }); - expect(mockOnSelect).toHaveBeenCalledWith(event, { item: 'fem' }, activeCellMock.row, activeCellMock.cell, mockColumn, mockItemData); + // expect(saveSpy).toHaveBeenCalled(); + // expect(handleSelectSpy).toHaveBeenCalledWith(event, { item: 'fem' }); + expect(mockOnSelect).toHaveBeenCalledWith({ item: 'fem' }, activeCellMock.row, activeCellMock.cell, mockColumn, mockItemData); expect(editor.isValueTouched()).toBe(true); }); }); describe('renderItem callback method', () => { - it('should be able to override any jQuery UI callback method', () => { - const mockCallback = (ul: HTMLElement, item: any) => { - return $('
  • ') - .data('item.autocomplete', item) - .append(`
    Hello World`) - .appendTo(ul); - }; - mockColumn.internalColumnEditor = { - editorOptions: { - source: [], - classes: { 'ui-autocomplete': 'autocomplete-custom-four-corners' }, - } as AutocompleteOption, - callbacks: { _renderItem: mockCallback }, - }; - - editor = new AutoCompleteEditor(editorArguments); - - expect(editor.instance).toBeTruthy(); - expect(editor.instance._renderItem).toEqual(mockCallback); - }); - - it('should provide "renderItem" in the "filterOptions" and expect the jQueryUI "_renderItem" to be overriden', () => { + it('should provide "renderItem" in the "filterOptions" and expect the autocomplete "render" to be overriden', () => { const mockTemplateString = `
    Hello World
    `; const mockTemplateCallback = () => mockTemplateString; mockColumn.internalColumnEditor = { + collection: ['male', 'female'], editorOptions: { - source: [], + showOnFocus: true, renderItem: { layout: 'fourCorners', templateCallback: mockTemplateCallback }, - } as AutocompleteOption, + } as AutocompleterOption }; const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: KeyCode.LEFT, bubbles: true, cancelable: true }); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; - const autoCompleteSpy = jest.spyOn(editor.editorDomElement, 'autocomplete'); editorElm.focus(); editorElm.dispatchEvent(event); + jest.runAllTimers(); // fast-forward timer + + const autocompleteListElms = document.body.querySelectorAll('.autocomplete-custom-four-corners'); expect(editor.editorDomElement).toBeTruthy(); expect(editor.instance).toBeTruthy(); - expect(editor.autoCompleteOptions).toEqual(expect.objectContaining({ classes: { 'ui-autocomplete': 'autocomplete-custom-four-corners' } })); - expect(autoCompleteSpy).toHaveBeenCalledWith('instance'); - expect(editor.instance._renderItem).toEqual(expect.any(Function)); - - const ulElm = document.createElement('ul'); - editor.instance._renderItem(ulElm, { name: 'John' }); - - const liElm = ulElm.querySelector('li') as HTMLLIElement; - expect(liElm.innerHTML).toBe(mockTemplateString); + expect(editor.autocompleterOptions.render).toEqual(expect.any(Function)); + expect(autocompleteListElms.length).toBe(1); + expect(autocompleteListElms[0].innerHTML).toContain(mockTemplateString); }); }); it('should call "clear" method and expect the DOM element to become blank & untouched', () => { - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const saveSpy = jest.spyOn(editor, 'save'); editor.loadValue({ ...mockItemData, gender: 'male' }); editor.show(); editor.clear(); expect(saveSpy).toHaveBeenCalled(); - expect(editor.editorDomElement.val()).toEqual(''); + expect(editor.editorDomElement.value).toEqual(''); + }); + + it('should add custom "fetch" call and expect "renderCollectionItem" callback be called when focusing on the autocomplete input', async () => { + const mockCollection = [{ value: 'male', label: 'Male' }, { value: 'unknown', label: 'Unknown' }]; + const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: 109, bubbles: true, cancelable: true }); + + mockColumn.internalColumnEditor = { + collection: mockCollection, + editorOptions: { showOnFocus: true } as AutocompleterOption + }; + editor = new AutocompleterEditor(editorArguments); + + const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; + editorElm.focus(); + editorElm.dispatchEvent(event); + + jest.runAllTimers(); // fast-forward timer + + const autocompleteListElms = document.body.querySelectorAll('.slick-autocomplete div'); + expect(autocompleteListElms.length).toBe(2); + expect(autocompleteListElms[0].textContent).toBe('Male'); + expect(autocompleteListElms[1].textContent).toBe('Unknown'); + }); + + it('should call "clear" method when clear button is clicked', () => { + const mockCollection = [{ value: 'male', label: 'Male' }, { value: 'unknown', label: 'Unknown' }]; + mockColumn.internalColumnEditor = { + collection: mockCollection, + editorOptions: { showOnFocus: true } as AutocompleterOption + }; + editor = new AutocompleterEditor(editorArguments); + const clearSpy = jest.spyOn(editor, 'clear'); + + const clearBtnElm = divContainer.querySelector('.btn.icon-clear') as HTMLButtonElement; + clearBtnElm.dispatchEvent(new Event('click')); + + expect(clearSpy).toHaveBeenCalled(); + }); + + it('should add custom "fetch" call and expect "renderRegularItem" callback be called when focusing on the autocomplete input', async () => { + const mockCollection = [{ value: 'female', label: 'Female' }, { value: 'undefined', label: 'Undefined' }]; + const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: 109, bubbles: true, cancelable: true }); + + mockColumn.internalColumnEditor = { + editorOptions: { + showOnFocus: true, + fetch: (searchText, updateCallback) => { + updateCallback(mockCollection); + } + } as AutocompleterOption + }; + editor = new AutocompleterEditor(editorArguments); + + const editorElm = divContainer.querySelector('input.editor-gender') as HTMLInputElement; + editorElm.focus(); + editorElm.dispatchEvent(event); + + jest.runAllTimers(); // fast-forward timer + + const autocompleteListElms = document.body.querySelectorAll('.slick-autocomplete div'); + expect(autocompleteListElms.length).toBe(2); + expect(autocompleteListElms[0].textContent).toBe('Female'); + expect(autocompleteListElms[1].textContent).toBe('Undefined'); }); }); @@ -829,7 +819,7 @@ describe('AutoCompleteEditor', () => { const activeCellMock = { row: 0, cell: 0 }; jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(activeCellMock); const onCompositeEditorSpy = jest.spyOn(gridStub.onCompositeEditorChange, 'notify').mockReturnValue(false); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.setValue({ value: 'male', label: 'Male' }, true); expect(editor.getValue()).toBe('Male'); @@ -844,7 +834,7 @@ describe('AutoCompleteEditor', () => { const getCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(activeCellMock); const onBeforeEditSpy = jest.spyOn(gridStub.onBeforeEditCell, 'notify').mockReturnValue(undefined); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const disableSpy = jest.spyOn(editor, 'disable'); editor.show(); @@ -859,7 +849,7 @@ describe('AutoCompleteEditor', () => { const onBeforeEditSpy = jest.spyOn(gridStub.onBeforeEditCell, 'notify').mockReturnValue(false); const onCompositeEditorSpy = jest.spyOn(gridStub.onCompositeEditorChange, 'notify').mockReturnValue(false); - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue(mockItemData); const disableSpy = jest.spyOn(editor, 'disable'); editor.show(); @@ -871,8 +861,8 @@ describe('AutoCompleteEditor', () => { formValues: { gender: '' }, editors: {}, triggeredBy: 'user', }, expect.anything()); expect(disableSpy).toHaveBeenCalledWith(true); - expect(editor.editorDomElement.attr('disabled')).toEqual('disabled'); - expect(editor.editorDomElement.val()).toEqual(''); + expect(editor.editorDomElement.disabled).toBe(true); + expect(editor.editorDomElement.value).toEqual(''); }); it('should call "show" and expect the DOM element to become disabled and empty when "onBeforeEditCell" returns false and also expect "onBeforeComposite" to not be called because the value is blank', () => { @@ -884,7 +874,7 @@ describe('AutoCompleteEditor', () => { excludeDisabledFieldFormValues: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue(mockItemData); const disableSpy = jest.spyOn(editor, 'disable'); editor.show(); @@ -893,8 +883,8 @@ describe('AutoCompleteEditor', () => { expect(onBeforeEditSpy).toHaveBeenCalledWith({ ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub, target: 'composite', compositeEditorOptions: editorArguments.compositeEditorOptions }); expect(onCompositeEditorSpy).not.toHaveBeenCalled(); expect(disableSpy).toHaveBeenCalledWith(true); - expect(editor.editorDomElement.attr('disabled')).toEqual('disabled'); - expect(editor.editorDomElement.val()).toEqual(''); + expect(editor.editorDomElement.disabled).toBe(true); + expect(editor.editorDomElement.value).toEqual(''); }); it('should call "disable" method and expect the DOM element to become disabled and have an empty formValues be passed in the onCompositeEditorChange event', () => { @@ -905,7 +895,7 @@ describe('AutoCompleteEditor', () => { excludeDisabledFieldFormValues: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue({ ...mockItemData, gender: 'male' }); editor.show(); editor.disable(); @@ -915,8 +905,8 @@ describe('AutoCompleteEditor', () => { ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub, formValues: {}, editors: {}, triggeredBy: 'user', }, expect.anything()); - expect(editor.editorDomElement.attr('disabled')).toEqual('disabled'); - expect(editor.editorDomElement.val()).toEqual(''); + expect(editor.editorDomElement.disabled).toBe(true); + expect(editor.editorDomElement.value).toEqual(''); }); it('should call "reset" method and expect the DOM element to become blank & untouched and have an empty formValues be passed in the onCompositeEditorChange event', () => { @@ -927,7 +917,7 @@ describe('AutoCompleteEditor', () => { excludeDisabledFieldFormValues: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.loadValue({ ...mockItemData, gender: 'male' }); editor.show(); editor.reset(''); @@ -937,8 +927,8 @@ describe('AutoCompleteEditor', () => { ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub, formValues: {}, editors: {}, triggeredBy: 'user', }, expect.anything()); - expect(editor.editorDomElement.attr('disabled')).toEqual('disabled'); - expect(editor.editorDomElement.val()).toEqual(''); + expect(editor.editorDomElement.disabled).toBe(true); + expect(editor.editorDomElement.value).toEqual(''); }); it('should expect "setValue" to have been called and also "onCompositeEditorChange" to have been triggered with the new value showing up in its "formValues" object', () => { @@ -950,9 +940,9 @@ describe('AutoCompleteEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).collection = ['male', 'female']; mockItemData = { id: 123, gender: 'female', isActive: true }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); const spySetValue = jest.spyOn(editor, 'setValue'); - const output = editor.handleSelect(null as any, { item: mockItemData.gender }); + const output = editor.handleSelect(mockItemData.gender); expect(output).toBe(false); expect(spySetValue).toHaveBeenCalledWith('female'); @@ -973,7 +963,7 @@ describe('AutoCompleteEditor', () => { collection: ['Other', 'Male', 'Female'], collectionOverride: (inputCollection) => inputCollection.filter(item => item !== 'other') }; - editor = new AutoCompleteEditor(editorArguments); + editor = new AutocompleterEditor(editorArguments); editor.setValue('Male', true); expect(editor.getValue()).toBe('Male'); diff --git a/packages/common/src/editors/__tests__/checkboxEditor.spec.ts b/packages/common/src/editors/__tests__/checkboxEditor.spec.ts index df58fffdf..39884d5eb 100644 --- a/packages/common/src/editors/__tests__/checkboxEditor.spec.ts +++ b/packages/common/src/editors/__tests__/checkboxEditor.spec.ts @@ -1,6 +1,6 @@ import { Editors } from '../index'; import { CheckboxEditor } from '../checkboxEditor'; -import { AutocompleteOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; +import { AutocompleterOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; declare const Slick: SlickNamespace; const KEY_CHAR_SPACE = 32; @@ -100,7 +100,7 @@ describe('CheckboxEditor', () => { }); it('should initialize the editor even when user define his own editor options', () => { - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleteOption; + (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleterOption; editor = new CheckboxEditor(editorArguments); const editorCount = divContainer.querySelectorAll('input.editor-checkbox.editor-isActive').length; diff --git a/packages/common/src/editors/__tests__/inputEditor.spec.ts b/packages/common/src/editors/__tests__/inputEditor.spec.ts index ab85ff477..2e065e0c1 100644 --- a/packages/common/src/editors/__tests__/inputEditor.spec.ts +++ b/packages/common/src/editors/__tests__/inputEditor.spec.ts @@ -1,7 +1,7 @@ import { Editors } from '../index'; import { InputEditor } from '../inputEditor'; import { KeyCode } from '../../enums/index'; -import { AutocompleteOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; +import { AutocompleterOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; declare const Slick: SlickNamespace; jest.useFakeTimers(); @@ -116,7 +116,7 @@ describe('InputEditor (TextEditor)', () => { }); it('should initialize the editor even when user define his own editor options', () => { - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleteOption; + (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleterOption; editor = new InputEditor(editorArguments, 'text'); const editorCount = divContainer.querySelectorAll('input.editor-text.editor-title').length; diff --git a/packages/common/src/editors/__tests__/inputPasswordEditor.spec.ts b/packages/common/src/editors/__tests__/inputPasswordEditor.spec.ts index 8cb3aa7a1..511aa8055 100644 --- a/packages/common/src/editors/__tests__/inputPasswordEditor.spec.ts +++ b/packages/common/src/editors/__tests__/inputPasswordEditor.spec.ts @@ -1,7 +1,7 @@ import { Editors } from '../index'; import { InputPasswordEditor } from '../inputPasswordEditor'; import { KeyCode } from '../../enums/index'; -import { AutocompleteOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; +import { AutocompleterOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; declare const Slick: SlickNamespace; jest.useFakeTimers(); @@ -116,7 +116,7 @@ describe('InputPasswordEditor', () => { }); it('should initialize the editor even when user define his own editor options', () => { - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleteOption; + (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleterOption; editor = new InputPasswordEditor(editorArguments); const editorCount = divContainer.querySelectorAll('input.editor-text.editor-title').length; diff --git a/packages/common/src/editors/__tests__/longTextEditor.spec.ts b/packages/common/src/editors/__tests__/longTextEditor.spec.ts index 8e099d0e3..dc7bd43cb 100644 --- a/packages/common/src/editors/__tests__/longTextEditor.spec.ts +++ b/packages/common/src/editors/__tests__/longTextEditor.spec.ts @@ -1,7 +1,7 @@ import { Editors } from '../index'; import { LongTextEditor } from '../longTextEditor'; import { KeyCode } from '../../enums/index'; -import { AutocompleteOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; +import { AutocompleterOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; import * as domUtilities from '../../services/domUtilities'; const mockGetHtmlElementOffset = jest.fn(); @@ -150,7 +150,7 @@ describe('LongTextEditor', () => { }); it('should initialize the editor even when user define his own editor options', () => { - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleteOption; + (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleterOption; editor = new LongTextEditor(editorArguments); const editorCount = document.body.querySelectorAll('.slick-large-editor-text.editor-title textarea').length; @@ -596,7 +596,7 @@ describe('LongTextEditor', () => { it('should return False when field is required and field is empty', () => { (mockColumn.internalColumnEditor as ColumnEditor).required = true; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, ''); + const validation = editor.validate(undefined, ''); expect(validation).toEqual({ valid: false, msg: 'Field is required' }); }); @@ -604,7 +604,7 @@ describe('LongTextEditor', () => { it('should return True when field is required and input is a valid input value', () => { (mockColumn.internalColumnEditor as ColumnEditor).required = true; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text'); + const validation = editor.validate(undefined, 'text'); expect(validation).toEqual({ valid: true, msg: '' }); }); @@ -612,7 +612,7 @@ describe('LongTextEditor', () => { it('should return False when field is lower than a minLength defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 5; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text'); + const validation = editor.validate(undefined, 'text'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is at least 5 character(s)' }); }); @@ -621,7 +621,7 @@ describe('LongTextEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 5; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'exclusive'; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text'); + const validation = editor.validate(undefined, 'text'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is more than 5 character(s)' }); }); @@ -629,7 +629,7 @@ describe('LongTextEditor', () => { it('should return True when field is equal to the minLength defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 4; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text'); + const validation = editor.validate(undefined, 'text'); expect(validation).toEqual({ valid: true, msg: '' }); }); @@ -637,7 +637,7 @@ describe('LongTextEditor', () => { it('should return False when field is greater than a maxLength defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 10; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text is 16 chars'); + const validation = editor.validate(undefined, 'text is 16 chars'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is less than or equal to 10 characters' }); }); @@ -646,7 +646,7 @@ describe('LongTextEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 10; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'exclusive'; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text is 16 chars'); + const validation = editor.validate(undefined, 'text is 16 chars'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is less than 10 characters' }); }); @@ -654,7 +654,7 @@ describe('LongTextEditor', () => { it('should return True when field is equal to the maxLength defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 16; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text is 16 chars'); + const validation = editor.validate(undefined, 'text is 16 chars'); expect(validation).toEqual({ valid: true, msg: '' }); }); @@ -663,7 +663,7 @@ describe('LongTextEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 16; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'inclusive'; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text is 16 chars'); + const validation = editor.validate(undefined, 'text is 16 chars'); expect(validation).toEqual({ valid: true, msg: '' }); }); @@ -672,7 +672,7 @@ describe('LongTextEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 16; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'exclusive'; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text is 16 chars'); + const validation = editor.validate(undefined, 'text is 16 chars'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is less than 16 characters' }); }); @@ -681,7 +681,7 @@ describe('LongTextEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 0; (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 10; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text is 16 chars'); + const validation = editor.validate(undefined, 'text is 16 chars'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text length is between 0 and 10 characters' }); }); @@ -690,7 +690,7 @@ describe('LongTextEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).minLength = 0; (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 16; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text is 16 chars'); + const validation = editor.validate(undefined, 'text is 16 chars'); expect(validation).toEqual({ valid: true, msg: '' }); }); @@ -700,7 +700,7 @@ describe('LongTextEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 15; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'inclusive'; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'text'); + const validation = editor.validate(undefined, 'text'); expect(validation).toEqual({ valid: true, msg: '' }); }); @@ -710,8 +710,8 @@ describe('LongTextEditor', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 16; (mockColumn.internalColumnEditor as ColumnEditor).operatorConditionalType = 'exclusive'; editor = new LongTextEditor(editorArguments); - const validation1 = editor.validate(null, 'text is 16 chars'); - const validation2 = editor.validate(null, 'text'); + const validation1 = editor.validate(undefined, 'text is 16 chars'); + const validation2 = editor.validate(undefined, 'text'); expect(validation1).toEqual({ valid: false, msg: 'Please make sure your text length is between 4 and 16 characters' }); expect(validation2).toEqual({ valid: false, msg: 'Please make sure your text length is between 4 and 16 characters' }); @@ -720,7 +720,7 @@ describe('LongTextEditor', () => { it('should return False when field is greater than a maxValue defined', () => { (mockColumn.internalColumnEditor as ColumnEditor).maxLength = 10; editor = new LongTextEditor(editorArguments); - const validation = editor.validate(null, 'Task is longer than 10 chars'); + const validation = editor.validate(undefined, 'Task is longer than 10 chars'); expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is less than or equal to 10 characters' }); }); diff --git a/packages/common/src/editors/__tests__/selectEditor.spec.ts b/packages/common/src/editors/__tests__/selectEditor.spec.ts index 425034916..e21763d19 100644 --- a/packages/common/src/editors/__tests__/selectEditor.spec.ts +++ b/packages/common/src/editors/__tests__/selectEditor.spec.ts @@ -4,7 +4,7 @@ import 'multiple-select-modified'; import { Editors } from '../index'; import { SelectEditor } from '../selectEditor'; import { FieldType, OperatorType } from '../../enums/index'; -import { AutocompleteOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; +import { AutocompleterOption, Column, ColumnEditor, EditorArguments, GridOption, SlickDataView, SlickGrid, SlickNamespace } from '../../interfaces/index'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; declare const Slick: SlickNamespace; @@ -167,7 +167,7 @@ describe('SelectEditor', () => { }); it('should initialize the editor even when user define his own editor options', () => { - (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleteOption; + (mockColumn.internalColumnEditor as ColumnEditor).editorOptions = { minLength: 3 } as AutocompleterOption; editor = new SelectEditor(editorArguments, true); const editorCount = document.body.querySelectorAll('select.ms-filter.editor-gender').length; diff --git a/packages/common/src/editors/autoCompleteEditor.ts b/packages/common/src/editors/autocompleterEditor.ts similarity index 59% rename from packages/common/src/editors/autoCompleteEditor.ts rename to packages/common/src/editors/autocompleterEditor.ts index ce2549169..437b4c1c0 100644 --- a/packages/common/src/editors/autoCompleteEditor.ts +++ b/packages/common/src/editors/autocompleterEditor.ts @@ -1,9 +1,14 @@ -import { setDeepValue, toKebabCase } from '@slickgrid-universal/utils'; -import 'jquery-ui/ui/widgets/autocomplete'; +import * as autocompleter_ from 'autocompleter'; +const autocomplete = (autocompleter_ && autocompleter_['default'] || autocompleter_) as (settings: AutocompleteSettings) => AutocompleteResult; // patch for rollup +import { AutocompleteItem, AutocompleteResult, AutocompleteSettings } from 'autocompleter'; +import { isObject, isPrimmitive, setDeepValue, toKebabCase } from '@slickgrid-universal/utils'; + +import { Constants } from './../constants'; import { FieldType, KeyCode, } from '../enums/index'; import { - AutocompleteOption, + AutocompleterOption, + AutocompleteSearchItem, CollectionCustomStructure, CollectionOverrideArgs, Column, @@ -16,10 +21,14 @@ import { GridOption, SlickGrid, SlickNamespace, -} from './../interfaces/index'; + Locale, +} from '../interfaces/index'; import { textValidator } from '../editorValidators/textValidator'; -import { sanitizeTextByAvailableSanitizer, } from '../services/domUtilities'; +import { addAutocompleteLoadingByOverridingFetch } from '../commonEditorFilter'; +import { createDomElement, sanitizeTextByAvailableSanitizer, } from '../services/domUtilities'; import { findOrDefault, getDescendantProperty, } from '../services/utilities'; +import { BindingEventService } from '../services/bindingEvent.service'; +import { TranslaterService } from '../services/translater.service'; // minimum length of chars to type before starting to start querying const MIN_LENGTH = 3; @@ -31,20 +40,27 @@ declare const Slick: SlickNamespace; * An example of a 'detached' editor. * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. */ -export class AutoCompleteEditor implements Editor { - protected _autoCompleteOptions!: AutocompleteOption; +export class AutocompleterEditor implements Editor { + protected _autocompleterOptions!: Partial>; + protected _bindEventService: BindingEventService; protected _currentValue: any; protected _defaultTextValue!: string; protected _originalValue: any; - protected _elementCollection!: any[] | null; + protected _elementCollection!: T[] | null; + protected _instance?: AutocompleteResult; protected _isValueTouched = false; - protected _lastInputKeyEvent?: JQuery.Event; + protected _lastInputKeyEvent?: KeyboardEvent; protected _lastTriggeredByClearInput = false; + protected _locales: Locale; /** The JQuery DOM element */ - protected _$editorInputGroupElm: any; - protected _$input: any; - protected _$closeButtonGroupElm: any; + protected _editorInputGroupElm!: HTMLDivElement; + protected _inputElm!: HTMLInputElement; + protected _closeButtonGroupElm!: HTMLSpanElement; + protected _clearButtonElm!: HTMLButtonElement; + + /** The translate library */ + protected _translater?: TranslaterService; /** is the Editor disabled? */ disabled = false; @@ -67,33 +83,41 @@ export class AutoCompleteEditor implements Editor { forceUserInput = false; /** Final collection displayed in the UI, that is after processing filter/sort/override */ - finalCollection: any[] = []; + finalCollection: T[] = []; constructor(protected readonly args: EditorArguments) { if (!args) { throw new Error('[Slickgrid-Universal] Something is wrong with this grid, an Editor must always have valid arguments.'); } this.grid = args.grid; + this._bindEventService = new BindingEventService(); + if (this.gridOptions?.translater) { + this._translater = this.gridOptions.translater; + } + + // get locales provided by user in forRoot or else use default English locales via the Constants + this._locales = this.gridOptions && this.gridOptions.locales || Constants.locales; + this.init(); } /** Getter for the Autocomplete Option */ - get autoCompleteOptions(): Partial { - return this._autoCompleteOptions || {}; + get autocompleterOptions(): Partial { + return this._autocompleterOptions || {}; } /** Getter of the Collection */ - get collection(): any[] { + get collection(): T[] { return this.columnEditor?.collection ?? []; } /** Getter for the Editor DOM Element */ - get editorDomElement(): any { - return this._$input; + get editorDomElement(): HTMLInputElement { + return this._inputElm; } /** Getter for the Final Collection used in the AutoCompleted Source (this may vary from the "collection" especially when providing a customStructure) */ - get elementCollection(): any[] | null { + get elementCollection(): Array | null { return this._elementCollection; } @@ -121,11 +145,11 @@ export class AutoCompleteEditor implements Editor { } /** Getter for the item data context object */ - get dataContext(): any { + get dataContext(): T { return this.args.item; } - get editorOptions(): AutocompleteOption { + get editorOptions(): AutocompleterOption { return this.columnEditor?.editorOptions || {}; } @@ -134,9 +158,9 @@ export class AutoCompleteEditor implements Editor { return this.grid?.getOptions?.() ?? {}; } - /** jQuery UI AutoComplete instance */ - get instance(): any { - return this._$input.autocomplete('instance'); + /** Kraaden AutoComplete instance */ + get instance(): AutocompleteResult | undefined { + return this._instance; } get hasAutoCommitEdit(): boolean { @@ -149,13 +173,17 @@ export class AutoCompleteEditor implements Editor { } init() { - this.labelName = this.customStructure && this.customStructure.label || 'label'; - this.valueName = this.customStructure && this.customStructure.value || 'value'; - this.labelPrefixName = this.customStructure && this.customStructure.labelPrefix || 'labelPrefix'; - this.labelSuffixName = this.customStructure && this.customStructure.labelSuffix || 'labelSuffix'; + this.labelName = this.customStructure?.label ?? 'label'; + this.valueName = this.customStructure?.value ?? 'value'; + this.labelPrefixName = this.customStructure?.labelPrefix ?? 'labelPrefix'; + this.labelSuffixName = this.customStructure?.labelSuffix ?? 'labelSuffix'; // always render the DOM element, even if user passed a "collectionAsync", - const newCollection = this.columnEditor.collection || []; + let newCollection = this.columnEditor.collection; + if (this.columnEditor?.collectionAsync && !newCollection) { + newCollection = []; + } + // const newCollection = this.columnEditor.collection; this.renderDomElement(newCollection); // when having a collectionAsync and a collection that is empty, we'll toggle the Editor to disabled, @@ -166,36 +194,19 @@ export class AutoCompleteEditor implements Editor { } destroy() { - if (this._$input) { - this._$input.autocomplete('destroy'); - this._$input.off('keydown.nav').remove(); - } - this._$input = null; + this._bindEventService.unbindAll(); + this._instance?.destroy(); + this._inputElm?.remove?.(); this._elementCollection = null; } - /** - * Dynamically change an Editor option, this is especially useful with Composite Editor - * since this is the only way to change option after the Editor is created (for example dynamically change "minDate" or another Editor) - * @param {string} optionName - MultipleSelect option name - * @param {newValue} newValue - MultipleSelect new option value - */ - changeEditorOption(optionName: keyof AutocompleteOption, newValue: any) { - if (!this.columnEditor.editorOptions) { - this.columnEditor.editorOptions = {}; - } - this.columnEditor.editorOptions[optionName] = newValue; - this._autoCompleteOptions = { ...this._autoCompleteOptions, [optionName]: newValue }; - this._$input.autocomplete('option', optionName, newValue); - } - disable(isDisabled = true) { const prevIsDisabled = this.disabled; this.disabled = isDisabled; - if (this._$input) { + if (this._inputElm) { if (isDisabled) { - this._$input.attr('disabled', 'disabled'); + this._inputElm.disabled = true; // clear value when it's newly disabled and not empty const currentValue = this.getValue(); @@ -203,14 +214,15 @@ export class AutoCompleteEditor implements Editor { this.clear(true); } } else { - this._$input.removeAttr('disabled'); + this._inputElm.disabled = false; } } } focus() { - if (this._$input) { - this._$input.focus().select(); + if (this._inputElm) { + this._inputElm.focus(); + this._inputElm.select(); } } @@ -223,18 +235,14 @@ export class AutoCompleteEditor implements Editor { } getValue() { - return this._$input.val(); + return this._inputElm.value; } setValue(inputValue: any, isApplyingValue = false, triggerOnCompositeEditorChange = true) { - let label = inputValue; // if user provided a custom structure, we will serialize the value returned from the object with custom structure - if (inputValue && inputValue.hasOwnProperty(this.labelName)) { - label = inputValue[this.labelName]; - } else { - label = inputValue; - } - this._$input.val(label); + this._inputElm.value = (inputValue?.hasOwnProperty(this.labelName)) + ? inputValue[this.labelName] + : inputValue; if (isApplyingValue) { this._currentValue = inputValue; @@ -251,19 +259,19 @@ export class AutoCompleteEditor implements Editor { applyValue(item: any, state: any) { let newValue = state; - const fieldName = this.columnDef && this.columnDef.field; + const fieldName = this.columnDef?.field; if (fieldName !== undefined) { // if we have a collection defined, we will try to find the string within the collection and return it if (Array.isArray(this.collection) && this.collection.length > 0) { newValue = findOrDefault(this.collection, (collectionItem: any) => { - if (collectionItem && typeof state === 'object' && collectionItem.hasOwnProperty(this.labelName)) { - return (collectionItem.hasOwnProperty(this.labelName) && collectionItem[this.labelName].toString()) === (state.hasOwnProperty(this.labelName) && state[this.labelName].toString()); - } else if (collectionItem && typeof state === 'string' && collectionItem.hasOwnProperty(this.labelName)) { - return (collectionItem.hasOwnProperty(this.labelName) && collectionItem[this.labelName].toString()) === state; + if (collectionItem && isObject(state) && collectionItem.hasOwnProperty(this.valueName)) { + return (collectionItem[this.valueName].toString()) === (state.hasOwnProperty(this.valueName) && state[this.valueName].toString()); + } else if (collectionItem && typeof state === 'string' && collectionItem.hasOwnProperty(this.valueName)) { + return (collectionItem[this.valueName].toString()) === state; } - return collectionItem && collectionItem.toString() === state; - }); + return collectionItem?.toString() === state; + }, ''); } // is the field a complex object, "address.streetNumber" @@ -271,7 +279,7 @@ export class AutoCompleteEditor implements Editor { // validate the value before applying it (if not valid we'll set an empty string) const validation = this.validate(null, newValue); - newValue = (validation && validation.valid) ? newValue : ''; + newValue = validation?.valid ? newValue : ''; // set the new value to the item datacontext if (isComplexObject) { @@ -286,9 +294,9 @@ export class AutoCompleteEditor implements Editor { } isValueChanged(): boolean { - const elmValue = this._$input.val(); - const lastKeyEvent = this._lastInputKeyEvent && this._lastInputKeyEvent.keyCode; - if (this.columnEditor && this.columnEditor.alwaysSaveOnEnterKey && lastKeyEvent === KeyCode.ENTER) { + const elmValue = this._inputElm.value; + const lastKeyCodeEvent = this._lastInputKeyEvent?.keyCode; + if (this.columnEditor?.alwaysSaveOnEnterKey && (lastKeyCodeEvent === KeyCode.ENTER)) { return true; } const isValueChanged = (!(elmValue === '' && (this._defaultTextValue === null || this._defaultTextValue === undefined))) && (elmValue !== this._defaultTextValue); @@ -300,23 +308,23 @@ export class AutoCompleteEditor implements Editor { } loadValue(item: any) { - const fieldName = this.columnDef && this.columnDef.field; + const fieldName = this.columnDef?.field; if (item && fieldName !== undefined) { // is the field a complex object, "address.streetNumber" const isComplexObject = fieldName?.indexOf('.') > 0; - const data = (isComplexObject) ? getDescendantProperty(item, fieldName) : item[fieldName]; + const data = isComplexObject ? getDescendantProperty(item, fieldName) : item[fieldName]; this._currentValue = data; this._originalValue = data; this._defaultTextValue = typeof data === 'string' ? data : (data?.[this.labelName] ?? ''); - this._$input.val(this._defaultTextValue); - this._$input.select(); + this._inputElm.value = this._defaultTextValue as string; + this._inputElm.select(); } } clear(clearByDisableCommand = false) { - if (this._$input) { + if (this._inputElm) { this._currentValue = ''; this._defaultTextValue = ''; this.setValue('', true); // set the input value and also apply the change to the datacontext item @@ -339,10 +347,10 @@ export class AutoCompleteEditor implements Editor { */ reset(value?: any, triggerCompositeEventWhenExist = true, clearByDisableCommand = false) { const inputValue = value ?? this._originalValue ?? ''; - if (this._$input) { + if (this._inputElm) { this._currentValue = inputValue; this._defaultTextValue = typeof inputValue === 'string' ? inputValue : (inputValue?.[this.labelName] ?? ''); - this._$input.val(this._defaultTextValue); + this._inputElm.value = this._defaultTextValue; } this._isValueTouched = false; @@ -355,7 +363,7 @@ export class AutoCompleteEditor implements Editor { save() { const validation = this.validate(); - const isValid = (validation && validation.valid) || false; + const isValid = validation?.valid ?? false; if (this.hasAutoCommitEdit && isValid) { // do not use args.commitChanges() as this sets the focus to the next row. @@ -368,9 +376,9 @@ export class AutoCompleteEditor implements Editor { serializeValue(): any { // if you want to add the autocomplete functionality but want the user to be able to input a new option - if (this.editorOptions.forceUserInput) { + if (this._inputElm && this.editorOptions.forceUserInput) { const minLength = this.editorOptions?.minLength ?? MIN_LENGTH; - this._currentValue = this._$input.val().length > minLength ? this._$input.val() : this._currentValue; + this._currentValue = this._inputElm.value.length > minLength ? this._inputElm.value : this._currentValue; } // if user provided a custom structure, we will serialize the value returned from the object with custom structure @@ -402,7 +410,7 @@ export class AutoCompleteEditor implements Editor { return { valid: true, msg: '' }; } - const val = (inputValue !== undefined) ? inputValue : this._$input?.val(); + const val = (inputValue !== undefined) ? inputValue : this._inputElm?.value; return textValidator(val, { editorArgs: this.args, errorMessage: this.columnEditor.errorMessage, @@ -453,16 +461,17 @@ export class AutoCompleteEditor implements Editor { // this function should be protected but for unit tests purposes we'll make it public until a better solution is found // a better solution would be to get the autocomplete DOM element to work with selection but I couldn't find how to do that in Jest - handleSelect(event: Event, ui: { item: any; }) { - if (ui && ui.item) { - const selectedItem = ui && ui.item; + handleSelect(item: AutocompleteSearchItem) { + if (item !== undefined) { + const event = null; // TODO do we need the event? + const selectedItem = item; this._currentValue = selectedItem; this._isValueTouched = true; const compositeEditorOptions = this.args.compositeEditorOptions; - // when the user defines a "renderItem" (or "_renderItem") template, then we assume the user defines his own custom structure of label/value pair - // otherwise we know that jQueryUI always require a label/value pair, we can pull them directly - const hasCustomRenderItemCallback = this.columnEditor?.callbacks?.hasOwnProperty('_renderItem') ?? this.columnEditor?.editorOptions?.renderItem ?? false; + // when the user defines a "renderItem" template, then we assume the user defines his own custom structure of label/value pair + // otherwise we know that the autocomplete lib always require a label/value pair, we can pull them directly + const hasCustomRenderItemCallback = this.editorOptions?.renderItem ?? false; const itemLabel = typeof selectedItem === 'string' ? selectedItem : (hasCustomRenderItemCallback ? selectedItem[this.labelName] : selectedItem.label); this.setValue(itemLabel); @@ -474,10 +483,10 @@ export class AutoCompleteEditor implements Editor { } // if user wants to hook to the "select", he can do via this "onSelect" - // it purposely has a similar signature as the "select" callback + some extra arguments (row, cell, column, dataContext) - if (this.editorOptions.onSelect) { - const activeCell = this.grid.getActiveCell(); - this.editorOptions.onSelect(event, ui, activeCell.row, activeCell.cell, this.args.column, this.args.item); + // its signature is purposely similar to the "onSelect" callback + some extra arguments (row, cell, column, dataContext) + if (typeof this.editorOptions.onSelectItem === 'function') { + const { row, cell } = this.grid.getActiveCell(); + this.editorOptions.onSelectItem(item, row, cell, this.args.column, this.args.item); } setTimeout(() => this._lastTriggeredByClearInput = false); // reset flag after a cycle @@ -485,20 +494,27 @@ export class AutoCompleteEditor implements Editor { return false; } - protected renderCustomItem(ul: HTMLElement, item: any) { - const templateString = this._autoCompleteOptions?.renderItem?.templateCallback(item) ?? ''; + protected renderRegularItem(item: T) { + const itemLabel = (typeof item === 'string' ? item : item?.label ?? '') as string; + return createDomElement('div', { + textContent: itemLabel || '' + }); + } + + protected renderCustomItem(item: T) { + const templateString = this._autocompleterOptions?.renderItem?.templateCallback(item) ?? ''; // sanitize any unauthorized html tags like script and others // for the remaining allowed tags we'll permit all attributes const sanitizedTemplateText = sanitizeTextByAvailableSanitizer(this.gridOptions, templateString) || ''; - return $('
  • ') - .data('item.autocomplete', item) - .append(sanitizedTemplateText) - .appendTo(ul); + return createDomElement('div', { + // dataset: { item }, + innerHTML: sanitizedTemplateText + }); } - protected renderCollectionItem(ul: HTMLElement, item: any) { + protected renderCollectionItem(item: any) { // CollectionCustomStructure const isRenderHtmlEnabled = this.columnEditor?.enableRenderHtml ?? false; const prefixText = item.labelPrefix || ''; const labelText = item.label || ''; @@ -509,63 +525,58 @@ export class AutoCompleteEditor implements Editor { // for the remaining allowed tags we'll permit all attributes const sanitizedText = sanitizeTextByAvailableSanitizer(this.gridOptions, finalText) || ''; - const $liDiv = $('
    ')[isRenderHtmlEnabled ? 'html' : 'text'](sanitizedText); - return $('
  • ') - .data('item.autocomplete', item) - .append($liDiv) - .appendTo(ul); + const div = document.createElement('div'); + div[isRenderHtmlEnabled ? 'innerHTML' : 'textContent'] = sanitizedText; + return div; } renderDomElement(collection?: any[]) { - if (!Array.isArray(collection)) { - throw new Error('The "collection" passed to the Autocomplete Editor is not a valid array.'); - } const columnId = this.columnDef?.id ?? ''; const placeholder = this.columnEditor?.placeholder ?? ''; const title = this.columnEditor?.title ?? ''; - this._$input = $(``) - .appendTo(this.args.container) - .on('keydown.nav', (event: JQuery.Event) => { - this._lastInputKeyEvent = event; - if (event.keyCode === KeyCode.LEFT || event.keyCode === KeyCode.RIGHT) { - event.stopImmediatePropagation(); - } - }); - - // append the new DOM element to the slick cell container, - // we need the autocomplete-container so that the spinner is aligned properly with the Composite Editor - if (this._$input && typeof this._$input.appendTo === 'function') { - this._$editorInputGroupElm = $(`
    `); - this._$editorInputGroupElm.appendTo(this.args.container); - this._$input.appendTo(this._$editorInputGroupElm); - this._$input.addClass('input-group-editor'); - - const $closeButtonGroupElm = $(``); - this._$closeButtonGroupElm = $(``); - - // add an empty in order to add loading spinner styling - $(``).appendTo(this._$editorInputGroupElm); - - // show clear date button (unless user specifically doesn't want it) - if (!this.columnEditor?.params?.hideClearButton) { - this._$closeButtonGroupElm.appendTo($closeButtonGroupElm); - $closeButtonGroupElm.appendTo(this._$editorInputGroupElm); - this._$closeButtonGroupElm.on('click', this.clear.bind(this)); - } + this._editorInputGroupElm = createDomElement('div', { className: 'autocomplete-container input-group' }); + const closeButtonGroupElm = createDomElement('span', { className: 'input-group-btn input-group-append', dataset: { clear: '' } }); + this._clearButtonElm = createDomElement('button', { type: 'button', className: 'btn btn-default icon-clear' }); + this._inputElm = createDomElement('input', { + type: 'text', placeholder, title, + className: `autocomplete form-control editor-text input-group-editor editor-${columnId}`, + dataset: { input: '' } + }); + + this._editorInputGroupElm.appendChild(this._inputElm); + + // add an empty in order to add loading spinner styling + this._editorInputGroupElm.appendChild(createDomElement('span')); + + // show clear date button (unless user specifically doesn't want it) + if (!this.columnEditor?.params?.hideClearButton) { + closeButtonGroupElm.appendChild(this._clearButtonElm); + this._editorInputGroupElm.appendChild(closeButtonGroupElm); + this._bindEventService.bind(this._clearButtonElm, 'click', () => this.clear()); } - // user might pass his own autocomplete options - const autoCompleteOptions: AutocompleteOption = { - position: { of: this._$editorInputGroupElm ?? this._$input }, - ...this.columnEditor.editorOptions, - }; + this._bindEventService.bind(this._inputElm, 'focus', () => this._inputElm?.select()); + this._bindEventService.bind(this._inputElm, 'keydown', ((event: KeyboardEvent) => { + this._lastInputKeyEvent = event; + if (event.keyCode === KeyCode.LEFT || event.keyCode === KeyCode.RIGHT || event.key === 'ArrowLeft' || event.key === 'ArrowRight') { + event.stopImmediatePropagation(); + } + + // in case the user wants to save even an empty value, + // we need to subscribe to the onKeyDown event for that use case and clear the current value + if (this.columnEditor.alwaysSaveOnEnterKey) { + if (event.keyCode === KeyCode.ENTER || event.key === 'Enter') { + this._currentValue = null; + } + } + }) as EventListener); // assign the collection to a temp variable before filtering/sorting the collection let finalCollection = collection; // user could also override the collection - if (this.columnEditor?.collectionOverride) { + if (finalCollection && this.columnEditor?.collectionOverride) { const overrideArgs: CollectionOverrideArgs = { column: this.columnDef, dataContext: this.dataContext, grid: this.grid, originalCollections: this.collection }; if (this.args.compositeEditorOptions) { const { formValues, modalType } = this.args.compositeEditorOptions; @@ -575,81 +586,81 @@ export class AutoCompleteEditor implements Editor { } // keep reference of the final collection displayed in the UI - this.finalCollection = finalCollection; - - // user might provide his own custom structure - // jQuery UI autocomplete requires a label/value pair, so we must remap them when user provide different ones - if (Array.isArray(finalCollection)) { - finalCollection = finalCollection.map((item) => { - return { label: item[this.labelName], value: item[this.valueName], labelPrefix: item[this.labelPrefixName] || '', labelSuffix: item[this.labelSuffixName] || '' }; - }); + if (finalCollection) { + this.finalCollection = finalCollection; } - // keep the final source collection used in the AutoComplete as reference - this._elementCollection = finalCollection; - - // when user passes it's own autocomplete options - // we still need to provide our own "select" callback implementation - if (autoCompleteOptions?.source) { - autoCompleteOptions.select = (event: Event, ui: { item: any; }) => this.handleSelect(event, ui); - this._autoCompleteOptions = { ...autoCompleteOptions }; - - // when "renderItem" is defined, we need to add our custom style CSS class - if (this._autoCompleteOptions.renderItem) { - this._autoCompleteOptions.classes = { - 'ui-autocomplete': `autocomplete-custom-${toKebabCase(this._autoCompleteOptions.renderItem.layout)}` - }; + // the kradeen autocomplete lib only works with label/value pair, make sure that our array is in accordance + if (Array.isArray(finalCollection)) { + if (this.collection.every(x => isPrimmitive(x))) { + // when detecting an array of primitives, we have to remap it to an array of value/pair objects + finalCollection = finalCollection.map(c => ({ label: c, value: c })); + } else { + // user might provide its own custom structures, if so remap them as the new label/value pair + finalCollection = finalCollection.map((item) => ({ + label: item?.[this.labelName], + value: item?.[this.valueName], + labelPrefix: item?.[this.labelPrefixName] ?? '', + labelSuffix: item?.[this.labelSuffixName] ?? '' + })); } - // create the jQueryUI AutoComplete - this._$input.autocomplete(this._autoCompleteOptions); - - // when "renderItem" is defined, we need to call the user's custom renderItem template callback - if (this._autoCompleteOptions.renderItem) { - this._$input.autocomplete('instance')._renderItem = this.renderCustomItem.bind(this); - } - } else { - const definedOptions: AutocompleteOption = { - source: finalCollection, - minLength: 0, - select: (event: Event, ui: { item: any; }) => this.handleSelect(event, ui), - }; - this._autoCompleteOptions = { ...definedOptions, ...(this.columnEditor.editorOptions as AutocompleteOption) }; - this._$input.autocomplete(this._autoCompleteOptions); + // keep the final source collection used in the AutoComplete as reference + this._elementCollection = finalCollection; + } + + // merge custom autocomplete options with default basic options + this._autocompleterOptions = { + input: this._inputElm, + debounceWaitMs: 200, + className: `slick-autocomplete ${this.editorOptions?.className ?? ''}`.trim(), + emptyMsg: this.gridOptions.enableTranslate && this._translater?.translate ? this._translater.translate('NO_ELEMENTS_FOUND') : this._locales?.TEXT_NO_ELEMENTS_FOUND ?? 'No elements found', + onSelect: this.handleSelect.bind(this), + ...this.editorOptions, + } as Partial>; + + // "render" callback overriding + if (this._autocompleterOptions.renderItem?.layout) { + // when "renderItem" is defined, we need to add our custom style CSS classes & custom item renderer + this._autocompleterOptions.className += ` autocomplete-custom-${toKebabCase(this._autocompleterOptions.renderItem.layout)}`; + this._autocompleterOptions.render = this.renderCustomItem.bind(this); + } else if (Array.isArray(collection)) { // we'll use our own renderer so that it works with label prefix/suffix and also with html rendering when enabled - this._$input.autocomplete('instance')._renderItem = this.renderCollectionItem.bind(this); + this._autocompleterOptions.render = this._autocompleterOptions.render?.bind(this) ?? this.renderCollectionItem.bind(this); + } else if (!this._autocompleterOptions.render) { + // when no render callback is defined, we still need to define our own renderer for regular item + // because we accept string array but the Kraaden autocomplete doesn't by default and we can change that + this._autocompleterOptions.render = this.renderRegularItem.bind(this); } - // in case the user wants to save even an empty value, - // we need to subscribe to the onKeyDown event for that use case and clear the current value - if (this.columnEditor.alwaysSaveOnEnterKey) { - this._$input.keydown((event: KeyboardEvent) => { - if (event.keyCode === KeyCode.ENTER) { - this._currentValue = null; - } - }); - } + // when user passes it's own autocomplete options + // we still need to provide our own "select" callback implementation + if (this._autocompleterOptions?.fetch) { + // add loading class by overriding user's fetch method + addAutocompleteLoadingByOverridingFetch(this._inputElm, this._autocompleterOptions); - // user might override any of the jQueryUI callback methods - if (this.columnEditor.callbacks) { - for (const callback of Object.keys(this.columnEditor.callbacks)) { - if (typeof this.columnEditor.callbacks[callback] === 'function') { - this.instance[callback] = this.columnEditor.callbacks[callback]; - } - } + // create the Kraaden AutoComplete + this._instance = autocomplete(this._autocompleterOptions as AutocompleteSettings); + } else { + this._instance = autocomplete({ + ...this._autocompleterOptions, + fetch: (searchTerm, updateCallback) => { + if (finalCollection) { + // you can also use AJAX requests instead of preloaded data + // also at this point our collection was already modified, by the previous map, to have the "label" property (unless it's a string) + updateCallback(finalCollection!.filter(c => { + const label = (typeof c === 'string' ? c : c?.label) || ''; + return label.toLowerCase().includes(searchTerm.toLowerCase()); + })); + } + }, + } as AutocompleteSettings); } - this._$input.on('focus', () => { - this._$input.select(); - - // we could optionally trigger a search to open the AutoComplete search list - if (this.editorOptions.openSearchListOnFocus) { - this._$input.autocomplete('search', this._$input.val()); - } - }); + this.args.container.appendChild(this._editorInputGroupElm); if (!this.args.compositeEditorOptions) { setTimeout(() => this.focus(), 50); } } -} +} \ No newline at end of file diff --git a/packages/common/src/editors/editors.index.ts b/packages/common/src/editors/editors.index.ts index cd43a4183..5ec07e67d 100644 --- a/packages/common/src/editors/editors.index.ts +++ b/packages/common/src/editors/editors.index.ts @@ -1,4 +1,4 @@ -import { AutoCompleteEditor } from './autoCompleteEditor'; +import { AutocompleterEditor } from './autocompleterEditor'; import { CheckboxEditor } from './checkboxEditor'; import { DateEditor } from './dateEditor'; import { DualInputEditor } from './dualInputEditor'; @@ -12,8 +12,8 @@ import { SingleSelectEditor } from './singleSelectEditor'; import { SliderEditor } from './sliderEditor'; export const Editors = { - /** AutoComplete Editor (using jQuery UI autocomplete feature) */ - autoComplete: AutoCompleteEditor, + /** Autocompleter Editor (using https://github.com/kraaden/autocomplete) */ + autocompleter: AutocompleterEditor, /** Checkbox Editor (uses native checkbox DOM element) */ checkbox: CheckboxEditor, diff --git a/packages/common/src/editors/index.ts b/packages/common/src/editors/index.ts index b7b2d24b7..9c35cbb29 100644 --- a/packages/common/src/editors/index.ts +++ b/packages/common/src/editors/index.ts @@ -1,4 +1,4 @@ -export * from './autoCompleteEditor'; +export * from './autocompleterEditor'; export * from './checkboxEditor'; export * from './dateEditor'; export * from './dualInputEditor'; diff --git a/packages/common/src/enums/columnReorderFunction.type.ts b/packages/common/src/enums/columnReorderFunction.type.ts index ec6f7f491..00bd831e5 100644 --- a/packages/common/src/enums/columnReorderFunction.type.ts +++ b/packages/common/src/enums/columnReorderFunction.type.ts @@ -1,3 +1,3 @@ import { Column, SlickEvent, SlickGrid } from '../interfaces/index'; -export type ColumnReorderFunction = (grid: SlickGrid, headers: any, headerColumnWidthDiff: any, setColumns: (cols: Column[]) => void, setupColumnResize: () => void, columns: Column[], getColumnIndex: (column: Column) => number, uid: string, trigger: (slickEvent: SlickEvent, data?: any) => void) => void; +export type ColumnReorderFunction = (grid: SlickGrid, headers: any, headerColumnWidthDiff: any, setColumns: (cols: Column[]) => void, setupColumnResize: () => void, columns: Column[], getColumnIndex: (columnId: string) => number, uid: string, trigger: (slickEvent: SlickEvent, data?: any) => void) => void; diff --git a/packages/common/src/extensions/__tests__/slickContextMenu.spec.ts b/packages/common/src/extensions/__tests__/slickContextMenu.spec.ts index aa2f92dfc..131f93c57 100644 --- a/packages/common/src/extensions/__tests__/slickContextMenu.spec.ts +++ b/packages/common/src/extensions/__tests__/slickContextMenu.spec.ts @@ -741,7 +741,7 @@ describe('ContextMenu Plugin', () => { }); it('should call "copyToClipboard", WITH export formatter, when the command triggered is "copy"', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: false, enableTextExport: false, exportOptions: { exportWithFormatter: true } } as GridOption; + const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: false, enableTextExport: false, excelExportOptions: { exportWithFormatter: true } } as GridOption; const columnMock = { id: 'firstName', name: 'First Name', field: 'firstName', formatter: Formatters.uppercase } as Column; const dataContextMock = { id: 123, firstName: 'John', lastName: 'Doe', age: 50 }; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); @@ -766,7 +766,7 @@ describe('ContextMenu Plugin', () => { }); it('should call "copyToClipboard", with a number when the command triggered is "copy" and expect it to be copied without transformation', () => { - const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: false, enableTextExport: false, exportOptions: { exportWithFormatter: true } } as GridOption; + const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: false, enableTextExport: false, excelExportOptions: { exportWithFormatter: true } } as GridOption; const columnMock = { id: 'age', name: 'Age', field: 'age' } as Column; const dataContextMock = { id: 123, firstName: 'John', lastName: 'Doe', age: 50 }; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); diff --git a/packages/common/src/extensions/__tests__/slickDraggableGrouping.spec.ts b/packages/common/src/extensions/__tests__/slickDraggableGrouping.spec.ts index b455916f7..ae9ba60fd 100644 --- a/packages/common/src/extensions/__tests__/slickDraggableGrouping.spec.ts +++ b/packages/common/src/extensions/__tests__/slickDraggableGrouping.spec.ts @@ -1,11 +1,39 @@ -import 'jquery-ui/ui/widgets/sortable'; +const sortableMock = { + el: undefined, + options: {} as SortableOptions, + constructor: jest.fn(), + create: (el, options) => { + sortableMock.el = el; + sortableMock.options = { + ...(options || {}), + onAdd: jest.fn(), + onEnd: jest.fn(), + onStart: jest.fn(), + onUpdate: jest.fn(), + }; + + return { + el, + options, + destroy: jest.fn(), + toArray: jest.fn(), + }; + }, + utils: { + clone: (el) => el.cloneNode(true), + }, +}; +jest.mock('sortablejs', () => sortableMock); + +import 'jest-extended'; +import { SortableOptions } from 'sortablejs'; import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; import { Aggregators } from '../../aggregators/aggregators.index'; import { SlickDraggableGrouping } from '../slickDraggableGrouping'; import { ExtensionUtility } from '../../extensions/extensionUtility'; import { Column, DraggableGroupingOption, GridOption, SlickGrid, SlickNamespace } from '../../interfaces/index'; -import { BackendUtilityService, } from '../../services'; +import { BackendUtilityService, createDomElement, } from '../../services'; import { SharedService } from '../../services/shared.service'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; @@ -23,6 +51,7 @@ const gridOptionsMock = { enableDraggableGrouping: true, draggableGrouping: { hideToggleAllButton: false, + deleteIconCssClass: 'mdi mdi-close color-danger', }, showHeaderRow: false, showTopPanel: false, @@ -57,9 +86,9 @@ const gridStub = { onMouseEnter: new Slick.Event(), } as unknown as SlickGrid; -const mockColumns = [ // The column definitions +const mockColumns = [ { id: 'firstName', name: 'First Name', field: 'firstName', width: 100 }, - { id: 'lasstName', name: 'Last Name', field: 'lasstName', width: 100 }, + { id: 'lastName', name: 'Last Name', field: 'lastName', width: 100 }, { id: 'age', name: 'Age', field: 'age', width: 50, grouping: { @@ -84,20 +113,24 @@ describe('Draggable Grouping Plugin', () => { let sharedService: SharedService; let backendUtilityService: BackendUtilityService; let extensionUtility: ExtensionUtility; + let gridContainerDiv: HTMLDivElement; let translateService: TranslateServiceStub; let headerDiv: HTMLDivElement; let preHeaderDiv: HTMLDivElement; - let dragGroupDiv: HTMLDivElement; + let dropzoneElm: HTMLDivElement; beforeEach(() => { - preHeaderDiv = document.createElement('div'); + gridContainerDiv = document.createElement('div'); + gridContainerDiv.className = `slickgrid-container ${GRID_UID}`; headerDiv = document.createElement('div'); - dragGroupDiv = document.createElement('div'); - dragGroupDiv.className = 'ui-droppable ui-sortable'; + dropzoneElm = document.createElement('div'); + dropzoneElm.className = 'slick-dropzone'; headerDiv.className = 'slick-header-column'; + preHeaderDiv = document.createElement('div'); preHeaderDiv.className = 'slick-preheader-panel'; - preHeaderDiv.appendChild(dragGroupDiv); - document.body.appendChild(preHeaderDiv); + gridContainerDiv.appendChild(preHeaderDiv); + preHeaderDiv.appendChild(dropzoneElm); + document.body.appendChild(gridContainerDiv); eventPubSubService = new EventPubSubService(); backendUtilityService = new BackendUtilityService(); @@ -107,7 +140,7 @@ describe('Draggable Grouping Plugin', () => { jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); - jest.spyOn(gridStub, 'getPreHeaderPanel').mockReturnValue(preHeaderDiv); + jest.spyOn(gridStub, 'getPreHeaderPanel').mockReturnValue(dropzoneElm); plugin = new SlickDraggableGrouping(extensionUtility, eventPubSubService, sharedService); }); @@ -140,8 +173,8 @@ describe('Draggable Grouping Plugin', () => { it('should initialize the Draggable Grouping and expect optional "Toggle All" button text when provided to the plugin', () => { plugin.init(gridStub, { ...addonOptions, toggleAllButtonText: 'Toggle all Groups' }); - const preHeaderElm = document.querySelector('.slick-preheader-panel'); - const toggleAllTextElm = preHeaderElm.querySelector('.slick-group-toggle-all-text'); + const preHeaderElm = document.querySelector('.slick-preheader-panel') as HTMLDivElement; + const toggleAllTextElm = preHeaderElm.querySelector('.slick-group-toggle-all-text') as HTMLDivElement; expect(preHeaderElm).toBeTruthy(); expect(toggleAllTextElm.textContent).toBe('Toggle all Groups'); }); @@ -151,8 +184,8 @@ describe('Draggable Grouping Plugin', () => { translateService.use('fr'); plugin.init(gridStub, { ...addonOptions, toggleAllButtonTextKey: 'TOGGLE_ALL_GROUPS' }); - const preHeaderElm = document.querySelector('.slick-preheader-panel'); - const toggleAllTextElm = preHeaderElm.querySelector('.slick-group-toggle-all-text'); + const preHeaderElm = document.querySelector('.slick-preheader-panel') as HTMLDivElement; + const toggleAllTextElm = preHeaderElm.querySelector('.slick-group-toggle-all-text') as HTMLDivElement; expect(preHeaderElm).toBeTruthy(); expect(toggleAllTextElm.textContent).toBe('Basculer tous les groupes'); @@ -163,10 +196,10 @@ describe('Draggable Grouping Plugin', () => { translateService.use('fr'); plugin.init(gridStub, { ...addonOptions, toggleAllPlaceholderTextKey: 'TOGGLE_ALL_GROUPS' }); - const preHeaderElm = document.querySelector('.slick-preheader-panel'); - const toggleAllTextElm = preHeaderElm.querySelector('.slick-group-toggle-all') as HTMLDivElement; + const dropzoneElm = document.querySelector('.slick-preheader-panel') as HTMLDivElement; + const toggleAllTextElm = dropzoneElm.querySelector('.slick-group-toggle-all') as HTMLDivElement; - expect(preHeaderElm).toBeTruthy(); + expect(dropzoneElm).toBeTruthy(); expect(toggleAllTextElm.title).toBe('Basculer tous les groupes'); }); @@ -175,8 +208,8 @@ describe('Draggable Grouping Plugin', () => { translateService.use('fr'); plugin.init(gridStub, { ...addonOptions, dropPlaceHolderTextKey: 'GROUP_BY' }); - const preHeaderElm = document.querySelector('.slick-preheader-panel'); - const dropboxPlaceholderElm = preHeaderElm.querySelector('.slick-draggable-dropbox-toggle-placeholder'); + const preHeaderElm = document.querySelector('.slick-preheader-panel') as HTMLDivElement; + const dropboxPlaceholderElm = preHeaderElm.querySelector('.slick-draggable-dropzone-placeholder') as HTMLDivElement; expect(preHeaderElm).toBeTruthy(); expect(dropboxPlaceholderElm.textContent).toBe('Groupé par'); @@ -196,11 +229,15 @@ describe('Draggable Grouping Plugin', () => { describe('setupColumnReorder definition', () => { let dropEvent; - let dropTargetElm: HTMLSpanElement; let mockHelperElm: HTMLSpanElement; - let $headerColumnElm: any; - let $mockDivPaneContainer1: any; - let $mockDivPaneContainer2: any; + let mockDivPaneContainer1: HTMLDivElement; + let mockDivPaneContainer2: HTMLDivElement; + let headerColumnDiv1: HTMLDivElement; + let headerColumnDiv2: HTMLDivElement; + let headerColumnDiv3: HTMLDivElement; + let headerColumnDiv4: HTMLDivElement; + let mockHeaderLeftDiv1: HTMLDivElement; + let mockHeaderLeftDiv2: HTMLDivElement; const setColumnsSpy = jest.fn(); const setColumnResizeSpy = jest.fn(); const getColumnIndexSpy = jest.fn(); @@ -208,64 +245,71 @@ describe('Draggable Grouping Plugin', () => { const setGroupingSpy = jest.spyOn(dataViewStub, 'setGrouping'); beforeEach(() => { + mockDivPaneContainer1 = document.createElement('div'); + mockDivPaneContainer2 = document.createElement('div'); mockHelperElm = document.createElement('span'); const mockDivPaneContainerElm = document.createElement('div'); mockDivPaneContainerElm.className = 'slick-pane-header'; const mockDivPaneContainerElm2 = document.createElement('div'); mockDivPaneContainerElm2.className = 'slick-pane-header'; - const mockHeaderLeftDiv1 = document.createElement('div'); - const mockHeaderLeftDiv2 = document.createElement('div'); - mockHeaderLeftDiv1.className = 'slick-header-columns slick-header-columns-left ui-sortable'; - mockHeaderLeftDiv2.className = 'slick-header-columns slick-header-columns-right ui-sortable'; - const $mockHeaderLeftDiv1 = $(mockHeaderLeftDiv1); - const $mockHeaderLeftDiv2 = $(mockHeaderLeftDiv2); - $mockDivPaneContainer1 = $(mockDivPaneContainerElm); - $mockDivPaneContainer2 = $(mockDivPaneContainerElm2); - $mockHeaderLeftDiv1.appendTo($mockDivPaneContainer1); - $mockHeaderLeftDiv2.appendTo($mockDivPaneContainer2); - - dropTargetElm = document.createElement('div'); + mockHeaderLeftDiv1 = document.createElement('div'); + mockHeaderLeftDiv2 = document.createElement('div'); + mockHeaderLeftDiv1.className = 'slick-header-columns slick-header-columns-left'; + mockHeaderLeftDiv2.className = 'slick-header-columns slick-header-columns-right'; + + mockDivPaneContainerElm.appendChild(mockHeaderLeftDiv1); + mockDivPaneContainerElm2.appendChild(mockHeaderLeftDiv2); + gridContainerDiv.appendChild(mockDivPaneContainerElm); + gridContainerDiv.appendChild(mockDivPaneContainerElm2); dropEvent = new Event('mouseup'); - preHeaderDiv.appendChild(dropTargetElm); - Object.defineProperty(dropEvent, 'target', { writable: true, configurable: true, value: dropTargetElm }); - const headerColumnElm = document.createElement('div'); - headerColumnElm.className = 'slick-header-column'; - headerColumnElm.id = 'slickgrid12345age'; - headerColumnElm.dataset.id = 'age'; - const columnSpanElm = document.createElement('span'); - headerColumnElm.appendChild(columnSpanElm); - preHeaderDiv.appendChild(headerColumnElm); - $headerColumnElm = $(headerColumnElm); + headerColumnDiv1 = createDomElement('div', { + className: 'slick-header-column', id: `${GRID_UID}firstName`, dataset: { id: 'firstName' }, + }); + headerColumnDiv2 = createDomElement('div', { + className: 'slick-header-column', id: `${GRID_UID}lastName`, dataset: { id: 'lastName' }, + }); + headerColumnDiv3 = createDomElement('div', { + className: 'slick-header-column', id: `${GRID_UID}age`, dataset: { id: 'age' }, + }); + headerColumnDiv4 = createDomElement('div', { + className: 'slick-header-column', id: `${GRID_UID}medals`, dataset: { id: 'medals' }, + }); + headerColumnDiv1.appendChild(createDomElement('span', { className: 'slick-column-name', textContent: 'First Name' })); + headerColumnDiv2.appendChild(createDomElement('span', { className: 'slick-column-name', textContent: 'Last Name' })); + headerColumnDiv3.appendChild(createDomElement('span', { className: 'slick-column-name', textContent: 'Age' })); + headerColumnDiv4.appendChild(createDomElement('span', { className: 'slick-column-name', textContent: 'Medals' })); + preHeaderDiv.appendChild(headerColumnDiv1); + preHeaderDiv.appendChild(headerColumnDiv2); + preHeaderDiv.appendChild(headerColumnDiv3); + preHeaderDiv.appendChild(headerColumnDiv4); }); afterEach(() => { jest.clearAllMocks(); }); - it('should execute the "start" callback of the jQueryUI Sortable and expect css classes to be updated', () => { + it('should execute the "onStart" and "onAdd" callbacks of Sortable and expect css classes to be updated', () => { plugin.init(gridStub, { ...addonOptions }); - const droppableOptions = ($(plugin.dropboxElement) as any).droppable('option') as any; - droppableOptions.drop(dropEvent, { draggable: $headerColumnElm }); - const fn = plugin.setupColumnReorder(gridStub, $mockDivPaneContainer1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); - let placeholderElm = preHeaderDiv.querySelector('.slick-draggable-dropbox-toggle-placeholder') as HTMLDivElement; - let dropGroupingElm = dropTargetElm.querySelector('.slick-dropped-grouping') as HTMLDivElement; - const startFn = fn.sortable('option', 'start'); - startFn(new Event('click'), { helper: mockHelperElm }); - - expect(mockHelperElm.classList.contains('slick-header-column-active')).toBeTruthy(); - expect(placeholderElm.style.display).toBe('inline-block'); - expect(dropGroupingElm.style.display).toBe('none'); + const fn = plugin.setupColumnReorder(gridStub, mockHeaderLeftDiv1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + jest.spyOn(fn.sortableLeftInstance, 'toArray').mockReturnValue(['age', 'medals']); + + fn.sortableLeftInstance!.options.onStart!({} as any); + plugin.droppableInstance!.options.onAdd!({ item: headerColumnDiv3, clone: headerColumnDiv3.cloneNode(true) } as any); - let groupByRemoveElm = preHeaderDiv.querySelector('.slick-groupby-remove'); - const groupByRemoveImageElm = document.querySelector('.slick-groupby-remove-image'); + let groupByRemoveElm = preHeaderDiv.querySelector('.slick-groupby-remove') as HTMLDivElement; + const groupByRemoveImageElm = document.querySelector('.slick-groupby-remove-icon') as HTMLDivElement; + let placeholderElm = preHeaderDiv.querySelector('.slick-draggable-dropzone-placeholder') as HTMLDivElement; + expect(fn.sortableLeftInstance).toEqual(plugin.sortableLeftInstance); + expect(fn.sortableRightInstance).toEqual(plugin.sortableRightInstance); + expect(fn.sortableLeftInstance.destroy).toBeTruthy(); expect(groupByRemoveElm).toBeTruthy(); expect(groupByRemoveImageElm).toBeTruthy(); groupByRemoveElm.dispatchEvent(new Event('click')); - groupByRemoveElm = preHeaderDiv.querySelector('.slick-groupby-remove'); - placeholderElm = preHeaderDiv.querySelector('.slick-draggable-dropbox-toggle-placeholder') as HTMLDivElement; + groupByRemoveElm = preHeaderDiv.querySelector('.slick-groupby-remove') as HTMLDivElement; + placeholderElm = preHeaderDiv.querySelector('.slick-draggable-dropzone-placeholder') as HTMLDivElement; const toggleAllElm = preHeaderDiv.querySelector('.slick-group-toggle-all') as HTMLDivElement; expect(setGroupingSpy).toHaveBeenCalledWith([]); @@ -274,84 +318,98 @@ describe('Draggable Grouping Plugin', () => { expect(toggleAllElm.style.display).toBe('none'); }); - it('should execute the "beforeStop" callback of the jQueryUI Sortable and expect css classes to be updated', () => { - plugin.init(gridStub, { ...addonOptions, deleteIconCssClass: 'mdi mdi-close' }); - const droppableOptions = ($(plugin.dropboxElement) as any).droppable('option') as any; - droppableOptions.drop(dropEvent, { draggable: $headerColumnElm }); - const fn = plugin.setupColumnReorder(gridStub, $mockDivPaneContainer1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); - const beforeStopFn = fn.sortable('option', 'beforeStop'); - beforeStopFn(new Event('click'), { helper: mockHelperElm }); + it('should execute the "onEnd" callback of Sortable and expect css classes to be updated', () => { + plugin.init(gridStub, { ...addonOptions }); + const fn = plugin.setupColumnReorder(gridStub, mockHeaderLeftDiv1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + jest.spyOn(fn.sortableLeftInstance, 'toArray').mockReturnValue([]); + + fn.sortableLeftInstance!.options.onStart!({} as any); + plugin.droppableInstance!.options.onAdd!({ item: headerColumnDiv3, clone: headerColumnDiv3.cloneNode(true) } as any); + fn.sortableLeftInstance.options.onEnd!(new CustomEvent('end') as any); - let placeholderElm = preHeaderDiv.querySelector('.slick-draggable-dropbox-toggle-placeholder') as HTMLDivElement; - let dropGroupingElm = dropTargetElm.querySelector('.slick-dropped-grouping') as HTMLDivElement; + let placeholderElm = preHeaderDiv.querySelector('.slick-draggable-dropzone-placeholder') as HTMLDivElement; + let dropGroupingElm = preHeaderDiv.querySelector('.slick-dropped-grouping') as HTMLDivElement; expect(placeholderElm.style.display).toBe('none'); expect(dropGroupingElm.style.display).toBe('inline-block'); }); - it('should execute the "stop" callback of the jQueryUI Sortable and expect sortable to be cancelled', () => { - plugin.init(gridStub, { ...addonOptions, deleteIconCssClass: 'mdi mdi-close' }); - const droppableOptions = ($(plugin.dropboxElement) as any).droppable('option') as any; - droppableOptions.drop(dropEvent, { draggable: $headerColumnElm }); - jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(false); - const fn = plugin.setupColumnReorder(gridStub, $mockDivPaneContainer1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); - const stopFn = fn.sortable('option', 'stop'); + it('should execute the "onEnd" callback of Sortable and expect sortable to be cancelled', () => { + plugin.init(gridStub, { ...addonOptions }); + plugin.setAddonOptions({ deleteIconCssClass: 'mdi mdi-close color-danger' }); + const fn = plugin.setupColumnReorder(gridStub, mockHeaderLeftDiv1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); - const groupByRemoveElm = document.querySelector('.slick-groupby-remove.mdi-close'); - expect(groupByRemoveElm).toBeTruthy(); + fn.sortableLeftInstance!.options.onStart!({} as any); + plugin.droppableInstance!.options.onAdd!({ item: headerColumnDiv3, clone: headerColumnDiv3.cloneNode(true) } as any); + fn.sortableLeftInstance.options.onEnd!(new CustomEvent('end') as any); - stopFn(new Event('click'), { helper: mockHelperElm }); + const groupByRemoveElm = document.querySelector('.slick-groupby-remove.mdi-close') as HTMLDivElement; + expect(groupByRemoveElm).toBeTruthy(); expect(setColumnsSpy).not.toHaveBeenCalled(); expect(setColumnResizeSpy).not.toHaveBeenCalled(); expect(triggerSpy).not.toHaveBeenCalled(); }); - it('should execute the "stop" callback of the jQueryUI Sortable and expect css classes to be updated', () => { - plugin.init(gridStub, { ...addonOptions, deleteIconCssClass: 'mdi mdi-close' }); + it('should clear grouping and expect placeholder to be displayed when calling "onEnd" callback', () => { + plugin.init(gridStub, { ...addonOptions }); + plugin.setAddonOptions({ deleteIconCssClass: 'mdi mdi-close color-danger' }); + const fn = plugin.setupColumnReorder(gridStub, mockHeaderLeftDiv1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + + fn.sortableLeftInstance!.options.onStart!({} as any); + plugin.droppableInstance!.options.onAdd!({ item: headerColumnDiv3, clone: headerColumnDiv3.cloneNode(true) } as any); + plugin.clearDroppedGroups(); + fn.sortableLeftInstance.options.onEnd!(new CustomEvent('end') as any); + + const draggablePlaceholderElm = dropzoneElm.querySelector('.slick-draggable-dropzone-placeholder') as HTMLDivElement; + expect(draggablePlaceholderElm.style.display).toEqual('inline-block'); + }); + + it('should execute the "onEnd" callback of Sortable and expect css classes to be updated', () => { + plugin.init(gridStub, { ...addonOptions }); plugin.setColumns(mockColumns); - const droppableOptions = ($(plugin.dropboxElement) as any).droppable('option') as any; - droppableOptions.drop(dropEvent, { draggable: $headerColumnElm }); + plugin.setAddonOptions({ deleteIconCssClass: 'mdi mdi-close' }); jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); getColumnIndexSpy.mockReturnValue(2); - const fn = plugin.setupColumnReorder(gridStub, $mockDivPaneContainer1.add($mockDivPaneContainer2), {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); - const stopFn = fn.sortable('option', 'stop'); + const fn = plugin.setupColumnReorder(gridStub, mockHeaderLeftDiv1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + jest.spyOn(fn.sortableLeftInstance, 'toArray').mockReturnValue(['age', 'medals']); + + fn.sortableLeftInstance!.options.onStart!({} as any); + plugin.droppableInstance!.options.onAdd!({ item: headerColumnDiv3, clone: headerColumnDiv3.cloneNode(true) } as any); const groupByRemoveElm = document.querySelector('.slick-groupby-remove.mdi-close'); expect(groupByRemoveElm).toBeTruthy(); - stopFn(new Event('click'), { helper: mockHelperElm }); + fn.sortableLeftInstance.options.onEnd!(new CustomEvent('end') as any); expect(setColumnsSpy).toHaveBeenCalledWith([mockColumns[2], mockColumns[2]]); expect(setColumnResizeSpy).toHaveBeenCalled(); expect(triggerSpy).toHaveBeenCalledWith(gridStub.onColumnsReordered, { grid: gridStub }); }); - describe('setupColumnDropbox method', () => { - it('should expect denied class to be removed when "deactivate" is called', () => { - plugin.init(gridStub, { ...addonOptions }); - const deactivateFn = plugin.droppableInstance.droppable('option', 'deactivate'); - plugin.dropboxElement.classList.add('slick-header-column-denied'); - deactivateFn(); + it('should drag over dropzone and expect hover css class be added and removed when dragging outside of dropzone', () => { + plugin.init(gridStub, { ...addonOptions }); + plugin.setAddonOptions({ deleteIconCssClass: 'mdi mdi-close color-danger' }); + const fn = plugin.setupColumnReorder(gridStub, mockHeaderLeftDiv1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); - expect(plugin.dropboxElement.classList.contains('slick-header-column-denied')).toBeFalsy(); - }); + fn.sortableLeftInstance!.options.onStart!({} as any); + plugin.droppableInstance!.options.onAdd!({ item: headerColumnDiv3, clone: headerColumnDiv3.cloneNode(true) } as any); + fn.sortableLeftInstance.options.onEnd!(new CustomEvent('end') as any); - it('should expect denied class to be added when calling "over" with a header column that does not have a "grouping" property', () => { - const mockHeaderColumnDiv = document.createElement('div'); - mockHeaderColumnDiv.id = 'slickgrid12345firstName'; - mockHeaderColumnDiv.className = 'slick-header-column'; - const $mockHeaderColumnDiv = $(mockHeaderColumnDiv); + const dragoverEvent = new CustomEvent('dragover', { bubbles: true, detail: {} }); + dropzoneElm.dispatchEvent(dragoverEvent); - plugin.init(gridStub, { ...addonOptions }); - const overFn = plugin.droppableInstance.droppable('option', 'over'); - overFn(new Event('mouseup'), { draggable: $mockHeaderColumnDiv }); + const dragenterEvent = new CustomEvent('dragenter', { bubbles: true, detail: {} }); + dropzoneElm.dispatchEvent(dragenterEvent); + expect(dropzoneElm.classList.contains('slick-dropzone-hover')).toBeTruthy(); - expect(plugin.dropboxElement.classList.contains('slick-header-column-denied')).toBeTruthy(); - }); + const dragleaveEvent = new CustomEvent('dragleave', { bubbles: true, detail: {} }); + dropzoneElm.dispatchEvent(dragleaveEvent); + expect(dropzoneElm.classList.contains('slick-dropzone-hover')).toBeFalsy(); + }); + describe('setupColumnDropbox method', () => { describe('setupColumnDropbox update & toggler click event', () => { let groupChangedSpy: any; - const updateEvent = new Event('mouseup'); let mockHeaderColumnDiv1: HTMLDivElement; let mockHeaderColumnDiv2: HTMLDivElement; @@ -361,26 +419,27 @@ describe('Draggable Grouping Plugin', () => { mockHeaderColumnDiv1.className = 'slick-dropped-grouping'; mockHeaderColumnDiv1.id = 'age'; mockHeaderColumnDiv1.dataset.id = 'age'; - mockColumns[2].grouping.collapsed = false; + mockColumns[2].grouping!.collapsed = false; mockHeaderColumnDiv2 = document.createElement('div'); mockHeaderColumnDiv2.className = 'slick-dropped-grouping'; mockHeaderColumnDiv2.id = 'medals'; mockHeaderColumnDiv2.dataset.id = 'medals'; - dragGroupDiv.appendChild(mockHeaderColumnDiv1); - dragGroupDiv.appendChild(mockHeaderColumnDiv2); - $(mockHeaderColumnDiv1).appendTo($mockDivPaneContainer1); - $(mockHeaderColumnDiv2).appendTo($mockDivPaneContainer1); + dropzoneElm.appendChild(mockHeaderColumnDiv1); + dropzoneElm.appendChild(mockHeaderColumnDiv2); - Object.defineProperty(updateEvent, 'target', { writable: true, configurable: true, value: $mockDivPaneContainer1.get(0) }); + mockHeaderColumnDiv1.appendChild(mockDivPaneContainer1); + mockHeaderColumnDiv2.appendChild(mockDivPaneContainer1); plugin.init(gridStub, { ...addonOptions, deleteIconCssClass: 'mdi mdi-close' }); - const droppableOptions = ($(plugin.dropboxElement) as any).droppable('option') as any; - droppableOptions.drop(dropEvent, { draggable: $headerColumnElm }); + plugin.setAddonOptions({ deleteIconCssClass: 'mdi mdi-close' }); + jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(false); - const fn = plugin.setupColumnReorder(gridStub, $mockDivPaneContainer1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); - const updateFn = plugin.sortableInstance.sortable('option', 'update'); - updateFn(updateEvent); + const fn = plugin.setupColumnReorder(gridStub, mockHeaderLeftDiv1, {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + jest.spyOn(plugin.droppableInstance!, 'toArray').mockReturnValue(['age', 'medals']); + fn.sortableLeftInstance!.options.onStart!({} as any); + plugin.droppableInstance!.options.onAdd!({ item: headerColumnDiv3, clone: headerColumnDiv3.cloneNode(true) } as any); + plugin.droppableInstance?.options.onUpdate!({ item: mockDivPaneContainer1, clone: mockDivPaneContainer1.cloneNode(true) } as any); }); afterEach(() => { @@ -388,10 +447,16 @@ describe('Draggable Grouping Plugin', () => { }); it('should call sortable "update" from setupColumnDropbox and expect "updateGroupBy" to be called with a sort-group', () => { + expect(plugin.dropboxElement).toEqual(dropzoneElm); expect(plugin.columnsGroupBy.length).toBeGreaterThan(0); expect(groupChangedSpy).toHaveBeenCalledWith({ caller: 'sort-group', - groupColumns: [{ aggregators: expect.toBeArray(), formatter: mockColumns[2].grouping.formatter, getter: 'age', collapsed: false, }], + groupColumns: [{ + aggregators: expect.toBeArray(), + formatter: mockColumns[2].grouping!.formatter, + getter: 'age', + collapsed: false, + }], }); jest.spyOn(gridStub, 'getHeaderColumn').mockReturnValue(mockHeaderColumnDiv1); @@ -399,12 +464,12 @@ describe('Draggable Grouping Plugin', () => { }); it('should call "clearDroppedGroups" and expect the grouping to be cleared', () => { - const preHeaderElm = document.querySelector('.slick-preheader-panel'); - let dropboxPlaceholderElm = preHeaderElm.querySelector('.slick-draggable-dropbox-toggle-placeholder') as HTMLDivElement; + const preHeaderElm = document.querySelector('.slick-preheader-panel') as HTMLDivElement; + let dropboxPlaceholderElm = preHeaderElm.querySelector('.slick-draggable-dropzone-placeholder') as HTMLDivElement; expect(dropboxPlaceholderElm.style.display).toBe('none'); plugin.clearDroppedGroups(); - dropboxPlaceholderElm = preHeaderElm.querySelector('.slick-draggable-dropbox-toggle-placeholder') as HTMLDivElement; + dropboxPlaceholderElm = preHeaderElm.querySelector('.slick-draggable-dropzone-placeholder') as HTMLDivElement; expect(dropboxPlaceholderElm.style.display).toBe('inline-block'); expect(groupChangedSpy).toHaveBeenCalledWith({ caller: 'clear-all', groupColumns: [], }); }); @@ -412,8 +477,8 @@ describe('Draggable Grouping Plugin', () => { it('should use the Toggle All and expect classes to be toggled and DataView to call necessary method', () => { const dvExpandSpy = jest.spyOn(dataViewStub, 'expandAllGroups'); const dvCollapseSpy = jest.spyOn(dataViewStub, 'collapseAllGroups'); - let toggleAllElm = document.querySelector('.slick-group-toggle-all'); - const toggleAllIconElm = toggleAllElm.querySelector('.slick-group-toggle-all-icon'); + let toggleAllElm = document.querySelector('.slick-group-toggle-all') as HTMLDivElement; + const toggleAllIconElm = toggleAllElm.querySelector('.slick-group-toggle-all-icon') as HTMLDivElement; const clickEvent = new Event('click'); Object.defineProperty(clickEvent, 'target', { writable: true, configurable: true, value: toggleAllIconElm }); @@ -423,14 +488,14 @@ describe('Draggable Grouping Plugin', () => { // collapsed after toggle toggleAllElm.dispatchEvent(clickEvent); - toggleAllElm = document.querySelector('.slick-group-toggle-all'); + toggleAllElm = document.querySelector('.slick-group-toggle-all') as HTMLDivElement; expect(toggleAllIconElm.classList.contains('collapsed')).toBeTruthy(); expect(toggleAllIconElm.classList.contains('expanded')).toBeFalsy(); expect(dvCollapseSpy).toHaveBeenCalled(); // expanded after toggle toggleAllElm.dispatchEvent(clickEvent); - toggleAllElm = document.querySelector('.slick-group-toggle-all'); + toggleAllElm = document.querySelector('.slick-group-toggle-all') as HTMLDivElement; expect(toggleAllIconElm.classList.contains('collapsed')).toBeFalsy(); expect(toggleAllIconElm.classList.contains('expanded')).toBeTruthy(); expect(dvExpandSpy).toHaveBeenCalled(); @@ -444,8 +509,8 @@ describe('Draggable Grouping Plugin', () => { it('should change the toggle icon to collapsed when that action is called from the Context Menu', () => { eventPubSubService.publish('onContextMenuCollapseAllGroups'); - const toggleAllElm = document.querySelector('.slick-group-toggle-all'); - const toggleAllIconElm = toggleAllElm.querySelector('.slick-group-toggle-all-icon'); + const toggleAllElm = document.querySelector('.slick-group-toggle-all') as HTMLDivElement; + const toggleAllIconElm = toggleAllElm.querySelector('.slick-group-toggle-all-icon') as HTMLDivElement; expect(toggleAllIconElm.classList.contains('expanded')).toBeFalsy(); expect(toggleAllIconElm.classList.contains('collapsed')).toBeTruthy(); @@ -453,13 +518,45 @@ describe('Draggable Grouping Plugin', () => { it('should change the toggle icon to expanded when that action is called from the Context Menu', () => { eventPubSubService.publish('onContextMenuExpandAllGroups'); - const toggleAllElm = document.querySelector('.slick-group-toggle-all'); - const toggleAllIconElm = toggleAllElm.querySelector('.slick-group-toggle-all-icon'); + const toggleAllElm = document.querySelector('.slick-group-toggle-all') as HTMLDivElement; + const toggleAllIconElm = toggleAllElm.querySelector('.slick-group-toggle-all-icon') as HTMLDivElement; expect(toggleAllIconElm.classList.contains('expanded')).toBeTruthy(); expect(toggleAllIconElm.classList.contains('collapsed')).toBeFalsy(); }); }); }); + + describe('with Frozen Grid', () => { + beforeEach(() => { + gridOptionsMock.frozenColumn = 2; + setColumnsSpy.mockClear(); + jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit').mockReturnValue(true); + getColumnIndexSpy + .mockReturnValueOnce(0) + .mockReturnValueOnce(1) + .mockReturnValueOnce(2) + .mockReturnValueOnce(3) + .mockReturnValueOnce(4) + }); + + it('should execute the "onEnd" callback of Sortable and expect sortable to be cancelled', () => { + plugin.init(gridStub, { ...addonOptions }); + plugin.setAddonOptions({ deleteIconCssClass: 'mdi mdi-close color-danger' }); + const fn = plugin.setupColumnReorder(gridStub, [mockHeaderLeftDiv1, mockHeaderLeftDiv2], {}, setColumnsSpy, setColumnResizeSpy, mockColumns, getColumnIndexSpy, GRID_UID, triggerSpy); + jest.spyOn(fn.sortableLeftInstance, 'toArray').mockReturnValue(['firstName', 'lastName', 'age']); + jest.spyOn(fn.sortableRightInstance, 'toArray').mockReturnValue(['gender']); + + fn.sortableLeftInstance!.options.onStart!({} as any); + plugin.droppableInstance!.options.onAdd!({ item: headerColumnDiv3, clone: headerColumnDiv3.cloneNode(true) } as any); + fn.sortableLeftInstance.options.onEnd!(new CustomEvent('end') as any); + + const groupByRemoveElm = document.querySelector('.slick-groupby-remove.mdi-close') as HTMLDivElement; + expect(groupByRemoveElm).toBeTruthy(); + + mockColumns.pop(); // all original columns except last column which is what we returned on sortableRightInstance + expect(setColumnsSpy).toHaveBeenCalledWith(mockColumns); + }); + }); }); }); diff --git a/packages/common/src/extensions/menuBaseClass.ts b/packages/common/src/extensions/menuBaseClass.ts index ae8491d24..06f01841d 100644 --- a/packages/common/src/extensions/menuBaseClass.ts +++ b/packages/common/src/extensions/menuBaseClass.ts @@ -5,7 +5,7 @@ import { CellMenu, Column, ContextMenu, - DOMMouseEvent, + DOMMouseOrTouchEvent, GridMenu, GridOption, HeaderButton, @@ -115,7 +115,7 @@ export class MenuBaseClass>, args: unknown, - itemClickCallback: (event: DOMMouseEvent, type: MenuType, item: ExtractMenuType, columnDef?: Column) => void + itemClickCallback: (event: DOMMouseOrTouchEvent, type: MenuType, item: ExtractMenuType, columnDef?: Column) => void ) { if (args && commandOrOptionItems && menuOptions) { for (const item of commandOrOptionItems) { @@ -148,7 +148,7 @@ export class MenuBaseClass, args: any, - itemClickCallback: (event: DOMMouseEvent, type: MenuType, item: ExtractMenuType, columnDef?: Column) => void + itemClickCallback: (event: DOMMouseOrTouchEvent, type: MenuType, item: ExtractMenuType, columnDef?: Column) => void ): HTMLLIElement | null { let commandLiElm: HTMLLIElement | null = null; @@ -227,12 +227,12 @@ export class MenuBaseClass) => + this._bindEventService.bind(commandLiElm, 'click', ((e: DOMMouseOrTouchEvent) => itemClickCallback.call(this, e, itemType, item, args?.column)) as EventListener); // Header Button can have an optional handler if ((item as HeaderButtonItem).handler && !(item as HeaderButtonItem).disabled) { - this._bindEventService.bind(commandLiElm, 'click', ((e: DOMMouseEvent) => + this._bindEventService.bind(commandLiElm, 'click', ((e: DOMMouseOrTouchEvent) => (item as HeaderButtonItem).handler!.call(this, e)) as EventListener); } } diff --git a/packages/common/src/extensions/menuFromCellBaseClass.ts b/packages/common/src/extensions/menuFromCellBaseClass.ts index 662a561c7..02e672b36 100644 --- a/packages/common/src/extensions/menuFromCellBaseClass.ts +++ b/packages/common/src/extensions/menuFromCellBaseClass.ts @@ -4,7 +4,7 @@ import { titleCase } from '@slickgrid-universal/utils'; import { CellMenu, ContextMenu, - DOMMouseEvent, + DOMMouseOrTouchEvent, MenuCallbackArgs, MenuCommandItem, MenuCommandItemCallbackArgs, @@ -30,7 +30,7 @@ export class MenuFromCellBaseClass extends Men super(extensionUtility, pubSubService, sharedService); } - createMenu(event: DOMMouseEvent) { + createMenu(event: DOMMouseOrTouchEvent) { this.menuElement?.remove(); this._menuElm = undefined; const cell = this.grid.getCellFromEvent(event); @@ -40,7 +40,7 @@ export class MenuFromCellBaseClass extends Men this._currentRow = cell.row ?? 0; const columnDef = this.grid.getColumns()[this._currentCell]; const dataContext = this.grid.getDataItem(this._currentRow); - + const targetEvent: MouseEvent | Touch = (event as TouchEvent)?.touches?.[0] ?? event; const commandItems = this._addonOptions?.commandItems || []; const optionItems = this._addonOptions?.optionItems || []; @@ -84,7 +84,7 @@ export class MenuFromCellBaseClass extends Men // create a new Menu this._menuElm = createDomElement('div', { className: `${this._menuPluginCssPrefix || this._menuCssPrefix} ${this.gridUid}`, - style: { display: 'none', left: `${event.pageX}px`, top: `${event.pageY + 5}px` } + style: { display: 'none', left: `${targetEvent.pageX}px`, top: `${targetEvent.pageY + 5}px` } }); const maxHeight = isNaN(this.addonOptions.maxHeight as any) ? this.addonOptions.maxHeight : `${this.addonOptions.maxHeight ?? 0}px`; @@ -151,7 +151,7 @@ export class MenuFromCellBaseClass extends Men return this._menuElm; } - closeMenu(e: DOMMouseEvent, args: MenuFromCellCallbackArgs) { + closeMenu(e: DOMMouseOrTouchEvent, args: MenuFromCellCallbackArgs) { if (this.menuElement) { if (typeof this.addonOptions?.onBeforeMenuClose === 'function' && (this.addonOptions as CellMenu | ContextMenu).onBeforeMenuClose!(e, args) === false) { return; @@ -178,19 +178,19 @@ export class MenuFromCellBaseClass extends Men } /** Mouse down handler when clicking anywhere in the DOM body */ - protected handleBodyMouseDown(e: DOMMouseEvent) { + protected handleBodyMouseDown(e: DOMMouseOrTouchEvent) { if ((this.menuElement !== e.target && !this.menuElement?.contains(e.target)) || e.target.className === 'close') { this.closeMenu(e, { cell: this._currentCell, row: this._currentRow, grid: this.grid }); } } - protected handleCloseButtonClicked(e: DOMMouseEvent) { + protected handleCloseButtonClicked(e: DOMMouseOrTouchEvent) { if (!e.defaultPrevented) { this.closeMenu(e, { cell: 0, row: 0, grid: this.grid, }); } } - protected handleMenuItemCommandClick(event: DOMMouseEvent, type: MenuType, item: ExtractMenuType) { + protected handleMenuItemCommandClick(event: DOMMouseOrTouchEvent, type: MenuType, item: ExtractMenuType) { if ((item as never)?.[type] !== undefined && item !== 'divider' && !item.disabled && !(item as MenuCommandItem | MenuOptionItem).divider && this._currentCell !== undefined && this._currentRow !== undefined) { if (type === 'option' && !this.grid.getEditorLock().commitCurrentEdit()) { return; @@ -235,23 +235,24 @@ export class MenuFromCellBaseClass extends Men } protected populateCommandOrOptionCloseBtn(itemType: MenuType, closeButtonElm: HTMLButtonElement, commandOrOptionMenuElm: HTMLDivElement) { - this._bindEventService.bind(closeButtonElm, 'click', ((e: DOMMouseEvent) => this.handleCloseButtonClicked(e)) as EventListener); + this._bindEventService.bind(closeButtonElm, 'click', ((e: DOMMouseOrTouchEvent) => this.handleCloseButtonClicked(e)) as EventListener); const commandOrOptionMenuHeaderElm = commandOrOptionMenuElm.querySelector(`.slick-${itemType}-header`) ?? createDomElement('div', { className: `slick-${itemType}-header` }); commandOrOptionMenuHeaderElm?.appendChild(closeButtonElm); commandOrOptionMenuElm.appendChild(commandOrOptionMenuHeaderElm); commandOrOptionMenuHeaderElm.classList.add('with-close'); } - protected repositionMenu(event: DOMMouseEvent) { + protected repositionMenu(event: DOMMouseOrTouchEvent) { if (this._menuElm && event.target) { // move to 0,0 before calulating height/width since it could be cropped values // when element is outside browser viewport this._menuElm.style.top = `0px`; this._menuElm.style.left = `0px`; + const targetEvent: MouseEvent | Touch = (event as TouchEvent)?.touches?.[0] ?? event; const parentElm = event.target.closest('.slick-cell') as HTMLDivElement; - let menuOffsetLeft = (parentElm && this._camelPluginName === 'cellMenu') ? getHtmlElementOffset(parentElm)?.left ?? 0 : event.pageX; - let menuOffsetTop = (parentElm && this._camelPluginName === 'cellMenu') ? getHtmlElementOffset(parentElm)?.top ?? 0 : event.pageY; + let menuOffsetLeft = (parentElm && this._camelPluginName === 'cellMenu') ? getHtmlElementOffset(parentElm)?.left ?? 0 : targetEvent.pageX; + let menuOffsetTop = (parentElm && this._camelPluginName === 'cellMenu') ? getHtmlElementOffset(parentElm)?.top ?? 0 : targetEvent.pageY; const parentCellWidth = parentElm.offsetWidth || 0; const menuHeight = this._menuElm?.offsetHeight || 0; const menuWidth = this._menuElm?.offsetWidth || this._addonOptions.width || 0; diff --git a/packages/common/src/extensions/slickCellExcelCopyManager.ts b/packages/common/src/extensions/slickCellExcelCopyManager.ts index f6f53a923..af3d7ec71 100644 --- a/packages/common/src/extensions/slickCellExcelCopyManager.ts +++ b/packages/common/src/extensions/slickCellExcelCopyManager.ts @@ -148,11 +148,10 @@ export class SlickCellExcelCopyManager { // when grid or cell is not editable, we will possibly evaluate the Formatter if it was passed // to decide if we evaluate the Formatter, we will use the same flag from Export which is "exportWithFormatter" if (!this.gridOptions.editable || !columnDef.editor) { - const textExportOptions = { ...this.gridOptions.exportOptions, ...this.gridOptions.textExportOptions }; - const isEvaluatingFormatter = (columnDef.exportWithFormatter !== undefined) ? columnDef.exportWithFormatter : (textExportOptions?.exportWithFormatter); + const isEvaluatingFormatter = (columnDef.exportWithFormatter !== undefined) ? columnDef.exportWithFormatter : (this.gridOptions.textExportOptions?.exportWithFormatter); if (columnDef.formatter && isEvaluatingFormatter) { const formattedOutput = columnDef.formatter(0, 0, item[columnDef.field], columnDef, item, this._grid); - if (columnDef.sanitizeDataExport || (textExportOptions?.sanitizeDataExport)) { + if (columnDef.sanitizeDataExport || (this.gridOptions.textExportOptions?.sanitizeDataExport)) { let outputString = formattedOutput as string; if (formattedOutput && typeof formattedOutput === 'object' && formattedOutput.hasOwnProperty('text')) { outputString = formattedOutput.text; diff --git a/packages/common/src/extensions/slickCellMenu.ts b/packages/common/src/extensions/slickCellMenu.ts index 91e6c4332..1ee2fcfc7 100644 --- a/packages/common/src/extensions/slickCellMenu.ts +++ b/packages/common/src/extensions/slickCellMenu.ts @@ -4,7 +4,7 @@ import { CellMenu, CellMenuOption, Column, - DOMMouseEvent, + DOMMouseOrTouchEvent, MenuCommandItem, MenuCommandItemCallbackArgs, MenuOptionItem, @@ -100,7 +100,7 @@ export class SlickCellMenu extends MenuFromCellBaseClass { // event handlers // ------------------ - protected handleCellClick(event: DOMMouseEvent, args: MenuCommandItemCallbackArgs) { + protected handleCellClick(event: DOMMouseOrTouchEvent, args: MenuCommandItemCallbackArgs) { const cell = this.grid.getCellFromEvent(event); if (cell) { const dataContext = this.grid.getDataItem(cell.row); diff --git a/packages/common/src/extensions/slickCellRangeSelector.ts b/packages/common/src/extensions/slickCellRangeSelector.ts index d7881af4f..bf1501b74 100644 --- a/packages/common/src/extensions/slickCellRangeSelector.ts +++ b/packages/common/src/extensions/slickCellRangeSelector.ts @@ -3,7 +3,7 @@ import { deepMerge } from '@slickgrid-universal/utils'; import { CellRange, CellRangeSelectorOption, - DOMMouseEvent, + DOMMouseOrTouchEvent, DragPosition, DragRange, GridOption, @@ -122,6 +122,7 @@ export class SlickCellRangeSelector { } getMouseOffsetViewport(e: SlickEventData, dd: DragPosition): MouseOffsetViewport { + const targetEvent: MouseEvent | Touch = (e as TouchEvent)?.touches?.[0] ?? e; const viewportLeft = this._activeViewport.scrollLeft; const viewportTop = this._activeViewport.scrollTop; const viewportRight = viewportLeft + this._viewportWidth; @@ -147,16 +148,16 @@ export class SlickCellRangeSelector { }; // ... horizontal - if (e.pageX < viewportOffsetLeft) { - result.offset.x = e.pageX - viewportOffsetLeft; - } else if (e.pageX > viewportOffsetRight) { - result.offset.x = e.pageX - viewportOffsetRight; + if (targetEvent.pageX < viewportOffsetLeft) { + result.offset.x = targetEvent.pageX - viewportOffsetLeft; + } else if (targetEvent.pageX > viewportOffsetRight) { + result.offset.x = targetEvent.pageX - viewportOffsetRight; } // ... vertical - if (e.pageY < viewportOffsetTop) { - result.offset.y = viewportOffsetTop - e.pageY; - } else if (e.pageY > viewportOffsetBottom) { - result.offset.y = viewportOffsetBottom - e.pageY; + if (targetEvent.pageY < viewportOffsetTop) { + result.offset.y = viewportOffsetTop - targetEvent.pageY; + } else if (targetEvent.pageY > viewportOffsetBottom) { + result.offset.y = viewportOffsetBottom - targetEvent.pageY; } result.isOutsideViewport = !!result.offset.x || !!result.offset.y; return result; @@ -256,9 +257,10 @@ export class SlickCellRangeSelector { } protected handleDragTo(e: { pageX: number; pageY: number; }, dd: DragPosition) { + const targetEvent: MouseEvent | Touch = (e as unknown as TouchEvent)?.touches?.[0] ?? e; const end = this._grid.getCellFromPoint( - e.pageX - (getHtmlElementOffset(this._activeCanvas)?.left ?? 0) + this._columnOffset, - e.pageY - (getHtmlElementOffset(this._activeCanvas)?.top ?? 0) + this._rowOffset + targetEvent.pageX - (getHtmlElementOffset(this._activeCanvas)?.left ?? 0) + this._columnOffset, + targetEvent.pageY - (getHtmlElementOffset(this._activeCanvas)?.top ?? 0) + this._rowOffset ); if (end !== undefined) { @@ -344,7 +346,7 @@ export class SlickCellRangeSelector { e.stopImmediatePropagation(); } - protected handleDragStart(e: DOMMouseEvent, dd: DragPosition) { + protected handleDragStart(e: DOMMouseOrTouchEvent, dd: DragPosition) { const cellObj = this._grid.getCellFromEvent(e); if (cellObj && this.onBeforeCellRangeSelected.notify(cellObj) !== false && this._grid.canCellBeSelected(cellObj.row, cellObj.cell)) { this._dragging = true; @@ -373,7 +375,7 @@ export class SlickCellRangeSelector { return this._decorator.show(new Slick.Range(start.row, start.cell)); } - protected handleScroll(_e: DOMMouseEvent, args: OnScrollEventArgs) { + protected handleScroll(_e: DOMMouseOrTouchEvent, args: OnScrollEventArgs) { this._scrollTop = args.scrollTop; this._scrollLeft = args.scrollLeft; } diff --git a/packages/common/src/extensions/slickCheckboxSelectColumn.ts b/packages/common/src/extensions/slickCheckboxSelectColumn.ts index be2d1fd1c..85455689c 100644 --- a/packages/common/src/extensions/slickCheckboxSelectColumn.ts +++ b/packages/common/src/extensions/slickCheckboxSelectColumn.ts @@ -1,5 +1,5 @@ import { KeyCode } from '../enums/keyCode.enum'; -import { CheckboxSelectorOption, Column, DOMMouseEvent, GridOption, SelectableOverrideCallback, SlickEventData, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index'; +import { CheckboxSelectorOption, Column, DOMMouseOrTouchEvent, GridOption, SelectableOverrideCallback, SlickEventData, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index'; import { SlickRowSelectionModel } from './slickRowSelectionModel'; import { createDomElement, emptyElement } from '../services/domUtilities'; import { BindingEventService } from '../services/bindingEvent.service'; @@ -266,7 +266,7 @@ export class SlickCheckboxSelectColumn { args.node.appendChild(spanElm); this._headerRowNode = args.node; - this._bindEventService.bind(spanElm, 'click', ((e: DOMMouseEvent) => this.handleHeaderClick(e, args)) as EventListener); + this._bindEventService.bind(spanElm, 'click', ((e: DOMMouseOrTouchEvent) => this.handleHeaderClick(e, args)) as EventListener); } }); } @@ -303,7 +303,7 @@ export class SlickCheckboxSelectColumn { return this._checkboxColumnCellIndex; } - protected handleClick(e: DOMMouseEvent, args: { row: number; cell: number; grid: SlickGrid; }) { + protected handleClick(e: DOMMouseOrTouchEvent, args: { row: number; cell: number; grid: SlickGrid; }) { // clicking on a row select checkbox if (this._grid.getColumns()[args.cell].id === this._addonOptions.columnId && e.target.type === 'checkbox') { // if editing, try to commit @@ -319,7 +319,7 @@ export class SlickCheckboxSelectColumn { } } - protected handleHeaderClick(e: DOMMouseEvent, args: { column: Column; node: HTMLDivElement; grid: SlickGrid; }) { + protected handleHeaderClick(e: DOMMouseOrTouchEvent, args: { column: Column; node: HTMLDivElement; grid: SlickGrid; }) { if (args.column.id === this._addonOptions.columnId && e.target.type === 'checkbox') { // if editing, try to commit if (this._grid.getEditorLock().isActive() && !this._grid.getEditorLock().commitCurrentEdit()) { diff --git a/packages/common/src/extensions/slickColumnPicker.ts b/packages/common/src/extensions/slickColumnPicker.ts index 9f24c63fc..1bbdd62ed 100644 --- a/packages/common/src/extensions/slickColumnPicker.ts +++ b/packages/common/src/extensions/slickColumnPicker.ts @@ -3,7 +3,7 @@ import { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; import { Column, ColumnPickerOption, - DOMMouseEvent, + DOMMouseOrTouchEvent, GridOption, SlickEventHandler, SlickGrid, @@ -174,7 +174,7 @@ export class SlickColumnPicker { // ------------------ /** Mouse down handler when clicking anywhere in the DOM body */ - protected handleBodyMouseDown(e: DOMMouseEvent) { + protected handleBodyMouseDown(e: DOMMouseOrTouchEvent) { if ((this._menuElm !== e.target && !this._menuElm.contains(e.target)) || e.target.className === 'close') { this._menuElm.setAttribute('aria-expanded', 'false'); this._menuElm.style.display = 'none'; @@ -182,7 +182,7 @@ export class SlickColumnPicker { } /** Mouse header context handler when doing a right+click on any of the header column title */ - protected handleHeaderContextMenu(e: DOMMouseEvent) { + protected handleHeaderContextMenu(e: DOMMouseOrTouchEvent) { e.preventDefault(); emptyElement(this._listElm); updateColumnPickerOrder.call(this); @@ -192,11 +192,12 @@ export class SlickColumnPicker { this.repositionMenu(e); } - protected repositionMenu(event: DOMMouseEvent) { - this._menuElm.style.top = `${event.pageY - 10}px`; - this._menuElm.style.left = `${event.pageX - 10}px`; + protected repositionMenu(event: DOMMouseOrTouchEvent) { + const targetEvent: MouseEvent | Touch = (event as TouchEvent)?.touches?.[0] ?? event; + this._menuElm.style.top = `${targetEvent.pageY - 10}px`; + this._menuElm.style.left = `${targetEvent.pageX - 10}px`; this._menuElm.style.minHeight = findWidthOrDefault(this.addonOptions.minHeight, ''); - this._menuElm.style.maxHeight = findWidthOrDefault(this.addonOptions.maxHeight, `${window.innerHeight - event.clientY}px`); + this._menuElm.style.maxHeight = findWidthOrDefault(this.addonOptions.maxHeight, `${window.innerHeight - targetEvent.clientY}px`); this._menuElm.style.display = 'block'; this._menuElm.setAttribute('aria-expanded', 'true'); this._menuElm.appendChild(this._listElm); diff --git a/packages/common/src/extensions/slickContextMenu.ts b/packages/common/src/extensions/slickContextMenu.ts index 16b343a40..2111423a2 100644 --- a/packages/common/src/extensions/slickContextMenu.ts +++ b/packages/common/src/extensions/slickContextMenu.ts @@ -3,7 +3,7 @@ import { ContextMenu, ContextMenuOption, Column, - DOMMouseEvent, + DOMMouseOrTouchEvent, MenuCallbackArgs, MenuCommandItem, MenuCommandItemCallbackArgs, @@ -108,7 +108,7 @@ export class SlickContextMenu extends MenuFromCellBaseClass { // event handlers // ------------------ - protected handleClick(event: DOMMouseEvent, args: MenuCommandItemCallbackArgs) { + protected handleClick(event: DOMMouseOrTouchEvent, args: MenuCommandItemCallbackArgs) { const cell = this.grid.getCellFromEvent(event); if (cell) { const dataContext = this.grid.getDataItem(cell.row); @@ -189,7 +189,7 @@ export class SlickContextMenu extends MenuFromCellBaseClass { } // show context menu: Export to file - if ((gridOptions?.enableExport || gridOptions?.enableTextExport) && contextMenu && !contextMenu.hideExportCsvCommand) { + if (gridOptions?.enableTextExport && contextMenu && !contextMenu.hideExportCsvCommand) { const commandName = 'export-csv'; if (!originalCommandItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { menuCommandItems.push( @@ -242,7 +242,7 @@ export class SlickContextMenu extends MenuFromCellBaseClass { } // show context menu: export to text file as tab delimited - if ((gridOptions?.enableExport || gridOptions?.enableTextExport) && contextMenu && !contextMenu.hideExportTextDelimitedCommand) { + if (gridOptions?.enableTextExport && contextMenu && !contextMenu.hideExportTextDelimitedCommand) { const commandName = 'export-text-delimited'; if (!originalCommandItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { menuCommandItems.push( @@ -386,7 +386,7 @@ export class SlickContextMenu extends MenuFromCellBaseClass { const columnDef = args?.column; const dataContext = args?.dataContext; const grid = this.sharedService?.slickGrid; - const exportOptions = gridOptions && ((gridOptions.excelExportOptions || { ...gridOptions.exportOptions, ...gridOptions.textExportOptions })); + const exportOptions = gridOptions && ((gridOptions.excelExportOptions || gridOptions.textExportOptions)); let textToCopy = exportWithFormatterWhenDefined(row, cell, columnDef, dataContext, grid, exportOptions); if (typeof columnDef.queryFieldNameGetterFn === 'function') { textToCopy = getCellValueFromQueryFieldGetter(columnDef, dataContext, ''); diff --git a/packages/common/src/extensions/slickDraggableGrouping.ts b/packages/common/src/extensions/slickDraggableGrouping.ts index 32f2af5b1..7aa8be291 100644 --- a/packages/common/src/extensions/slickDraggableGrouping.ts +++ b/packages/common/src/extensions/slickDraggableGrouping.ts @@ -1,16 +1,18 @@ import { BasePubSubService, EventSubscription } from '@slickgrid-universal/event-pub-sub'; import { isEmptyObject } from '@slickgrid-universal/utils'; +import SortableInstance, { Options as SortableOptions, SortableEvent } from 'sortablejs'; +import * as Sortable_ from 'sortablejs'; +const Sortable = ((Sortable_ as any)?.['default'] ?? Sortable_); // patch for rollup import { ExtensionUtility } from '../extensions/extensionUtility'; import { Column, - DOMMouseEvent, + DOMMouseOrTouchEvent, DraggableGrouping, DraggableGroupingOption, GridOption, Grouping, GroupingGetterFunction, - HtmlElementPosition, SlickDataView, SlickEvent, SlickEventHandler, @@ -24,28 +26,10 @@ import { createDomElement, emptyElement } from '../services/domUtilities'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; -export interface JQueryUiDraggableOption { - draggable: any; - helper: any; - originalPosition: { left: number; top: number; }; - placeholder?: string; - position?: HtmlElementPosition; - sender?: any; - offset?: HtmlElementPosition; -} - -export interface JQueryUiSortableOptions { - distance?: number; - cursor?: string; - tolerance?: string; - helper?: string; - placeholder?: string; - forcePlaceholderSize?: boolean; - appendTo?: string; - start?: (e: Event, ui: JQueryUiDraggableOption) => void; - beforeStop?: (e: Event, ui: JQueryUiDraggableOption) => void; - stop?: (e: any) => void; -} +// use global variable because "setupColumnReorder()" method is used as static +// and doesn't have access to anything inside the class and we need to dipose/destroy cleanly of all Sortable instances +// let sortableLeftInstance: SortableInstance; +// let sortableRightInstance: SortableInstance; /** * @@ -72,15 +56,17 @@ export interface JQueryUiSortableOptions { export class SlickDraggableGrouping { protected _addonOptions!: DraggableGroupingOption; protected _bindEventService: BindingEventService; - protected _droppableInstance: any; - protected _dropboxElm!: HTMLDivElement; - protected _dropboxPlaceholderElm!: HTMLDivElement; + protected _droppableInstance?: SortableInstance; + protected _dropzoneElm!: HTMLDivElement; + protected _dropzonePlaceholderElm!: HTMLDivElement; protected _eventHandler!: SlickEventHandler; protected _grid?: SlickGrid; protected _gridColumns: Column[] = []; protected _gridUid = ''; protected _groupToggler?: HTMLDivElement; - protected _sortableInstance: any; + protected _reorderedColumns: Column[] = []; + protected _sortableLeftInstance?: SortableInstance; + protected _sortableRightInstance?: SortableInstance; protected _subscriptions: EventSubscription[] = []; protected _defaults = { dropPlaceHolderText: 'Drop a column header here to group by the column', @@ -113,15 +99,19 @@ export class SlickDraggableGrouping { } get dropboxElement() { - return this._dropboxElm; + return this._dropzoneElm; } get droppableInstance() { return this._droppableInstance; } - get sortableInstance() { - return this._sortableInstance; + get sortableLeftInstance() { + return this._sortableLeftInstance; + } + + get sortableRightInstance() { + return this._sortableRightInstance; } get eventHandler(): SlickEventHandler { @@ -151,7 +141,8 @@ export class SlickDraggableGrouping { if (grid) { this._gridUid = grid.getUID(); this._gridColumns = grid.getColumns(); - this._dropboxElm = grid.getPreHeaderPanel(); + this._dropzoneElm = grid.getPreHeaderPanel(); + this._dropzoneElm.classList.add('slick-dropzone'); // add optional group "Toggle All" with its button & text when provided if (!this._addonOptions.hideToggleAllButton) { @@ -179,7 +170,7 @@ export class SlickDraggableGrouping { }) ); } - this._dropboxElm.appendChild(this._groupToggler); + this._dropzoneElm.appendChild(this._groupToggler); // when calling Expand/Collapse All Groups from Context Menu, we also need to inform this plugin as well of the action this._subscriptions.push( @@ -188,12 +179,12 @@ export class SlickDraggableGrouping { ); } - this._dropboxPlaceholderElm = createDomElement('div', { className: 'slick-draggable-dropbox-toggle-placeholder' }); + this._dropzonePlaceholderElm = createDomElement('div', { className: 'slick-draggable-dropzone-placeholder' }); if (this.gridOptions.enableTranslate && this._addonOptions?.dropPlaceHolderTextKey) { this._addonOptions.dropPlaceHolderText = this.extensionUtility.translateWhenEnabledAndServiceExist(this._addonOptions.dropPlaceHolderTextKey, 'TEXT_TOGGLE_ALL_GROUPS'); } - this._dropboxPlaceholderElm.textContent = this._addonOptions?.dropPlaceHolderText ?? this._defaults.dropPlaceHolderText ?? ''; - this._dropboxElm.appendChild(this._dropboxPlaceholderElm); + this._dropzonePlaceholderElm.textContent = this._addonOptions?.dropPlaceHolderText ?? this._defaults.dropPlaceHolderText ?? ''; + this._dropzoneElm.appendChild(this._dropzonePlaceholderElm); this.setupColumnDropbox(); @@ -227,9 +218,16 @@ export class SlickDraggableGrouping { /** Dispose the plugin. */ dispose() { + if (this._sortableLeftInstance?.el) { + this._sortableLeftInstance?.destroy(); + } + if (this._sortableRightInstance?.el) { + this._sortableRightInstance?.destroy(); + } this.onGroupChanged.unsubscribe(); this._eventHandler.unsubscribeAll(); this.pubSubService.unsubscribeAll(this._subscriptions); + this._bindEventService.unbindAll(); emptyElement(document.querySelector('.slick-preheader-panel')); this._grid = undefined as any; } @@ -237,7 +235,7 @@ export class SlickDraggableGrouping { clearDroppedGroups() { this.columnsGroupBy = []; this.updateGroupBy('clear-all'); - const allDroppedGroupingElms = this._dropboxElm.querySelectorAll('.slick-dropped-grouping'); + const allDroppedGroupingElms = this._dropzoneElm.querySelectorAll('.slick-dropped-grouping'); for (const groupElm of Array.from(allDroppedGroupingElms)) { const groupRemoveBtnElm = document.querySelector('.slick-groupby-remove'); groupRemoveBtnElm?.remove(); @@ -245,92 +243,134 @@ export class SlickDraggableGrouping { } // show placeholder text & hide the "Toggle All" when that later feature is enabled - this._dropboxPlaceholderElm.style.display = 'inline-block'; + this._dropzonePlaceholderElm.style.display = 'inline-block'; if (this._groupToggler) { this._groupToggler.style.display = 'none'; } } + setAddonOptions(options: Partial) { + this._addonOptions = { ...this._addonOptions, ...options }; + } + setColumns(cols: Column[]) { this._gridColumns = cols; } setDroppedGroups(groupingInfo: Array | string) { + this._dropzonePlaceholderElm.style.display = 'none'; const groupingInfos = Array.isArray(groupingInfo) ? groupingInfo : [groupingInfo]; - this._dropboxPlaceholderElm.style.display = 'none'; for (const groupInfo of groupingInfos) { - const column = $(this.grid.getHeaderColumn(groupInfo as string)); - this.handleGroupByDrop(this._dropboxElm, column); + const columnElm = this.grid.getHeaderColumn(groupInfo as string); + this.handleGroupByDrop(this._dropzoneElm, columnElm); } } - setupColumnReorder(grid: SlickGrid, $headers: any, _headerColumnWidthDiff: any, setColumns: (columns: Column[]) => void, setupColumnResize: () => void, columns: Column[], getColumnIndex: (column: Column) => number, uid: string, trigger: (slickEvent: SlickEvent, data?: any) => void) { - $headers.filter(':ui-sortable').sortable('destroy'); - const headerDraggableGroupByElm = grid.getPreHeaderPanel(); - - return $headers.sortable({ - distance: 3, - cursor: 'default', - tolerance: 'intersection', - helper: 'clone', - placeholder: 'slick-sortable-placeholder ui-state-default slick-header-column', - forcePlaceholderSize: true, - appendTo: 'body', - start: (_e: Event, ui: JQueryUiDraggableOption) => { - $(ui.helper).addClass('slick-header-column-active'); - const placeholderElm = headerDraggableGroupByElm.querySelector('.slick-draggable-dropbox-toggle-placeholder'); - if (placeholderElm) { - placeholderElm.style.display = 'inline-block'; + /** + * Setup the column reordering + * NOTE: this function is a standalone function and is called externally and does not have access to `this` instance + * @param grid - slick grid object + * @param headers - slick grid column header elements + * @param _headerColumnWidthDiff - header column width difference + * @param setColumns - callback to reassign columns + * @param setupColumnResize - callback to setup the column resize + * @param columns - columns array + * @param getColumnIndex - callback to find index of a column + * @param uid - grid UID + * @param trigger - callback to execute when triggering a column grouping + */ + setupColumnReorder(grid: SlickGrid, headers: any, _headerColumnWidthDiff: any, setColumns: (columns: Column[]) => void, setupColumnResize: () => void, columns: Column[], getColumnIndex: (columnId: string) => number, uid: string, trigger: (slickEvent: SlickEvent, data?: any) => void) { + let reorderedColumns = grid.getColumns(); + const dropzoneElm = grid.getPreHeaderPanel(); + const draggablePlaceholderElm = dropzoneElm.querySelector('.slick-draggable-dropzone-placeholder'); + const groupTogglerElm = document.querySelector('.slick-group-toggle-all'); + + const sortableOptions = { + animation: 50, + chosenClass: 'slick-header-column-active', + ghostClass: 'slick-sortable-placeholder', + draggable: '.slick-header-column', + dataIdAttr: 'data-id', + group: { + name: 'shared', + pull: 'clone', + put: false, + }, + revertClone: true, + // filter: function (_e, target) { + // // block column from being able to be dragged if it's already a grouped column + // // NOTE: need to disable for now since it also blocks the column reordering + // return columnsGroupBy.some(c => c.id === target.getAttribute('data-id')); + // }, + onStart: () => { + if (draggablePlaceholderElm) { + draggablePlaceholderElm.style.display = 'none'; } - const droppedGroupingElm = headerDraggableGroupByElm.querySelector('.slick-dropped-grouping'); + const droppedGroupingElm = dropzoneElm.querySelector('.slick-dropped-grouping'); if (droppedGroupingElm) { droppedGroupingElm.style.display = 'none'; } + if (groupTogglerElm) { + groupTogglerElm.style.display = 'none'; + } }, - beforeStop: (_e: Event, ui: JQueryUiDraggableOption) => { - $(ui.helper).removeClass('slick-header-column-active'); - const hasDroppedColumn = headerDraggableGroupByElm.querySelectorAll('.slick-dropped-grouping').length; - if (hasDroppedColumn > 0) { - const placeholderElm = headerDraggableGroupByElm.querySelector('.slick-draggable-dropbox-toggle-placeholder'); - if (placeholderElm) { - placeholderElm.style.display = 'none'; + onEnd: (e: Event & { item: any; clone: HTMLElement; }) => { + dropzoneElm?.classList.remove('slick-dropzone-hover'); + draggablePlaceholderElm?.parentElement?.classList.remove('slick-dropzone-placeholder-hover'); + + if (dropzoneElm.querySelectorAll('.slick-dropped-grouping').length) { + if (draggablePlaceholderElm) { + draggablePlaceholderElm.style.display = 'none'; } - const droppedGroupingElm = headerDraggableGroupByElm.querySelector('.slick-dropped-grouping'); + const droppedGroupingElm = dropzoneElm.querySelector('.slick-dropped-grouping'); if (droppedGroupingElm) { droppedGroupingElm.style.display = 'inline-block'; } + if (groupTogglerElm) { + groupTogglerElm.style.display = 'inline-block'; + } + } else if (draggablePlaceholderElm) { + draggablePlaceholderElm.style.display = 'inline-block'; } - }, - stop: (e: DOMMouseEvent) => { + if (!grid.getEditorLock().commitCurrentEdit()) { - ($(e.target) as any).sortable('cancel'); return; } - const reorderedIds = $headers.sortable('toArray'); - // If frozen columns are used, headers has more than one entry and we need the ids from all of them. + + const reorderedIds = this.sortableLeftInstance?.toArray() ?? []; + + // when frozen columns are used, headers has more than one entry and we need the ids from all of them. // though there is only really a left and right header, this will work even if that should change. - if ($headers.length > 1) { - for (let headerIdx = 1, ln = $headers.length; headerIdx < ln; headerIdx += 1) { - const $header = $($headers[headerIdx]); - const ids = ($header as any).sortable('toArray'); - // Note: the loop below could be simplified with: - // reorderedIds.push.apply(reorderedIds,ids); - // However, the loop is more in keeping with way-backward compatibility - for (const id of ids) { - reorderedIds.push(id); - } + if (headers.length > 1) { + const ids = this._sortableRightInstance?.toArray() ?? []; + + // Note: the loop below could be simplified with: + // reorderedIds.push.apply(reorderedIds,ids); + // However, the loop is more in keeping with way-backward compatibility + for (const id of ids) { + reorderedIds.push(id); } } - const reorderedColumns = []; + + const finalReorderedColumns: Column[] = []; for (const reorderedId of reorderedIds) { - reorderedColumns.push(columns[getColumnIndex(reorderedId.replace(uid, ''))]); + finalReorderedColumns.push(reorderedColumns[getColumnIndex(reorderedId)]); } - setColumns(reorderedColumns); - trigger(grid.onColumnsReordered, { grid: this.grid }); + setColumns(finalReorderedColumns); + trigger(grid.onColumnsReordered, { grid }); e.stopPropagation(); setupColumnResize(); + reorderedColumns = finalReorderedColumns; } - } as JQueryUiSortableOptions); + } as SortableOptions; + + this._sortableLeftInstance = Sortable.create(document.querySelector(`.${grid.getUID()} .slick-header-columns.slick-header-columns-left`) as HTMLDivElement, sortableOptions) as SortableInstance; + this._sortableRightInstance = Sortable.create(document.querySelector(`.${grid.getUID()} .slick-header-columns.slick-header-columns-right`) as HTMLDivElement, sortableOptions) as SortableInstance; + + return { + sortableLeftInstance: this._sortableLeftInstance, + sortableRightInstance: this._sortableRightInstance + }; } // @@ -342,7 +382,7 @@ export class SlickDraggableGrouping { this.updateGroupBy('add-group'); } - protected addGroupByRemoveClickHandler(id: string | number, _container: HTMLDivElement, headerColumn: JQuery, entry: any) { + protected addGroupByRemoveClickHandler(id: string | number, headerColumnElm: HTMLDivElement, entry: any) { const groupRemoveElm = document.querySelector(`${this.gridUidSelector}_${id}_entry > .slick-groupby-remove`); if (groupRemoveElm) { this._bindEventService.bind(groupRemoveElm, 'click', () => { @@ -350,29 +390,34 @@ export class SlickDraggableGrouping { for (const boundedEvent of boundedElms) { this._bindEventService.unbind(boundedEvent.element, 'click', boundedEvent.listener); } - this.removeGroupBy(id, headerColumn, entry); + this.removeGroupBy(id, headerColumnElm, entry); }); } } - protected handleGroupByDrop(container: HTMLDivElement, headerColumn: JQuery) { - const columnid = headerColumn.attr('id')!.replace(this._gridUid, ''); + protected handleGroupByDrop(containerElm: HTMLDivElement, headerColumnElm: HTMLDivElement) { + const columnId = headerColumnElm.getAttribute('data-id')?.replace(this._gridUid, ''); let columnAllowed = true; - this.columnsGroupBy.forEach(col => { - if (col.id === columnid) { + for (const colGroupBy of this.columnsGroupBy) { + if (colGroupBy.id === columnId) { columnAllowed = false; } - }); + } if (columnAllowed) { - this._gridColumns.forEach(col => { - if (col.id === columnid) { + for (const col of this._gridColumns) { + if (col.id === columnId) { if (col.grouping !== null && !isEmptyObject(col.grouping)) { - const columnName = headerColumn.children('.slick-column-name').first(); - const entryElm = createDomElement('div', { id: `${this._gridUid}_${col.id}_entry`, className: 'slick-dropped-grouping', dataset: { id: `${col.id}` } }); + const columnNameElm = headerColumnElm.querySelector('.slick-column-name'); + const entryElm = createDomElement('div', { + id: `${this._gridUid}_${col.id}_entry`, + className: 'slick-dropped-grouping', + dataset: { id: `${col.id}` } + }); const groupTextElm = createDomElement('div', { - textContent: columnName.length ? columnName.text() : headerColumn.text(), + className: 'slick-dropped-grouping-title', style: { display: 'inline-flex' }, + textContent: columnNameElm ? columnNameElm.textContent : headerColumnElm.textContent, }); entryElm.appendChild(groupTextElm); @@ -381,12 +426,12 @@ export class SlickDraggableGrouping { groupRemoveIconElm.classList.add(...this._addonOptions.deleteIconCssClass.split(' ')); } if (!this._addonOptions.deleteIconCssClass) { - groupRemoveIconElm.classList.add('slick-groupby-remove-image'); + groupRemoveIconElm.classList.add('slick-groupby-remove-icon'); } entryElm.appendChild(groupRemoveIconElm); entryElm.appendChild(document.createElement('div')); - container.appendChild(entryElm); + containerElm.appendChild(entryElm); // if we're grouping by only 1 group, at the root, we'll analyze Toggle All and add collapsed/expanded class if (this._groupToggler && this.columnsGroupBy.length === 0) { @@ -401,10 +446,10 @@ export class SlickDraggableGrouping { } this.addColumnGroupBy(col); - this.addGroupByRemoveClickHandler(col.id, container, headerColumn, entryElm); + this.addGroupByRemoveClickHandler(col.id, headerColumnElm, entryElm); } } - }); + } // show the "Toggle All" when feature is enabled if (this._groupToggler && this.columnsGroupBy.length > 0) { @@ -423,14 +468,14 @@ export class SlickDraggableGrouping { return arrayToModify; } - protected removeGroupBy(id: string | number, _hdrColumn: JQuery, entry: any) { + protected removeGroupBy(id: string | number, _hdrColumnElm: HTMLDivElement, entry: any) { entry.remove(); const groupByColumns: Column[] = []; this._gridColumns.forEach(col => groupByColumns[col.id as number] = col); this.removeFromArray(this.columnsGroupBy, groupByColumns[id as any]); if (this.columnsGroupBy.length === 0) { // show placeholder text & hide the "Toggle All" when that later feature is enabled - this._dropboxPlaceholderElm.style.display = 'inline-block'; + this._dropzonePlaceholderElm.style.display = 'inline-block'; if (this._groupToggler) { this._groupToggler.style.display = 'none'; } @@ -438,35 +483,34 @@ export class SlickDraggableGrouping { this.updateGroupBy('remove-group'); } + protected addDragOverDropzoneListeners() { + if (this._dropzoneElm) { + this._bindEventService.bind(this._dropzoneElm, 'dragover', (e: Event) => e.preventDefault); + this._bindEventService.bind(this._dropzoneElm, 'dragenter', () => this._dropzoneElm.classList.add('slick-dropzone-hover')); + this._bindEventService.bind(this._dropzoneElm, 'dragleave', () => this._dropzoneElm.classList.remove('slick-dropzone-hover')); + } + } + protected setupColumnDropbox() { - this._droppableInstance = ($(this._dropboxElm) as any).droppable({ - activeClass: 'ui-state-default', - hoverClass: 'ui-state-hover', - accept: ':not(.ui-sortable-helper)', - deactivate: () => { - this._dropboxElm.classList.remove('slick-header-column-denied'); - }, - drop: (e: DOMMouseEvent, ui: JQueryUiDraggableOption) => { - this.handleGroupByDrop(e.target, ui.draggable); - }, - over: (_e: Event, ui: JQueryUiDraggableOption) => { - const id = (ui.draggable).attr('id').replace(this._gridUid, ''); - for (const col of this._gridColumns) { - if (col.id === id && !col.grouping) { - this._dropboxElm.classList.add('slick-header-column-denied'); - } + const dropzoneElm = this._dropzoneElm; + + this._droppableInstance = Sortable.create(dropzoneElm, { + group: 'shared', + // chosenClass: 'slick-header-column-active', + ghostClass: 'slick-droppable-sortitem-hover', + draggable: '.slick-dropped-grouping', + dragoverBubble: true, + onAdd: (evt: SortableEvent) => { + const el = evt.item; + if (el.getAttribute('id')?.replace(this._gridUid, '')) { + this.handleGroupByDrop(dropzoneElm, (Sortable.utils as any).clone(evt.item)); } - } - }); - - this._sortableInstance = ($(this._dropboxElm) as any).sortable({ - items: 'div.slick-dropped-grouping', - cursor: 'default', - tolerance: 'pointer', - helper: 'clone', - update: (event: DOMMouseEvent) => { - const sortArray: string[] = ($(event.target) as any).sortable('toArray', { attribute: 'data-id' }); - const newGroupingOrder = []; + evt.clone.style.opacity = '.5'; + el.parentNode?.removeChild(el); + }, + onUpdate: () => { + const sortArray = this._droppableInstance?.toArray() ?? []; + const newGroupingOrder: Column[] = []; for (const sortGroupId of sortArray) { for (const groupByColumn of this.columnsGroupBy) { if (groupByColumn.id === sortGroupId) { @@ -477,11 +521,14 @@ export class SlickDraggableGrouping { } this.columnsGroupBy = newGroupingOrder; this.updateGroupBy('sort-group'); - } + }, }); + // Sortable doesn't have onOver, we need to implement it ourselves + this.addDragOverDropzoneListeners(); + if (this._groupToggler) { - this._bindEventService.bind(this._groupToggler, 'click', ((event: DOMMouseEvent) => { + this._bindEventService.bind(this._groupToggler, 'click', ((event: DOMMouseOrTouchEvent) => { const target = event.target.classList.contains('slick-group-toggle-all-icon') ? event.target : event.currentTarget.querySelector('.slick-group-toggle-all-icon'); this.toggleGroupToggler(target, target?.classList.contains('expanded')); }) as EventListener); @@ -509,14 +556,14 @@ export class SlickDraggableGrouping { protected updateGroupBy(originator: string) { if (this.columnsGroupBy.length === 0) { this.dataView.setGrouping([]); - this._dropboxPlaceholderElm.style.display = 'inline-block'; + this._dropzonePlaceholderElm.style.display = 'inline-block'; this.onGroupChanged.notify({ caller: originator, groupColumns: [] }); return; } const groupingArray: Grouping[] = []; this.columnsGroupBy.forEach(element => groupingArray.push(element.grouping!)); this.dataView.setGrouping(groupingArray); - this._dropboxPlaceholderElm.style.display = 'none'; + this._dropzonePlaceholderElm.style.display = 'none'; this.onGroupChanged.notify({ caller: originator, groupColumns: groupingArray }); } } \ No newline at end of file diff --git a/packages/common/src/extensions/slickGridMenu.ts b/packages/common/src/extensions/slickGridMenu.ts index c8bb44222..85b5e157c 100644 --- a/packages/common/src/extensions/slickGridMenu.ts +++ b/packages/common/src/extensions/slickGridMenu.ts @@ -306,7 +306,8 @@ export class SlickGridMenu extends MenuBaseClass { this.init(); } - repositionMenu(e: MouseEvent, addonOptions: GridMenu, showMenu = true) { + repositionMenu(e: MouseEvent | TouchEvent, addonOptions: GridMenu, showMenu = true) { + const targetEvent: MouseEvent | Touch = (e as TouchEvent)?.touches?.[0] ?? e; if (this._menuElm) { let buttonElm = (e.target as HTMLButtonElement).nodeName === 'BUTTON' ? (e.target as HTMLButtonElement) : (e.target as HTMLElement).querySelector('button') as HTMLButtonElement; // get button element if (!buttonElm) { @@ -349,7 +350,7 @@ export class SlickGridMenu extends MenuBaseClass { if (addonOptions?.height !== undefined) { this._menuElm.style.height = findWidthOrDefault(addonOptions.height, ''); } else { - this._menuElm.style.maxHeight = findWidthOrDefault(addonOptions.maxHeight, `${window.innerHeight - e.clientY - menuMarginBottom}px`); + this._menuElm.style.maxHeight = findWidthOrDefault(addonOptions.maxHeight, `${window.innerHeight - targetEvent.clientY - menuMarginBottom}px`); } this._menuElm.style.display = 'block'; @@ -580,7 +581,7 @@ export class SlickGridMenu extends MenuBaseClass { } // show grid menu: Export to file - if ((this.gridOptions?.enableExport || this.gridOptions?.enableTextExport) && this._gridMenuOptions && !this._gridMenuOptions.hideExportCsvCommand) { + if (this.gridOptions?.enableTextExport && this._gridMenuOptions && !this._gridMenuOptions.hideExportCsvCommand) { const commandName = 'export-csv'; if (!originalCommandItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCommandItems.push( @@ -612,7 +613,7 @@ export class SlickGridMenu extends MenuBaseClass { } // show grid menu: export to text file as tab delimited - if ((this.gridOptions?.enableExport || this.gridOptions?.enableTextExport) && this._gridMenuOptions && !this._gridMenuOptions.hideExportTextDelimitedCommand) { + if (this.gridOptions?.enableTextExport && this._gridMenuOptions && !this._gridMenuOptions.hideExportTextDelimitedCommand) { const commandName = 'export-text-delimited'; if (!originalCommandItems.some(item => item !== 'divider' && item.hasOwnProperty('command') && item.command === commandName)) { gridMenuCommandItems.push( diff --git a/packages/common/src/extensions/slickRowMoveManager.ts b/packages/common/src/extensions/slickRowMoveManager.ts index 7c21b4f9d..b26f4cd68 100644 --- a/packages/common/src/extensions/slickRowMoveManager.ts +++ b/packages/common/src/extensions/slickRowMoveManager.ts @@ -180,11 +180,13 @@ export class SlickRowMoveManager { this.onMoveRows.notify(eventData); } } + protected handleDrag(e: SlickEventData, dd: DragRowMove): boolean | void { if (this._dragging) { e.stopImmediatePropagation(); - const top = e.pageY - (getHtmlElementOffset(this._canvas)?.top ?? 0); + const targetEvent: MouseEvent | Touch = (e as TouchEvent)?.touches?.[0] ?? e; + const top = targetEvent.pageY - (getHtmlElementOffset(this._canvas)?.top ?? 0); dd.selectionProxy.style.top = `${top - 5}px`; dd.selectionProxy.style.display = 'block'; diff --git a/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts b/packages/common/src/filters/__tests__/autocompleterFilter.spec.ts similarity index 67% rename from packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts rename to packages/common/src/filters/__tests__/autocompleterFilter.spec.ts index 73416ccb3..206cba6b5 100644 --- a/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts +++ b/packages/common/src/filters/__tests__/autocompleterFilter.spec.ts @@ -1,9 +1,9 @@ import { of, Subject } from 'rxjs'; import { Filters } from '../index'; -import { AutoCompleteFilter } from '../autoCompleteFilter'; +import { AutocompleterFilter } from '../autocompleterFilter'; import { FieldType, OperatorType, KeyCode } from '../../enums/index'; -import { AutocompleteOption, Column, FilterArguments, GridOption, SlickGrid } from '../../interfaces/index'; +import { AutocompleterOption, Column, ColumnFilter, FilterArguments, GridOption, SlickGrid } from '../../interfaces/index'; import { CollectionService } from '../../services/collection.service'; import { HttpStub } from '../../../../../test/httpClientStub'; import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub'; @@ -28,13 +28,13 @@ const gridStub = { render: jest.fn(), } as unknown as SlickGrid; -describe('AutoCompleteFilter', () => { +describe('AutocompleterFilter', () => { let translaterService: TranslateServiceStub; let divContainer: HTMLDivElement; - let filter: AutoCompleteFilter; + let filter: AutocompleterFilter; let filterArguments: FilterArguments; let spyGetHeaderRow; - let mockColumn: Column; + let mockColumn: Column & { filter: ColumnFilter; }; let collectionService: CollectionService; const http = new HttpStub(); @@ -50,7 +50,7 @@ describe('AutoCompleteFilter', () => { mockColumn = { id: 'gender', field: 'gender', filterable: true, filter: { - model: Filters.autoComplete, + model: Filters.autocompleter, } }; filterArguments = { @@ -60,7 +60,7 @@ describe('AutoCompleteFilter', () => { filterContainerElm: gridStub.getHeaderRowColumn(mockColumn.id) }; - filter = new AutoCompleteFilter(translaterService, collectionService); + filter = new AutocompleterFilter(translaterService, collectionService); }); afterEach(() => { @@ -74,7 +74,7 @@ describe('AutoCompleteFilter', () => { it('should throw an error when there is no collection provided in the filter property', (done) => { try { - mockColumn.filter!.collection = undefined; + mockColumn.filter.collection = undefined; filter.init(filterArguments); } catch (e) { expect(e.toString()).toContain(`[Slickgrid-Universal] You need to pass a "collection" (or "collectionAsync") for the AutoComplete Filter to work correctly.`); @@ -82,44 +82,30 @@ describe('AutoCompleteFilter', () => { } }); - it('should throw an error when collection is not a valid array', (done) => { - try { - mockColumn.filter!.collection = { hello: 'world' } as any; - filter.init(filterArguments); - } catch (e) { - expect(e.toString()).toContain(`The "collection" passed to the Autocomplete Filter is not a valid array.`); - done(); - } - }); - it('should initialize the filter', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; filter.init(filterArguments); const filterCount = divContainer.querySelectorAll('input.search-filter.filter-gender').length; - const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); expect(filter.instance).toBeTruthy(); - expect(autocompleteUlElms.length).toBe(1); expect(spyGetHeaderRow).toHaveBeenCalled(); expect(filterCount).toBe(1); }); it('should initialize the filter even when user define his own filter options', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - mockColumn.filter!.filterOptions = { minLength: 3 } as AutocompleteOption; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.filterOptions = { minLength: 3 } as AutocompleterOption; filter.init(filterArguments); const filterCount = divContainer.querySelectorAll('input.search-filter.filter-gender').length; - const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); - expect(autocompleteUlElms.length).toBe(1); expect(spyGetHeaderRow).toHaveBeenCalled(); expect(filterCount).toBe(1); }); it('should have a placeholder when defined in its column definition', () => { const testValue = 'test placeholder'; - mockColumn.filter!.placeholder = testValue; - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.placeholder = testValue; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; filter.init(filterArguments); const filterElm = divContainer.querySelector('input.search-filter.filter-gender') as HTMLInputElement; @@ -127,9 +113,10 @@ describe('AutoCompleteFilter', () => { expect(filterElm.placeholder).toBe(testValue); }); - it('should call "setValues" and expect that value to be in the callback when triggered', () => { + it('should call "setValues" and expect that value to be in the callback when triggered and triggerOnEveryKeyStroke is enabled', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.filterOptions = { triggerOnEveryKeyStroke: true }; filter.init(filterArguments); filter.setValues('male'); @@ -137,16 +124,17 @@ describe('AutoCompleteFilter', () => { filterElm.focus(); filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 109, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: KeyCode.ENTER, bubbles: true, cancelable: true })); filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 109, bubbles: true, cancelable: true })); const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); expect(filterFilledElms.length).toBe(1); - // expect(autocompleteListElms.length).toBe(2); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true }); }); - it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableFilterTrimWhiteSpace" is enabled in grid options', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableFilterTrimWhiteSpace" is enabled in grid options and triggerOnEveryKeyStroke is enabled', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.filterOptions = { triggerOnEveryKeyStroke: true }; gridOptionMock.enableFilterTrimWhiteSpace = true; const spyCallback = jest.spyOn(filterArguments, 'callback'); @@ -162,10 +150,11 @@ describe('AutoCompleteFilter', () => { expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['abc'], shouldTriggerQuery: true }); }); - it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableTrimWhiteSpace" is enabled in the column filter', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableTrimWhiteSpace" is enabled in the column filter and triggerOnEveryKeyStroke is enabled', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.filterOptions = { triggerOnEveryKeyStroke: true }; gridOptionMock.enableFilterTrimWhiteSpace = false; - mockColumn.filter!.enableTrimWhiteSpace = true; + mockColumn.filter.enableTrimWhiteSpace = true; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); @@ -180,8 +169,9 @@ describe('AutoCompleteFilter', () => { expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['abc'], shouldTriggerQuery: true }); }); - it('should trigger the callback method when user types something in the input', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + it('should trigger the callback method when user types something in the input and triggerOnEveryKeyStroke is enabled', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.filterOptions = { triggerOnEveryKeyStroke: true }; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); @@ -191,12 +181,11 @@ describe('AutoCompleteFilter', () => { filterElm.value = 'a'; filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); - // expect(autocompleteListElms.length).toBe(2); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['a'], shouldTriggerQuery: true }); }); it('should create the input filter with a default search term when passed as a filter argument', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; filterArguments.searchTerms = ['xyz']; filter.init(filterArguments); @@ -206,7 +195,7 @@ describe('AutoCompleteFilter', () => { }); it('should expect the input not to have the "filled" css class when the search term provided is an empty string', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; filterArguments.searchTerms = ['']; filter.init(filterArguments); @@ -218,7 +207,7 @@ describe('AutoCompleteFilter', () => { }); it('should trigger a callback with the clear filter set when calling the "clear" method', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; const spyCallback = jest.spyOn(filterArguments, 'callback'); filterArguments.searchTerms = ['xyz']; @@ -232,8 +221,26 @@ describe('AutoCompleteFilter', () => { expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); }); + it('should expect "clear" method be called when input "blur" event is triggered', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + filterArguments.searchTerms = ['xyz']; + + filter.init(filterArguments); + const clearSpy = jest.spyOn(filter, 'clear'); + const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; + + filterElm.dispatchEvent(new Event('blur', { bubbles: true, cancelable: true })); + + jest.runAllTimers(); // fast-forward timer + + expect(clearSpy).toHaveBeenCalled(); + expect(filterElm.value).toBe(''); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); + }); + it('should trigger a callback with the clear filter but without querying when when calling the "clear" method with False as argument', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; const spyCallback = jest.spyOn(filterArguments, 'callback'); filterArguments.searchTerms = ['xyz']; @@ -247,49 +254,103 @@ describe('AutoCompleteFilter', () => { expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); }); - it('should create the filter with a default search term when using "collectionAsync" as a Promise', async () => { + it('should create the filter with a default search term when using "collectionAsync" as a Promise and triggerOnEveryKeyStroke is enabled', async () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); const mockCollection = ['male', 'female']; - mockColumn.filter!.collectionAsync = Promise.resolve(mockCollection); + mockColumn.filter.collectionAsync = Promise.resolve(mockCollection); + mockColumn.filter.filterOptions = { showOnFocus: true, triggerOnEveryKeyStroke: true } as AutocompleterOption; filterArguments.searchTerms = ['female']; await filter.init(filterArguments); - const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; - const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); - filter.setValues('male'); - filterElm.focus(); filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); + + jest.runAllTimers(); // fast-forward timer + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + const autocompleteListElms = document.body.querySelectorAll('.slick-autocomplete div'); + expect(autocompleteListElms.length).toBe(1); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); + }); - expect(autocompleteUlElms.length).toBe(1); + it('should add custom render callback and expect it to be called when a search is triggered', async () => { + const renderSpy = jest.spyOn(filter, 'renderDomElement'); + const mockDataResponse = [{ value: 'female', label: 'Female' }, { value: 'male', label: 'Male' }]; + const callbackMock = jest.fn().mockReturnValue(mockDataResponse); + + mockColumn.filter = { + filterOptions: { + triggerOnEveryKeyStroke: true, + showOnFocus: true, + fetch: (searchText, updateCallback) => { + callbackMock(searchText); + } + } + }; + + filterArguments.searchTerms = ['female']; + await filter.init(filterArguments); + const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; + filterElm.focus(); + + jest.runAllTimers(); // fast-forward timer + + expect(filter.filterDomElement.classList.contains('slick-autocomplete-loading')).toBeTrue(); + expect(callbackMock).toHaveBeenCalledWith('female'); + expect(renderSpy).toHaveBeenCalledTimes(1); + }); + + it('should add custom "fetch" call and expect "renderRegularItem" callback be called when focusing on the autocomplete input', async () => { + const mockDataResponse = [{ value: 'female', label: 'Female' }, { value: 'undefined', label: 'Undefined' }]; + + mockColumn.filter = { + filterOptions: { + showOnFocus: true, + fetch: (searchText, updateCallback) => { + updateCallback(mockDataResponse); + } + } + }; + + filterArguments.searchTerms = ['female']; + await filter.init(filterArguments); + const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; + filterElm.focus(); + + jest.runAllTimers(); // fast-forward timer + + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + const autocompleteListElms = document.body.querySelectorAll('.slick-autocomplete div'); + expect(autocompleteListElms.length).toBe(2); + expect(autocompleteListElms[0].textContent).toBe('Female'); + expect(autocompleteListElms[1].textContent).toBe('Undefined'); expect(filterFilledElms.length).toBe(1); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true }); }); - it('should create the filter with a default search term when using "collectionAsync" as a Promise with content to simulate http-client', async () => { + it('should create the filter with a default search term when using "collectionAsync" as a Promise with content to simulate http-client and triggerOnEveryKeyStroke is enabled', async () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); const mockCollection = ['male', 'female']; - mockColumn.filter!.collectionAsync = Promise.resolve({ content: mockCollection }); + mockColumn.filter.collectionAsync = Promise.resolve({ content: mockCollection }); + mockColumn.filter.filterOptions = { showOnFocus: true, triggerOnEveryKeyStroke: true } as AutocompleterOption; filterArguments.searchTerms = ['female']; await filter.init(filterArguments); - const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; - const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); - filter.setValues('male'); - filterElm.focus(); filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); - expect(autocompleteUlElms.length).toBe(1); + jest.runAllTimers(); // fast-forward time + + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + const autocompleteListElms = document.body.querySelectorAll('.slick-autocomplete div'); + expect(autocompleteListElms.length).toBe(1); expect(filterFilledElms.length).toBe(1); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true }); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); }); - it('should create the filter with a default search term when using "collectionAsync" is a Fetch Promise', async () => { + it('should create the filter with a default search term when using "collectionAsync" is a Fetch Promise and triggerOnEveryKeyStroke is enabled', async () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); const mockCollection = ['male', 'female']; @@ -298,22 +359,22 @@ describe('AutoCompleteFilter', () => { http.returnKey = 'date'; http.returnValue = '6/24/1984'; http.responseHeaders = { accept: 'json' }; - mockColumn.filter!.collectionAsync = http.fetch('/api', { method: 'GET' }); + mockColumn.filter.collectionAsync = http.fetch('http://locahost/api', { method: 'GET' }); + mockColumn.filter.filterOptions = { showOnFocus: true, triggerOnEveryKeyStroke: true } as AutocompleterOption; filterArguments.searchTerms = ['female']; await filter.init(filterArguments); - const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; - const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); - filter.setValues('male'); - filterElm.focus(); filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('input', { keyCode: 97, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); - expect(autocompleteUlElms.length).toBe(1); + jest.runAllTimers(); // fast-forward time + + const autocompleteListElms = document.body.querySelectorAll('.slick-autocomplete div'); + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + expect(autocompleteListElms.length).toBe(1); expect(filterFilledElms.length).toBe(1); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true }); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); }); it('should create the filter and filter the string collection when "collectionFilterBy" is set', () => { @@ -461,66 +522,37 @@ describe('AutoCompleteFilter', () => { expect(filterCollection[2]).toEqual({ value: 'other', description: 'other' }); }); - describe('onSelect method', () => { - it('should expect "setValue" and "autoCommitEdit" to have been called with a string when item provided is a string', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); - mockColumn.filter!.collection = ['male', 'female']; - mockColumn.filter!.filterOptions = { source: [] } as AutocompleteOption; - - filter.init(filterArguments); - const spySetValue = jest.spyOn(filter, 'setValues'); - const output = filter.onSelect(null as any, { item: 'female' }); - - expect(output).toBe(false); - expect(spySetValue).toHaveBeenCalledWith('female'); - expect(spyCallback).toHaveBeenCalledWith(null as any, { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); - }); - - it('should expect "setValue" and "autoCommitEdit" to have been called with the string label when item provided is an object', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + describe('handleSelect method', () => { + it('should expect the "handleSelect" method to be called when the callback method is triggered when user provide his own filterOptions', () => { + const spy = jest.spyOn(filter, 'handleSelect'); + mockColumn.filter.collection = []; + mockColumn.filter.filterOptions = { minLength: 3 } as AutocompleterOption; filter.init(filterArguments); - const spySetValue = jest.spyOn(filter, 'setValues'); - const output = filter.onSelect(null as any, { item: { value: 'f', label: 'Female' } }); + filter.autocompleterOptions.onSelect({ item: 'fem' }); - expect(output).toBe(false); - expect(spySetValue).toHaveBeenCalledWith('Female'); - expect(spyCallback).toHaveBeenCalledWith(null as any, { columnDef: mockColumn, operator: 'EQ', searchTerms: ['f'], shouldTriggerQuery: true }); + expect(spy).toHaveBeenCalledWith({ item: 'fem' }); }); - it('should expect the "onSelect" method to be called when the callback method is triggered when user provide his own filterOptions', () => { - const spy = jest.spyOn(filter, 'onSelect'); - const event = new CustomEvent('change'); + it('should expect the "handleSelect" method to be called when the callback method is triggered', () => { + const spy = jest.spyOn(filter, 'handleSelect'); - mockColumn.filter!.filterOptions = { source: [], minLength: 3 } as AutocompleteOption; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; filter.init(filterArguments); - filter.autoCompleteOptions!.select!(event, { item: 'fem' }); + filter.autocompleterOptions.onSelect({ item: 'fem' }); - expect(spy).toHaveBeenCalledWith(event, { item: 'fem' }); + expect(spy).toHaveBeenCalledWith({ item: 'fem' }); }); - it('should expect the "onSelect" method to be called when the callback method is triggered', () => { - const spy = jest.spyOn(filter, 'onSelect'); - const event = new CustomEvent('change'); + it('should initialize the filter with filterOptions and expect the "handleSelect" method to be called when the callback method is triggered', () => { + const spy = jest.spyOn(filter, 'handleSelect'); - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.filterOptions = { minLength: 3 } as AutocompleterOption; filter.init(filterArguments); - filter.autoCompleteOptions!.select!(event, { item: 'fem' }); + filter.autocompleterOptions.onSelect({ item: 'fem' }); - expect(spy).toHaveBeenCalledWith(event, { item: 'fem' }); - }); - - it('should initialize the filter with filterOptions and expect the "onSelect" method to be called when the callback method is triggered', () => { - const spy = jest.spyOn(filter, 'onSelect'); - const event = new CustomEvent('change'); - - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - mockColumn.filter!.filterOptions = { minLength: 3 } as AutocompleteOption; - filter.init(filterArguments); - filter.autoCompleteOptions!.select!(event, { item: 'fem' }); - - expect(spy).toHaveBeenCalledWith(event, { item: 'fem' }); + expect(spy).toHaveBeenCalledWith({ item: 'fem' }); }); it('should trigger a re-render of the DOM element when collection is replaced by new collection', async () => { @@ -535,10 +567,10 @@ describe('AutoCompleteFilter', () => { }; await filter.init(filterArguments); - mockColumn.filter!.collection = newCollection; - mockColumn.filter!.collection!.push({ value: 'val3', label: 'label3' }); + mockColumn.filter.collection = newCollection; + mockColumn.filter.collection.push({ value: 'val3', label: 'label3' }); - jest.runAllTimers(); // fast-forward timer] + jest.runAllTimers(); // fast-forward timer expect(renderSpy).toHaveBeenCalledTimes(3); expect(renderSpy).toHaveBeenCalledWith(newCollection); @@ -555,106 +587,47 @@ describe('AutoCompleteFilter', () => { }; await filter.init(filterArguments); - mockColumn.filter!.collection!.push({ value: 'other', label: 'other' }); + mockColumn.filter.collection!.push({ value: 'other', label: 'other' }); jest.runAllTimers(); // fast-forward timer expect(renderSpy).toHaveBeenCalledTimes(2); - expect(renderSpy).toHaveBeenCalledWith(mockColumn.filter!.collection); - }); - }); - - describe('openSearchListOnFocus flag', () => { - it('should open the search list by calling the AutoComplete "search" event with an empty string when there are no search term provided', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - mockColumn.filter!.filterOptions = { openSearchListOnFocus: true } as AutocompleteOption; - - const event = new (window.window as any).KeyboardEvent('click', { keyCode: KeyCode.LEFT, bubbles: true, cancelable: true }); - - filter.init(filterArguments); - const autoCompleteSpy = jest.spyOn(filter.filterDomElement, 'autocomplete'); - const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; - filterElm.focus(); - filterElm.dispatchEvent(event); - - expect(filter.filterDomElement).toBeTruthy(); - expect(autoCompleteSpy).toHaveBeenCalledWith('search', ''); - }); - - it('should open the search list by calling the AutoComplete "search" event with the same search term string that was provided', () => { - mockColumn.filter!.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - mockColumn.filter!.filterOptions = { openSearchListOnFocus: true } as AutocompleteOption; - - const event = new (window.window as any).KeyboardEvent('click', { keyCode: KeyCode.LEFT, bubbles: true, cancelable: true }); - - filter.init(filterArguments); - filter.setValues('female'); - const autoCompleteSpy = jest.spyOn(filter.filterDomElement, 'autocomplete'); - const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; - filterElm.focus(); - filterElm.dispatchEvent(event); - - expect(filter.filterDomElement).toBeTruthy(); - expect(autoCompleteSpy).toHaveBeenCalledWith('search', 'female'); + expect(renderSpy).toHaveBeenCalledWith(mockColumn.filter.collection); }); }); describe('renderItem callback method', () => { - it('should be able to override any jQuery UI callback method', () => { - const mockCallback = (ul: HTMLElement, item: any) => { - return $('
  • ') - .data('item.autocomplete', item) - .append(`
    Hello World`) - .appendTo(ul); - }; - mockColumn.filter!.filterOptions = { - source: [], - classes: { 'ui-autocomplete': 'autocomplete-custom-four-corners' }, - } as AutocompleteOption; - mockColumn.filter!.callbacks = { _renderItem: mockCallback }; - - filter.init(filterArguments); - const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; - filterElm.focus(); - - expect(filter.filterDomElement).toBeTruthy(); - expect(filter.instance).toBeTruthy(); - expect(filter.instance._renderItem).toEqual(mockCallback); - }); - - it('should provide "renderItem" in the "filterOptions" and expect the jQueryUI "_renderItem" to be overriden', () => { + it('should provide "renderItem" in the "filterOptions" and expect the autocomplete "render" to be overriden', () => { const mockTemplateString = `
    Hello World
    `; const mockTemplateCallback = () => mockTemplateString; - mockColumn.filter!.filterOptions = { - source: [], + mockColumn.filter.collection = ['male', 'female']; + mockColumn.filter.filterOptions = { + showOnFocus: true, renderItem: { layout: 'fourCorners', templateCallback: mockTemplateCallback }, - } as AutocompleteOption; + } as AutocompleterOption; filter.init(filterArguments); - const autoCompleteSpy = jest.spyOn(filter.filterDomElement, 'autocomplete'); const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; filterElm.focus(); - filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 109, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 97, bubbles: true, cancelable: true })); + + jest.runAllTimers(); // fast-forward timer + const autocompleteListElms = document.body.querySelectorAll('.autocomplete-custom-four-corners'); expect(filter.filterDomElement).toBeTruthy(); expect(filter.instance).toBeTruthy(); - expect(filter.autoCompleteOptions).toEqual(expect.objectContaining({ classes: { 'ui-autocomplete': 'autocomplete-custom-four-corners' } })); - expect(autoCompleteSpy).toHaveBeenCalledWith('instance'); - expect(filter.instance._renderItem).toEqual(expect.any(Function)); - - const ulElm = document.createElement('ul'); - filter.instance._renderItem(ulElm, { name: 'John' }); - - const liElm = ulElm.querySelector('li') as HTMLLIElement; - expect(liElm.innerHTML).toBe(mockTemplateString); + expect(filter.autocompleterOptions.render).toEqual(expect.any(Function)); + expect(autocompleteListElms.length).toBe(1); + expect(autocompleteListElms[0].innerHTML).toContain(mockTemplateString); }); it('should throw an error when "collectionAsync" Promise does not return a valid array', (done) => { const promise = Promise.resolve({ hello: 'world' }); - mockColumn.filter!.collectionAsync = promise; + mockColumn.filter.collectionAsync = promise; + mockColumn.filter.filterOptions = { showOnFocus: true } as AutocompleterOption; filter.init(filterArguments).catch((e) => { expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Filter, the collection is not a valid array.`); done(); @@ -662,12 +635,11 @@ describe('AutoCompleteFilter', () => { }); }); - describe('AutoCompleteFilter using RxJS Observables', () => { + describe('AutocompleterFilter using RxJS Observables', () => { let divContainer: HTMLDivElement; - let filter: AutoCompleteFilter; + let filter: AutocompleterFilter; let filterArguments: FilterArguments; - let spyGetHeaderRow; - let mockColumn: Column; + let mockColumn: Column & { filter: ColumnFilter; }; let collectionService: CollectionService; let rxjs: RxJsResourceStub; let translaterService: TranslateServiceStub; @@ -686,7 +658,7 @@ describe('AutoCompleteFilter', () => { mockColumn = { id: 'gender', field: 'gender', filterable: true, filter: { - model: Filters.autoComplete, + model: Filters.autocompleter, } }; filterArguments = { @@ -696,7 +668,7 @@ describe('AutoCompleteFilter', () => { filterContainerElm: gridStub.getHeaderRowColumn(mockColumn.id) }; - filter = new AutoCompleteFilter(translaterService, collectionService, rxjs); + filter = new AutocompleterFilter(translaterService, collectionService, rxjs); }); afterEach(() => { @@ -704,47 +676,54 @@ describe('AutoCompleteFilter', () => { jest.clearAllMocks(); }); - it('should create the filter with a default search term when using "collectionAsync" as an Observable', async () => { + it('should create the filter with a default search term when using "collectionAsync" as an Observable and triggerOnEveryKeyStroke is enabled', async () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); mockColumn.filter.collectionAsync = of(['male', 'female']); + mockColumn.filter.filterOptions = { showOnFocus: true, triggerOnEveryKeyStroke: true } as AutocompleterOption; filterArguments.searchTerms = ['female']; await filter.init(filterArguments); - const filterElm = divContainer.querySelector('input.filter-gender'); - const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); - filter.setValues('male'); + const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; + // filter.setValues('male'); filterElm.focus(); filterElm.dispatchEvent(new (window.window as any).Event('input', { keyCode: 97, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); - expect(autocompleteUlElms.length).toBe(1); + jest.runAllTimers(); // fast-forward time + + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + const autocompleteListElms = document.body.querySelectorAll('.slick-autocomplete div'); + expect(autocompleteListElms.length).toBe(1); expect(filterFilledElms.length).toBe(1); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true }); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); }); - it('should create the multi-select filter with a "collectionAsync" as an Observable and be able to call next on it', async () => { + it('should create the autocomplete filter with a "collectionAsync" as an Observable and be able to call next on it and triggerOnEveryKeyStroke is enabled', async () => { const mockCollection = ['male', 'female']; mockColumn.filter.collectionAsync = of(mockCollection); + mockColumn.filter.filterOptions = { showOnFocus: true } as AutocompleterOption; filterArguments.searchTerms = ['female']; await filter.init(filterArguments); - const filterElm = divContainer.querySelector('input.filter-gender'); - filter.setValues('male'); + const filterElm = divContainer.querySelector('input.filter-gender') as HTMLInputElement; + // filter.setValues('male'); filterElm.focus(); filterElm.dispatchEvent(new (window.window as any).Event('input', { keyCode: 97, bubbles: true, cancelable: true })); + // after await (or timeout delay) we'll get the Subject Observable mockCollection.push('other'); (mockColumn.filter.collectionAsync as Subject).next(mockCollection); - const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); + jest.runAllTimers(); // fast-forward time + + const autocompleteListElms = document.body.querySelectorAll('.slick-autocomplete div'); const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); - expect(autocompleteUlElms.length).toBe(1); + expect(autocompleteListElms.length).toBe(1); expect(filterFilledElms.length).toBe(1); }); @@ -761,8 +740,9 @@ describe('AutoCompleteFilter', () => { }; await filter.init(filterArguments); + jest.runAllTimers(); // fast-forward time - const filterCollection = filter.collection; + const filterCollection = filter.collection as any[]; expect(filterCollection.length).toBe(3); expect(filterCollection[0]).toEqual({ value: 'other', description: 'other' }); diff --git a/packages/common/src/filters/__tests__/filterFactory.spec.ts b/packages/common/src/filters/__tests__/filterFactory.spec.ts index 90d899638..0a538698a 100644 --- a/packages/common/src/filters/__tests__/filterFactory.spec.ts +++ b/packages/common/src/filters/__tests__/filterFactory.spec.ts @@ -5,19 +5,19 @@ import { SlickgridConfig } from '../../slickgrid-config'; import { CollectionService } from '../../services/collection.service'; import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; -import { AutoCompleteFilter } from '../autoCompleteFilter'; +import { AutocompleterFilter } from '../autocompleterFilter'; -const mockAutocompleteFilter = jest.fn().mockImplementation(() => ({ +const mockAutocompleterFilter = jest.fn().mockImplementation(() => ({ constructor: jest.fn(), init: jest.fn(), destroy: jest.fn(), -} as unknown as AutoCompleteFilter)); +} as unknown as AutocompleterFilter)); describe('Filter Factory', () => { - jest.mock('../autoCompleteFilter', () => mockAutocompleteFilter); + jest.mock('../autocompleterFilter', () => mockAutocompleterFilter); const Filters = { input: InputFilter, - autoComplete: mockAutocompleteFilter + autocompleter: mockAutocompleterFilter }; let factory: FilterFactory; let collectionService: CollectionService; @@ -48,8 +48,8 @@ describe('Filter Factory', () => { }); it('should create AutoComplete Filter when that is the Filter provided as a model', () => { - const mockColumn = { filter: { model: Filters.autoComplete } } as Column; - const filterSpy = jest.spyOn(mockAutocompleteFilter.prototype, 'constructor'); + const mockColumn = { filter: { model: Filters.autocompleter } } as Column; + const filterSpy = jest.spyOn(mockAutocompleterFilter.prototype, 'constructor'); const newFilter = factory.createFilter(mockColumn.filter); @@ -59,8 +59,8 @@ describe('Filter Factory', () => { it('should create AutoComplete Filter with RxJS when that is the Filter provided as a model', () => { factory = new FilterFactory(slickgridConfig, translateService, collectionService, rxjsResourceStub); - const mockColumn = { filter: { model: Filters.autoComplete } } as Column; - const filterSpy = jest.spyOn(mockAutocompleteFilter.prototype, 'constructor'); + const mockColumn = { filter: { model: Filters.autocompleter } } as Column; + const filterSpy = jest.spyOn(mockAutocompleterFilter.prototype, 'constructor'); const newFilter = factory.createFilter(mockColumn.filter); @@ -69,8 +69,8 @@ describe('Filter Factory', () => { }); it('should create AutoComplete Filter with RxJS when that is the Filter provided as a model', () => { - const mockColumn = { filter: { model: Filters.autoComplete } } as Column; - const filterSpy = jest.spyOn(mockAutocompleteFilter.prototype, 'constructor'); + const mockColumn = { filter: { model: Filters.autocompleter } } as Column; + const filterSpy = jest.spyOn(mockAutocompleterFilter.prototype, 'constructor'); factory.addRxJsResource(rxjsResourceStub); const newFilter = factory.createFilter(mockColumn.filter); diff --git a/packages/common/src/filters/__tests__/selectFilter.spec.ts b/packages/common/src/filters/__tests__/selectFilter.spec.ts index 834837a02..e74300e28 100644 --- a/packages/common/src/filters/__tests__/selectFilter.spec.ts +++ b/packages/common/src/filters/__tests__/selectFilter.spec.ts @@ -558,7 +558,7 @@ describe('SelectFilter', () => { filter.clear(); const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); - expect(filter.searchTerms.length).toBe(0); + expect(filter.searchTerms!.length).toBe(0); expect(filterFilledElms.length).toBe(0); expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); }); @@ -572,7 +572,7 @@ describe('SelectFilter', () => { filter.clear(false); const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); - expect(filter.searchTerms.length).toBe(0); + expect(filter.searchTerms!.length).toBe(0); expect(filterFilledElms.length).toBe(0); expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); }); @@ -696,7 +696,7 @@ describe('SelectFilter', () => { http.returnKey = 'date'; http.returnValue = '6/24/1984'; http.responseHeaders = { accept: 'json' }; - mockColumn.filter!.collectionAsync = http.fetch('/api', { method: 'GET' }); + mockColumn.filter!.collectionAsync = http.fetch('http://locahost/api', { method: 'GET' }); filterArguments.searchTerms = ['female']; await filter.init(filterArguments); @@ -881,7 +881,7 @@ describe('SelectFilter', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); const mockCollection = ['male', 'female']; mockColumn.filter!.collection = undefined; - mockColumn.filter.collectionAsync = of(mockCollection); + mockColumn.filter!.collectionAsync = of(mockCollection); filterArguments.searchTerms = ['female']; await filter.init(filterArguments); @@ -901,12 +901,12 @@ describe('SelectFilter', () => { it('should create the multi-select filter with a "collectionAsync" as an Observable and be able to call next on it', async () => { const mockCollection = ['male', 'female']; - mockColumn.filter.collectionAsync = of(mockCollection); + mockColumn.filter!.collectionAsync = of(mockCollection); filterArguments.searchTerms = ['female']; await filter.init(filterArguments); - const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); + const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice') as HTMLButtonElement; const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); filterBtnElm.click(); @@ -915,7 +915,7 @@ describe('SelectFilter', () => { // after await (or timeout delay) we'll get the Subject Observable mockCollection.push('other'); - (mockColumn.filter.collectionAsync as Subject).next(mockCollection); + (mockColumn.filter!.collectionAsync as Subject).next(mockCollection); const filterUpdatedListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); expect(filterUpdatedListElm.length).toBe(3); @@ -937,7 +937,7 @@ describe('SelectFilter', () => { filterArguments.searchTerms = ['female']; await filter.init(filterArguments); - const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); + const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice') as HTMLButtonElement; const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); filterBtnElm.click(); @@ -946,14 +946,14 @@ describe('SelectFilter', () => { // after await (or timeout delay) we'll get the Subject Observable mockCollection.deep.myCollection.push('other'); - (mockColumn.filter.collectionAsync as Subject).next(mockCollection.deep.myCollection); + (mockColumn.filter!.collectionAsync as Subject).next(mockCollection.deep.myCollection); const filterUpdatedListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); expect(filterUpdatedListElm.length).toBe(3); }); it('should throw an error when "collectionAsync" Observable does not return a valid array', (done) => { - mockColumn.filter.collectionAsync = of({ hello: 'world' }); + mockColumn.filter!.collectionAsync = of({ hello: 'world' }); filter.init(filterArguments).catch((e) => { expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Filter, the collection is not a valid array.`); done(); diff --git a/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts b/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts deleted file mode 100644 index 1d9d56d58..000000000 --- a/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { Filters } from '../filters.index'; -import { Column, FilterArguments, GridOption, JQueryUiSliderOption, SlickGrid } from '../../interfaces/index'; -import { SliderRangeFilter } from '../sliderRangeFilter'; - -const containerId = 'demo-container'; - -// define a
    container to simulate the grid container -const template = `
    `; - -const gridOptionMock = { - enableFiltering: true, - enableFilterTrimWhiteSpace: true, -} as GridOption; - -const gridStub = { - getOptions: () => gridOptionMock, - getColumns: jest.fn(), - getHeaderRowColumn: jest.fn(), - render: jest.fn(), -} as unknown as SlickGrid; - -describe('SliderRangeFilter', () => { - let divContainer: HTMLDivElement; - let filter: SliderRangeFilter; - let filterArguments: FilterArguments; - let spyGetHeaderRow; - let mockColumn: Column; - - beforeEach(() => { - divContainer = document.createElement('div'); - divContainer.innerHTML = template; - document.body.appendChild(divContainer); - spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer); - - mockColumn = { id: 'duration', field: 'duration', filterable: true, filter: { model: Filters.sliderRange } }; - filterArguments = { - grid: gridStub, - columnDef: mockColumn, - callback: jest.fn(), - filterContainerElm: gridStub.getHeaderRowColumn(mockColumn.id) - }; - - filter = new SliderRangeFilter(); - }); - - afterEach(() => { - filter.destroy(); - }); - - it('should throw an error when trying to call init without any arguments', () => { - expect(() => filter.init(null as any)).toThrowError('[Slickgrid-Universal] A filter must always have an "init()" with valid arguments.'); - }); - - it('should throw an error when trying to override the slider "change" method', (done) => { - try { - mockColumn.filter!.filterOptions = { change: () => { } } as JQueryUiSliderOption; - filter.init(filterArguments); - } catch (e) { - expect(e.toString()).toContain(`[Slickgrid-Universal] You cannot override the "change" and/or the "slide" callback methods`); - done(); - } - }); - - it('should throw an error when trying to override the slider "slide" method', (done) => { - try { - mockColumn.filter!.filterOptions = { slide: () => { } } as JQueryUiSliderOption; - filter.init(filterArguments); - } catch (e) { - expect(e.toString()).toContain(`[Slickgrid-Universal] You cannot override the "change" and/or the "slide" callback methods`); - done(); - } - }); - - it('should initialize the filter', () => { - filter.init(filterArguments); - const filterCount = divContainer.querySelectorAll('.search-filter.slider-range-container.filter-duration').length; - - expect(spyGetHeaderRow).toHaveBeenCalled(); - expect(filterCount).toBe(1); - }); - - it('should be able to retrieve default slider options through the Getter', () => { - filter.init(filterArguments); - - expect(filter.sliderOptions).toEqual({ - change: expect.anything(), - max: 100, - min: 0, - range: true, - slide: expect.anything(), - step: 1, - values: [0, 100], - }); - }); - - it('should be able to retrieve slider options defined through the Getter when passing different filterOptions', () => { - mockColumn.filter = { - minValue: 4, - maxValue: 69, - valueStep: 5, - }; - filter.init(filterArguments); - - expect(filter.sliderOptions).toEqual({ - change: expect.anything(), - max: 69, - min: 4, - range: true, - slide: expect.anything(), - step: 5, - values: [4, 69], - }); - }); - - it('should call "setValues" and expect that value to be in the callback when triggered', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); - - filter.init(filterArguments); - filter.setValues(['2..80']); - - expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [2, 80], shouldTriggerQuery: true }); - }); - - it('should call "setValues" and expect that value to be in the callback when triggered', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); - - filter.init(filterArguments); - filter.setValues([3, 84]); - - expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [3, 84], shouldTriggerQuery: true }); - }); - - it('should be able to call "setValues" and set empty values and the input to not have the "filled" css class', () => { - filter.init(filterArguments); - filter.setValues([3, 80]); - let filledInputElm = divContainer.querySelector('.search-filter.slider-range-container.filter-duration.filled') as HTMLInputElement; - - expect(filledInputElm).toBeTruthy(); - - filter.setValues(''); - filledInputElm = divContainer.querySelector('.search-filter.slider-range-container.filter-duration.filled') as HTMLInputElement; - expect(filledInputElm).toBeFalsy(); - }); - - it('should create the input filter with default search terms range when passed as a filter argument', () => { - filterArguments.searchTerms = [3, 80]; - - filter.init(filterArguments); - - const filterLowestElm = divContainer.querySelector('.lowest-range-duration') as HTMLInputElement; - const filterHighestElm = divContainer.querySelector('.highest-range-duration') as HTMLInputElement; - - expect(filterLowestElm.textContent).toBe('3'); - expect(filterHighestElm.textContent).toBe('80'); - expect(filter.currentValues).toEqual([3, 80]); - }); - - it('should create the input filter with min/max slider values being set by filter "minValue" and "maxValue"', () => { - mockColumn.filter = { - minValue: 4, - maxValue: 69, - }; - - filter.init(filterArguments); - - const filterLowestElm = divContainer.querySelector('.lowest-range-duration') as HTMLInputElement; - const filterHighestElm = divContainer.querySelector('.highest-range-duration') as HTMLInputElement; - - expect(filterLowestElm.textContent).toBe('4'); - expect(filterHighestElm.textContent).toBe('69'); - expect(filter.currentValues).toEqual([4, 69]); - }); - - it('should create the input filter with min/max slider values being set by filter "sliderStartValue" and "sliderEndValue" through the filter params', () => { - mockColumn.filter = { - params: { - sliderStartValue: 4, - sliderEndValue: 69, - } - }; - - filter.init(filterArguments); - - const filterLowestElm = divContainer.querySelector('.lowest-range-duration') as HTMLInputElement; - const filterHighestElm = divContainer.querySelector('.highest-range-duration') as HTMLInputElement; - - expect(filterLowestElm.textContent).toBe('4'); - expect(filterHighestElm.textContent).toBe('69'); - expect(filter.currentValues).toEqual([4, 69]); - }); - - it('should create the input filter with default search terms range but without showing side numbers when "hideSliderNumbers" is set in params', () => { - filterArguments.searchTerms = [3, 80]; - mockColumn.filter!.params = { hideSliderNumbers: true }; - - filter.init(filterArguments); - - const filterLowestElms = divContainer.querySelectorAll('.lowest-range-duration'); - const filterHighestElms = divContainer.querySelectorAll('.highest-range-duration'); - - expect(filterLowestElms.length).toBe(0); - expect(filterHighestElms.length).toBe(0); - expect(filter.currentValues).toEqual([3, 80]); - }); - - it('should trigger a callback with the clear filter set when calling the "clear" method', () => { - filterArguments.searchTerms = [3, 80]; - const spyCallback = jest.spyOn(filterArguments, 'callback'); - - filter.init(filterArguments); - filter.clear(); - - expect(filter.currentValues).toEqual([0, 100]); - expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); - }); - - it('should trigger a callback with the clear filter but without querying when when calling the "clear" method with False as argument', () => { - filterArguments.searchTerms = [3, 80]; - const spyCallback = jest.spyOn(filterArguments, 'callback'); - - filter.init(filterArguments); - filter.clear(false); - - expect(filter.currentValues).toEqual([0, 100]); - expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); - }); - - it('should trigger a callback with the clear filter set when calling the "clear" method and expect min/max slider values being with values of "sliderStartValue" and "sliderEndValue" when defined through the filter params', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); - mockColumn.filter = { - params: { - sliderStartValue: 4, - sliderEndValue: 69, - } - }; - - filter.init(filterArguments); - filter.clear(false); - - expect(filter.currentValues).toEqual([4, 69]); - expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); - }); - - it('should expect the slider values to be rendered when the callback method is triggered', () => { - const spy = jest.spyOn(filter, 'renderSliderValues'); - - mockColumn.filter = { minValue: 4, maxValue: 69, valueStep: 5 }; - filter.init(filterArguments); - filter.sliderOptions.slide!(new CustomEvent('change'), { handle: document.createElement('div'), handleIndex: 1, value: 2, values: [2, 3] }); - - expect(spy).toHaveBeenCalledWith(2, 3); - expect(filter.sliderOptions).toEqual({ - change: expect.anything(), - max: 69, - min: 4, - range: true, - slide: expect.anything(), - step: 5, - values: [4, 69], - }); - }); - - it('should expect the slider values to be rendered when the callback method is triggered', () => { - const spy = jest.spyOn(filter, 'renderSliderValues'); - - mockColumn.filter = { minValue: 4, maxValue: 69, valueStep: 5 }; - filter.init(filterArguments); - filter.sliderOptions.slide!(new CustomEvent('change'), { handle: document.createElement('div'), handleIndex: 1, value: 2, values: [2, 3] }); - - expect(spy).toHaveBeenCalledWith(2, 3); - expect(filter.sliderOptions).toEqual({ - change: expect.anything(), - max: 69, - min: 4, - range: true, - slide: expect.anything(), - step: 5, - values: [4, 69], - }); - }); -}); diff --git a/packages/common/src/filters/autoCompleteFilter.ts b/packages/common/src/filters/autocompleterFilter.ts similarity index 52% rename from packages/common/src/filters/autoCompleteFilter.ts rename to packages/common/src/filters/autocompleterFilter.ts index a8c2ceb5b..18c98335d 100644 --- a/packages/common/src/filters/autoCompleteFilter.ts +++ b/packages/common/src/filters/autocompleterFilter.ts @@ -1,5 +1,8 @@ -import { toKebabCase } from '@slickgrid-universal/utils'; -import 'jquery-ui/ui/widgets/autocomplete'; +import * as autocompleter_ from 'autocompleter'; +const autocomplete = (autocompleter_ && autocompleter_['default'] || autocompleter_) as (settings: AutocompleteSettings) => AutocompleteResult; // patch for rollup + +import { AutocompleteItem, AutocompleteResult, AutocompleteSettings } from 'autocompleter'; +import { isPrimmitive, toKebabCase, toSentenceCase } from '@slickgrid-universal/utils'; import { FieldType, @@ -8,17 +11,24 @@ import { SearchTerm, } from '../enums/index'; import { - AutocompleteOption, + AutocompleterOption, + AutocompleteSearchItem, CollectionCustomStructure, CollectionOption, Column, ColumnFilter, + DOMEvent, Filter, FilterArguments, FilterCallback, + FilterCallbackArg, GridOption, + Locale, SlickGrid, -} from './../interfaces/index'; +} from '../interfaces/index'; +import { addAutocompleteLoadingByOverridingFetch } from '../commonEditorFilter'; +import { createDomElement, emptyElement, } from '../services'; +import { BindingEventService } from '../services/bindingEvent.service'; import { CollectionService } from '../services/collection.service'; import { collectionObserver, propertyObserver } from '../services/observers'; import { sanitizeTextByAvailableSanitizer, } from '../services/domUtilities'; @@ -26,24 +36,27 @@ import { getDescendantProperty, unsubscribeAll } from '../services/utilities'; import { TranslaterService } from '../services/translater.service'; import { renderCollectionOptionsAsync } from './filterUtilities'; import { RxJsFacade, Subscription } from '../services/rxjsFacade'; +import { Constants } from '../constants'; -export class AutoCompleteFilter implements Filter { - protected _autoCompleteOptions!: AutocompleteOption; +export class AutocompleterFilter implements Filter { + protected _autocompleterOptions!: Partial>; + protected _bindEventService: BindingEventService; protected _clearFilterTriggered = false; protected _collection?: any[]; + protected _filterElm!: HTMLInputElement; + protected _instance: any; + protected _locales!: Locale; protected _shouldTriggerQuery = true; /** DOM Element Name, useful for auto-detecting positioning (dropup / dropdown) */ elementName!: string; - /** The JQuery DOM element */ - $filterElm: any; - grid!: SlickGrid; searchTerms: SearchTerm[] = []; columnDef!: Column; callback!: FilterCallback; isFilled = false; + isItemSelected = false; filterContainerElm!: HTMLDivElement; /** The property name for labels in the collection */ @@ -71,16 +84,18 @@ export class AutoCompleteFilter implements Filter { protected readonly translaterService: TranslaterService, protected readonly collectionService: CollectionService, protected readonly rxjs?: RxJsFacade - ) { } + ) { + this._bindEventService = new BindingEventService(); + } /** Getter for the Autocomplete Option */ - get autoCompleteOptions(): Partial { - return this._autoCompleteOptions || {}; + get autocompleterOptions(): any { + return this._autocompleterOptions || {}; } /** Getter for the Collection Options */ protected get collectionOptions(): CollectionOption { - return this.columnDef && this.columnDef.filter && this.columnDef.filter.collectionOptions || {}; + return this.columnDef?.filter?.collectionOptions ?? {}; } /** Getter for the Collection Used by the Filter */ @@ -95,10 +110,10 @@ export class AutoCompleteFilter implements Filter { /** Getter for the Editor DOM Element */ get filterDomElement(): any { - return this.$filterElm; + return this._filterElm; } - get filterOptions(): AutocompleteOption { + get filterOptions(): any { return this.columnFilter?.filterOptions || {}; } @@ -125,9 +140,9 @@ export class AutoCompleteFilter implements Filter { return (this.grid && this.grid.getOptions) ? this.grid.getOptions() : {}; } - /** jQuery UI AutoComplete instance */ + /** Kraaden AutoComplete instance */ get instance(): any { - return this.$filterElm.autocomplete('instance'); + return this._instance; } /** Getter of the Operator to use when doing the filter comparing */ @@ -156,17 +171,24 @@ export class AutoCompleteFilter implements Filter { this.filterContainerElm = args.filterContainerElm; if (!this.grid || !this.columnDef || !this.columnFilter || (!this.columnFilter.collection && !this.columnFilter.collectionAsync && !this.columnFilter.filterOptions)) { - throw new Error(`[Slickgrid-Universal] You need to pass a "collection" (or "collectionAsync") for the AutoComplete Filter to work correctly. Also each option should include a value/label pair (or value/labelKey when using Locale). For example:: { filter: model: Filters.autoComplete, collection: [{ value: true, label: 'True' }, { value: false, label: 'False'}] }`); + throw new Error( + `[Slickgrid-Universal] You need to pass a "collection" (or "collectionAsync") for the AutoComplete Filter to work correctly.` + + ` Also each option should include a value/label pair (or value/labelKey when using Locale).` + + ` For example:: { filter: model: Filters.autocompleter, collection: [{ value: true, label: 'True' }, { value: false, label: 'False'}] }` + ); } - this.enableTranslateLabel = this.columnFilter && this.columnFilter.enableTranslateLabel || false; - this.labelName = this.customStructure && this.customStructure.label || 'label'; - this.valueName = this.customStructure && this.customStructure.value || 'value'; - this.labelPrefixName = this.customStructure && this.customStructure.labelPrefix || 'labelPrefix'; - this.labelSuffixName = this.customStructure && this.customStructure.labelSuffix || 'labelSuffix'; + this.enableTranslateLabel = this.columnFilter?.enableTranslateLabel ?? false; + this.labelName = this.customStructure?.label ?? 'label'; + this.valueName = this.customStructure?.value ?? 'value'; + this.labelPrefixName = this.customStructure?.labelPrefix ?? 'labelPrefix'; + this.labelSuffixName = this.customStructure?.labelSuffix ?? 'labelSuffix'; + + // get locales provided by user in main file or else use default English locales via the Constants + this._locales = this.gridOptions?.locales ?? Constants.locales; // always render the DOM element - const newCollection = this.columnFilter.collection || []; + const newCollection = this.columnFilter.collection; this._collection = newCollection; this.renderDomElement(newCollection); @@ -201,13 +223,13 @@ export class AutoCompleteFilter implements Filter { * Clear the filter value */ clear(shouldTriggerQuery = true) { - if (this.$filterElm) { + if (this._filterElm) { this._clearFilterTriggered = true; this._shouldTriggerQuery = shouldTriggerQuery; this.searchTerms = []; - this.$filterElm.val(''); - this.$filterElm.trigger('input'); - this.$filterElm.removeClass('filled'); + this._filterElm.value = ''; + this._filterElm.dispatchEvent(new CustomEvent('input')); + this._filterElm.classList.remove('filled'); } } @@ -215,27 +237,32 @@ export class AutoCompleteFilter implements Filter { * destroy the filter */ destroy() { - if (this.$filterElm) { - this.$filterElm.autocomplete('destroy'); - this.$filterElm.off('input').remove(); + this._instance?.destroy(); + if (this._filterElm) { + // this._filterElm.autocomplete('destroy'); + // this._filterElm.off('input').remove(); } - this.$filterElm = null; + this._filterElm?.remove?.(); this._collection = undefined; + this._bindEventService.unbindAll(); // unsubscribe all the possible Observables if RxJS was used unsubscribeAll(this.subscriptions); } getValues() { - return this.$filterElm.val(); + return this._filterElm?.value; } /** Set value(s) on the DOM element */ setValues(values: SearchTerm | SearchTerm[], operator?: OperatorType | OperatorString) { - if (values) { - this.$filterElm.val(values); + if (values && this._filterElm) { + this._filterElm.value = values as string; } - this.getValues() !== '' ? this.$filterElm.addClass('filled') : this.$filterElm.removeClass('filled'); + + // add/remove "filled" class name + const classCmd = this.getValues() !== '' ? 'add' : 'remove'; + this._filterElm?.classList[classCmd]('filled'); // set the operator when defined this.operator = operator || this.defaultOperator; @@ -307,141 +334,159 @@ export class AutoCompleteFilter implements Filter { } } - renderDomElement(collection: any[]) { + renderDomElement(collection?: any[]) { if (!Array.isArray(collection) && this.collectionOptions?.collectionInsideObjectProperty) { const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty; collection = getDescendantProperty(collection, collectionInsideObjectProperty || ''); } - if (!Array.isArray(collection)) { - throw new Error('The "collection" passed to the Autocomplete Filter is not a valid array.'); - } + // if (!Array.isArray(collection)) { + // throw new Error('The "collection" passed to the Autocomplete Filter is not a valid array.'); + // } // assign the collection to a temp variable before filtering/sorting the collection let newCollection = collection; // user might want to filter and/or sort certain items of the collection - newCollection = this.filterCollection(newCollection); - newCollection = this.sortCollection(newCollection); + if (newCollection) { + newCollection = this.filterCollection(newCollection); + newCollection = this.sortCollection(newCollection); + } // filter input can only have 1 search term, so we will use the 1st array index if it exist const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; - // step 1, create HTML string template - const filterTemplate = this.buildTemplateHtmlString(); - - // step 2, create the DOM Element of the filter & pre-load search term + // step 1, create the DOM Element of the filter & pre-load search term // also subscribe to the onSelect event this._collection = newCollection; - this.createDomElement(filterTemplate, newCollection, searchTerm); + this._filterElm = this.createFilterElement(newCollection, searchTerm); // step 3, subscribe to the input change event and run the callback when that happens // also add/remove "filled" class for styling purposes - this.$filterElm.on('input', this.handleOnInputChange.bind(this)); + this._bindEventService.bind(this._filterElm, 'input', this.handleOnInputChange.bind(this) as EventListener); + this._bindEventService.bind(this._filterElm, 'blur', () => { + if (!this.isItemSelected) { + this.clear(); + } + }); } /** - * Create the HTML template as a string + * Create the autocomplete filter DOM element + * @param collection + * @param searchTerm + * @returns */ - protected buildTemplateHtmlString() { + protected createFilterElement(collection?: any[], searchTerm?: SearchTerm) { + this._collection = collection; const columnId = this.columnDef?.id ?? ''; - let placeholder = (this.gridOptions) ? (this.gridOptions.defaultFilterPlaceholder || '') : ''; + emptyElement(this.filterContainerElm); + + // create the DOM element & add an ID and filter class + let placeholder = this.gridOptions?.defaultFilterPlaceholder ?? ''; if (this.columnFilter?.placeholder) { placeholder = this.columnFilter.placeholder; } - return ``; - } - /** - * From the html template string, create a DOM element - * @param filterTemplate - */ - protected createDomElement(filterTemplate: string, collection: any[], searchTerm?: SearchTerm) { - this._collection = collection; - const columnId = this.columnDef?.id ?? ''; - - $(this.filterContainerElm).empty(); + const inputElm = createDomElement('input', { + type: 'text', + autocomplete: 'none', + placeholder, + className: `form-control search-filter filter-${columnId} slick-autocomplete-container`, + value: (searchTerm ?? '') as string, + dataset: { columnid: `${columnId}` } + }); + inputElm.setAttribute('aria-label', this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter`); + this._filterElm = inputElm; // create the DOM element & add an ID and filter class - this.$filterElm = $(filterTemplate) as any; const searchTermInput = searchTerm as string; - // user might provide his own custom structure - // jQuery UI autocomplete requires a label/value pair, so we must remap them when user provide different ones + // the kradeen autocomplete lib only works with label/value pair, make sure that our array is in accordance if (Array.isArray(collection)) { - collection = collection.map((item) => { - return { label: item[this.labelName], value: item[this.valueName], labelPrefix: item[this.labelPrefixName] || '', labelSuffix: item[this.labelSuffixName] || '' }; - }); + if (collection.every(x => isPrimmitive(x))) { + // when detecting an array of primitives, we have to remap it to an array of value/pair objects + collection = collection.map(c => ({ label: c, value: c })); + } else { + // user might provide its own custom structures, if so remap them as the new label/value pair + collection = collection.map((item) => ({ + label: item?.[this.labelName], + value: item?.[this.valueName], + labelPrefix: item?.[this.labelPrefixName] ?? '', + labelSuffix: item?.[this.labelSuffixName] ?? '' + })); + } } // user might pass his own autocomplete options - const autoCompleteOptions = this.filterOptions; - - // when user passes it's own autocomplete options - // we still need to provide our own "select" callback implementation - if (autoCompleteOptions?.source) { - autoCompleteOptions.select = (event: Event, ui: { item: any; }) => this.onSelect(event, ui); - this._autoCompleteOptions = { ...autoCompleteOptions }; - - // when renderItem is defined, we need to add our custom style CSS class - if (this._autoCompleteOptions.renderItem) { - this._autoCompleteOptions.classes = { - 'ui-autocomplete': `autocomplete-custom-${toKebabCase(this._autoCompleteOptions.renderItem.layout)}` - }; - } - - // create the jQueryUI AutoComplete - this.$filterElm.autocomplete(this._autoCompleteOptions); - - // when "renderItem" is defined, we need to call the user's custom renderItem template callback - if (this._autoCompleteOptions.renderItem) { - this.$filterElm.autocomplete('instance')._renderItem = this.renderCustomItem.bind(this); - } - } else { - const definedOptions: AutocompleteOption = { - minLength: 0, - source: collection, - select: (event: Event, ui: { item: any; }) => this.onSelect(event, ui), - }; - this._autoCompleteOptions = { ...definedOptions, ...this.filterOptions }; - this.$filterElm.autocomplete(this._autoCompleteOptions); - + this._autocompleterOptions = { + input: this._filterElm, + debounceWaitMs: 200, + className: `slick-autocomplete ${this.filterOptions?.className ?? ''}`.trim(), + emptyMsg: this.gridOptions.enableTranslate && this.translaterService?.translate ? this.translaterService.translate('NO_ELEMENTS_FOUND') : this._locales?.TEXT_NO_ELEMENTS_FOUND ?? 'No elements found', + onSelect: (item: AutocompleteSearchItem) => { + this.isItemSelected = true; + this.handleSelect(item); + }, + ...this.filterOptions, + } as Partial>; + + // "render" callback overriding + if (this._autocompleterOptions.renderItem?.layout) { + // when "renderItem" is defined, we need to add our custom style CSS classes & custom item renderer + this._autocompleterOptions.className += ` autocomplete-custom-${toKebabCase(this._autocompleterOptions.renderItem.layout)}`; + this._autocompleterOptions.render = this.renderCustomItem.bind(this); + } else if (Array.isArray(collection)) { // we'll use our own renderer so that it works with label prefix/suffix and also with html rendering when enabled - this.$filterElm.autocomplete('instance')._renderItem = this.renderCollectionItem.bind(this); + this._autocompleterOptions.render = this._autocompleterOptions.render?.bind(this) ?? this.renderCollectionItem.bind(this); + } else if (!this._autocompleterOptions.render) { + // when no render callback is defined, we still need to define our own renderer for regular item + // because we accept string array but the Kraaden autocomplete doesn't by default and we can change that + this._autocompleterOptions.render = this.renderRegularItem.bind(this); } - this.$filterElm.val(searchTermInput); - this.$filterElm.data('columnId', columnId); + // when user passes it's own autocomplete "fetch" method + // we still need to provide our own "onSelect" callback implementation + if (this.filterOptions?.fetch) { + // add loading class by overriding user's fetch method + addAutocompleteLoadingByOverridingFetch(this._filterElm, this._autocompleterOptions); - // if there's a search term, we will add the "filled" class for styling purposes - if (searchTerm) { - this.$filterElm.addClass('filled'); + // create the Kraaden AutoComplete + this._instance = autocomplete(this._autocompleterOptions as AutocompleteSettings); + } else { + this._instance = autocomplete({ + ...this._autocompleterOptions, + fetch: (searchText, updateCallback) => { + if (collection) { + // you can also use AJAX requests instead of preloaded data + // also at this point our collection was already modified, by the previous map, to have the "label" property (unless it's a string) + updateCallback(collection.filter(c => { + const label = (typeof c === 'string' ? c : c?.label) || ''; + return label.toLowerCase().includes(searchText.toLowerCase()); + })); + } + } + } as AutocompleteSettings); } + inputElm.value = searchTermInput ?? ''; + // append the new DOM element to the header row - if (this.$filterElm && typeof this.$filterElm.appendTo === 'function') { - const $container = $(`
    `); - $container.appendTo(this.filterContainerElm); - this.$filterElm.appendTo($container); + const filterDivContainerElm = createDomElement('div', { className: 'autocomplete-filter-container' }); + filterDivContainerElm.appendChild(inputElm); - // add a in order to add spinner styling - $(``).appendTo($container); - } + // add an empty in order to add loading spinner styling + filterDivContainerElm.appendChild(createDomElement('span')); - // we could optionally trigger a search when clicking on the AutoComplete - if (this.filterOptions.openSearchListOnFocus) { - this.$filterElm.click(() => this.$filterElm.autocomplete('search', this.$filterElm.val())); + // if there's a search term, we will add the "filled" class for styling purposes + if (searchTerm) { + inputElm.classList.add('filled'); } - // user might override any of the jQueryUI callback methods - if (this.columnFilter.callbacks) { - for (const callback of Object.keys(this.columnFilter.callbacks)) { - if (typeof this.columnFilter.callbacks[callback] === 'function') { - this.$filterElm.autocomplete('instance')[callback] = this.columnFilter.callbacks[callback]; - } - } - } + // append the new DOM element to the header row & an empty span + this.filterContainerElm.appendChild(filterDivContainerElm); + this.filterContainerElm.appendChild(document.createElement('span')); - return this.$filterElm; + return inputElm; } // @@ -450,18 +495,25 @@ export class AutoCompleteFilter implements Filter { // this function should be PRIVATE but for unit tests purposes we'll make it public until a better solution is found // a better solution would be to get the autocomplete DOM element to work with selection but I couldn't find how to do that in Jest - onSelect(event: Event, ui: { item: any; }) { - if (ui && ui.item) { - const item = ui.item; + handleSelect(item: AutocompleteSearchItem) { + if (item !== undefined) { + const event = undefined; // TODO do we need the event? // when the user defines a "renderItem" (or "_renderItem") template, then we assume the user defines his own custom structure of label/value pair - // otherwise we know that jQueryUI always require a label/value pair, we can pull them directly - const hasCustomRenderItemCallback = this.columnFilter?.callbacks?.hasOwnProperty('_renderItem') ?? this.columnFilter?.filterOptions?.renderItem ?? false; + // otherwise we know that the autocomplete lib always require a label/value pair, we can pull them directly + const hasCustomRenderItemCallback = this.columnFilter?.filterOptions?.renderItem ?? false; const itemLabel = typeof item === 'string' ? item : (hasCustomRenderItemCallback ? item[this.labelName] : item.label); - const itemValue = typeof item === 'string' ? item : (hasCustomRenderItemCallback ? item[this.valueName] : item.value); + let itemValue = typeof item === 'string' ? item : (hasCustomRenderItemCallback ? item[this.valueName] : item.value); + + // trim whitespaces when option is enabled globally or on the filter itself + itemValue = this.trimWhitespaceWhenEnabled(itemValue); + + // add/remove "filled" class name + const classCmd = itemValue === '' ? 'remove' : 'add'; + this._filterElm?.classList[classCmd]('filled'); + this.setValues(itemLabel); - itemValue === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('filled'); this.callback(event, { columnDef: this.columnDef, operator: this.operator, searchTerms: [itemValue], shouldTriggerQuery: this._shouldTriggerQuery }); // reset both flags for next use @@ -471,19 +523,30 @@ export class AutoCompleteFilter implements Filter { return false; } - protected handleOnInputChange(e: any) { - let value = e && e.target && e.target.value || ''; - const enableWhiteSpaceTrim = this.gridOptions.enableFilterTrimWhiteSpace || this.columnFilter.enableTrimWhiteSpace; - if (typeof value === 'string' && enableWhiteSpaceTrim) { - value = value.trim(); - } + protected handleOnInputChange(e: DOMEvent) { + let value = e?.target?.value ?? ''; + const shouldTriggerOnEveryKeyStroke = this.filterOptions.triggerOnEveryKeyStroke ?? false; - if (this._clearFilterTriggered) { - this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); - this.$filterElm.removeClass('filled'); - } else { - value === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('filled'); - this.callback(e, { columnDef: this.columnDef, operator: this.operator, searchTerms: [value], shouldTriggerQuery: this._shouldTriggerQuery }); + // trim whitespaces when option is enabled globally or on the filter itself + value = this.trimWhitespaceWhenEnabled(value); + + if (this._clearFilterTriggered || value === '' || shouldTriggerOnEveryKeyStroke) { + const callbackArgs: FilterCallbackArg = { columnDef: this.columnDef, shouldTriggerQuery: this._shouldTriggerQuery }; + if (this._clearFilterTriggered) { + callbackArgs.clearFilterTriggered = this._clearFilterTriggered; + } else { + callbackArgs.operator = this.operator; + callbackArgs.searchTerms = [value]; + } + + if (value !== '') { + this.isItemSelected = true; + this._filterElm?.classList.add('filled'); + } else { + this.isItemSelected = false; + this._filterElm?.classList.remove('filled'); + } + this.callback(e, callbackArgs); } // reset both flags for next use @@ -491,20 +554,26 @@ export class AutoCompleteFilter implements Filter { this._shouldTriggerQuery = true; } - protected renderCustomItem(ul: HTMLElement, item: any) { - const templateString = this._autoCompleteOptions?.renderItem?.templateCallback(item) ?? ''; + protected renderRegularItem(item: T) { + const itemLabel = (typeof item === 'string' ? item : item?.label ?? '') as string; + return createDomElement('div', { + textContent: itemLabel || '' + }); + } + + protected renderCustomItem(item: T) { + const templateString = this._autocompleterOptions?.renderItem?.templateCallback(item) ?? ''; // sanitize any unauthorized html tags like script and others // for the remaining allowed tags we'll permit all attributes const sanitizedTemplateText = sanitizeTextByAvailableSanitizer(this.gridOptions, templateString) || ''; - return $('
  • ') - .data('item.autocomplete', item) - .append(sanitizedTemplateText) - .appendTo(ul); + return createDomElement('div', { + innerHTML: sanitizedTemplateText + }); } - protected renderCollectionItem(ul: any, item: any) { + protected renderCollectionItem(item: any) { const isRenderHtmlEnabled = this.columnFilter?.enableRenderHtml ?? false; const prefixText = item.labelPrefix || ''; const labelText = item.label || ''; @@ -515,10 +584,22 @@ export class AutoCompleteFilter implements Filter { // for the remaining allowed tags we'll permit all attributes const sanitizedText = sanitizeTextByAvailableSanitizer(this.gridOptions, finalText) || ''; - const $liDiv = $('
    ')[isRenderHtmlEnabled ? 'html' : 'text'](sanitizedText); - return $('
  • ') - .data('item.autocomplete', item) - .append($liDiv) - .appendTo(ul); + const div = document.createElement('div'); + div[isRenderHtmlEnabled ? 'innerHTML' : 'textContent'] = sanitizedText; + return div; + } + + /** + * Trim whitespaces when option is enabled globally or on the filter itself + * @param value - value found which could be a string or an object + * @returns - trimmed value when it is a string and the feature is enabled + */ + protected trimWhitespaceWhenEnabled(value: any) { + let outputValue = value; + const enableWhiteSpaceTrim = this.gridOptions.enableFilterTrimWhiteSpace || this.columnFilter.enableTrimWhiteSpace; + if (typeof value === 'string' && enableWhiteSpaceTrim) { + outputValue = value.trim(); + } + return outputValue; } } \ No newline at end of file diff --git a/packages/common/src/filters/compoundDateFilter.ts b/packages/common/src/filters/compoundDateFilter.ts index 16e7da307..d5b0a733c 100644 --- a/packages/common/src/filters/compoundDateFilter.ts +++ b/packages/common/src/filters/compoundDateFilter.ts @@ -1,7 +1,7 @@ import * as flatpickr_ from 'flatpickr'; import { BaseOptions as FlatpickrBaseOptions, } from 'flatpickr/dist/types/options'; import { Instance as FlatpickrInstance, FlatpickrFn } from 'flatpickr/dist/types/instance'; -const flatpickr: FlatpickrFn = (flatpickr_ && flatpickr_['default'] || flatpickr_) as any; // patch for rollup +const flatpickr: FlatpickrFn = (flatpickr_?.['default'] ?? flatpickr_) as any; // patch for rollup import { Column, diff --git a/packages/common/src/filters/filters.index.ts b/packages/common/src/filters/filters.index.ts index 0d9452c1e..c98595f91 100644 --- a/packages/common/src/filters/filters.index.ts +++ b/packages/common/src/filters/filters.index.ts @@ -1,4 +1,4 @@ -import { AutoCompleteFilter } from './autoCompleteFilter'; +import { AutocompleterFilter } from './autocompleterFilter'; import { CompoundDateFilter } from './compoundDateFilter'; import { CompoundInputFilter } from './compoundInputFilter'; import { CompoundInputNumberFilter } from './compoundInputNumberFilter'; @@ -13,11 +13,10 @@ import { NativeSelectFilter } from './nativeSelectFilter'; import { DateRangeFilter } from './dateRangeFilter'; import { SingleSelectFilter } from './singleSelectFilter'; import { SliderFilter } from './sliderFilter'; -import { SliderRangeFilter } from './sliderRangeFilter'; export const Filters = { - /** AutoComplete Filter (using jQuery UI autocomplete feature) */ - autoComplete: AutoCompleteFilter, + /** AutoComplete Filter (using https://github.com/kraaden/autocomplete) */ + autocompleter: AutocompleterFilter, /** Compound Date Filter (compound of Operator + Date picker) */ compoundDate: CompoundDateFilter, @@ -69,7 +68,4 @@ export const Filters = { /** Slider Filter (only 1 value) */ slider: SliderFilter, - - /** Slider Range Filter, uses jQuery UI Range Slider (2 values, lowest/highest search range) */ - sliderRange: SliderRangeFilter, }; diff --git a/packages/common/src/filters/index.ts b/packages/common/src/filters/index.ts index 613716387..4cc6daedf 100644 --- a/packages/common/src/filters/index.ts +++ b/packages/common/src/filters/index.ts @@ -1,4 +1,4 @@ -export * from './autoCompleteFilter'; +export * from './autocompleterFilter'; export * from './compoundDateFilter'; export * from './compoundInputFilter'; export * from './compoundInputNumberFilter'; @@ -15,5 +15,4 @@ export * from './multipleSelectFilter'; export * from './nativeSelectFilter'; export * from './selectFilter'; export * from './singleSelectFilter'; -export * from './sliderRangeFilter'; export * from './sliderFilter'; diff --git a/packages/common/src/filters/selectFilter.ts b/packages/common/src/filters/selectFilter.ts index 8ee039760..567b0a8b7 100644 --- a/packages/common/src/filters/selectFilter.ts +++ b/packages/common/src/filters/selectFilter.ts @@ -120,7 +120,7 @@ export class SelectFilter implements Filter { throw new Error(`[Slickgrid-Universal] You need to pass a "collection" (or "collectionAsync") for the MultipleSelect/SingleSelect Filter to work correctly. Also each option should include a value/label pair (or value/labelKey when using Locale). For example:: { filter: model: Filters.multipleSelect, collection: [{ value: true, label: 'True' }, { value: false, label: 'False'}] }`); } - this.enableTranslateLabel = this.columnFilter && this.columnFilter.enableTranslateLabel || false; + this.enableTranslateLabel = this.columnFilter?.enableTranslateLabel ?? false; this.labelName = this.customStructure && this.customStructure.label || 'label'; this.labelPrefixName = this.customStructure && this.customStructure.labelPrefix || 'labelPrefix'; this.labelSuffixName = this.customStructure && this.customStructure.labelSuffix || 'labelSuffix'; @@ -132,7 +132,7 @@ export class SelectFilter implements Filter { } // get locales provided by user in main file or else use default English locales via the Constants - this._locales = this.gridOptions && this.gridOptions.locales || Constants.locales; + this._locales = this.gridOptions?.locales ?? Constants.locales; // create the multiple select element this.initMultipleSelectTemplate(); @@ -411,7 +411,7 @@ export class SelectFilter implements Filter { } protected initMultipleSelectTemplate() { - const isTranslateEnabled = this.gridOptions && this.gridOptions.enableTranslate; + const isTranslateEnabled = this.gridOptions?.enableTranslate ?? false; // default options used by this Filter, user can overwrite any of these by passing "otions" const options: MultipleSelectOption = { diff --git a/packages/common/src/filters/sliderRangeFilter.ts b/packages/common/src/filters/sliderRangeFilter.ts deleted file mode 100644 index 12df45dd4..000000000 --- a/packages/common/src/filters/sliderRangeFilter.ts +++ /dev/null @@ -1,280 +0,0 @@ -import 'jquery-ui/ui/widgets/slider'; - -import { OperatorType, OperatorString, SearchTerm, } from '../enums/index'; -import { - Column, - ColumnFilter, - Filter, - FilterArguments, - FilterCallback, - GridOption, - JQueryUiSliderOption, - JQueryUiSliderResponse, - SlickGrid, -} from '../interfaces/index'; - -const DEFAULT_MIN_VALUE = 0; -const DEFAULT_MAX_VALUE = 100; -const DEFAULT_STEP = 1; - -/** A Slider Range Filter which uses jQuery UI, this is only meant to be used as a range filter (with 2 handles lowest & highest values) */ -export class SliderRangeFilter implements Filter { - protected _clearFilterTriggered = false; - protected _currentValues?: number[]; - protected _shouldTriggerQuery = true; - protected _sliderOptions!: JQueryUiSliderOption; - protected $filterElm: any; - protected $filterContainerElm: any; - grid!: SlickGrid; - searchTerms: SearchTerm[] = []; - columnDef!: Column; - callback!: FilterCallback; - filterContainerElm!: HTMLDivElement; - - /** Getter for the Filter Generic Params */ - protected get filterParams(): any { - return this.columnDef && this.columnDef.filter && this.columnDef.filter.params || {}; - } - - /** Getter for the `filter` properties */ - protected get filterProperties(): ColumnFilter { - return this.columnDef && this.columnDef.filter || {}; - } - - /** Getter for the Column Filter */ - get columnFilter(): ColumnFilter { - return this.columnDef && this.columnDef.filter || {}; - } - - /** Getter for the Current Slider Values */ - get currentValues(): number[] | undefined { - return this._currentValues; - } - - /** Getter to know what would be the default operator when none is specified */ - get defaultOperator(): OperatorType | OperatorString { - return this.gridOptions.defaultFilterRangeOperator || OperatorType.rangeInclusive; - } - - /** Getter for the Grid Options pulled through the Grid Object */ - get gridOptions(): GridOption { - return (this.grid && this.grid.getOptions) ? this.grid.getOptions() : {}; - } - - /** Getter for the JQuery UI Slider Options */ - get sliderOptions(): JQueryUiSliderOption { - return this._sliderOptions || {}; - } - - /** Getter of the Operator to use when doing the filter comparing */ - get operator(): OperatorType | OperatorString { - return this.columnFilter?.operator ?? this.defaultOperator; - } - - /** Setter for the filter operator */ - set operator(operator: OperatorType | OperatorString) { - if (this.columnFilter) { - this.columnFilter.operator = operator; - } - } - - /** - * Initialize the Filter - */ - init(args: FilterArguments) { - if (!args) { - throw new Error('[Slickgrid-Universal] A filter must always have an "init()" with valid arguments.'); - } - this.grid = args.grid; - this.callback = args.callback; - this.columnDef = args.columnDef; - this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; - this.filterContainerElm = args.filterContainerElm; - - // step 1, create the DOM Element of the filter & initialize it if searchTerm is filled - this.$filterElm = this.createDomElement(this.searchTerms); - } - - /** - * Clear the filter value - */ - clear(shouldTriggerQuery = true) { - if (this.$filterElm) { - this._clearFilterTriggered = true; - this._shouldTriggerQuery = shouldTriggerQuery; - this.searchTerms = []; - const lowestValue = this.filterParams.hasOwnProperty('sliderStartValue') ? this.filterParams.sliderStartValue : DEFAULT_MIN_VALUE; - const highestValue = this.filterParams.hasOwnProperty('sliderEndValue') ? this.filterParams.sliderEndValue : DEFAULT_MAX_VALUE; - this._currentValues = [lowestValue, highestValue]; - this.$filterElm.slider('values', [lowestValue, highestValue]); - if (!this.filterParams.hideSliderNumbers) { - this.renderSliderValues(lowestValue, highestValue); - } - this.callback(undefined, { columnDef: this.columnDef, clearFilterTriggered: true, shouldTriggerQuery }); - this.$filterContainerElm.removeClass('filled'); - } - } - - /** - * destroy the filter - */ - destroy() { - if (this.$filterElm) { - this.$filterElm.off('change').remove(); - this.$filterContainerElm.remove(); - } - this.$filterElm = null; - this.$filterContainerElm = null; - } - - /** - * Render both slider values (low/high) on screen - * @param lowestValue number - * @param highestValue number - */ - renderSliderValues(lowestValue: number | string, highestValue: number | string) { - const columnId = this.columnDef?.id ?? ''; - const lowerElm = this.$filterContainerElm.get(0)?.querySelector(`.lowest-range-${columnId}`); - const highestElm = this.$filterContainerElm.get(0)?.querySelector(`.highest-range-${columnId}`); - if (lowerElm?.textContent) { - lowerElm.textContent = lowestValue.toString(); - } - if (highestElm?.textContent) { - highestElm.textContent = highestValue.toString(); - } - } - - getValues() { - return this._currentValues; - } - - /** - * Set value(s) on the DOM element - * @params searchTerms - */ - setValues(searchTerms: SearchTerm | SearchTerm[], operator?: OperatorType | OperatorString) { - if (searchTerms) { - let sliderValues: number[] | string[] = []; - - // get the slider values, if it's a string with the "..", we'll do the split else we'll use the array of search terms - if (typeof searchTerms === 'string' || (Array.isArray(searchTerms) && typeof searchTerms[0] === 'string') && (searchTerms[0] as string).indexOf('..') > 0) { - sliderValues = (typeof searchTerms === 'string') ? [(searchTerms as string)] : (searchTerms[0] as string).split('..'); - } else if (Array.isArray(searchTerms)) { - sliderValues = searchTerms as string[]; - } - - if (Array.isArray(sliderValues) && sliderValues.length === 2) { - this.$filterElm.slider('values', sliderValues); - if (!this.filterParams.hideSliderNumbers) { - this.renderSliderValues(sliderValues[0], sliderValues[1]); - } - } - } - (searchTerms && (this.getValues?.() ?? []).length > 0) ? this.$filterContainerElm.addClass('filled') : this.$filterContainerElm.removeClass('filled'); - - // set the operator when defined - this.operator = operator || this.defaultOperator; - } - - // - // protected functions - // ------------------ - - /** - * From the html template string, create a DOM element - * @param searchTerm optional preset search terms - */ - protected createDomElement(searchTerms?: SearchTerm | SearchTerm[]) { - if (this.columnFilter && this.columnFilter.filterOptions && (this.columnFilter.filterOptions.change || this.columnFilter.filterOptions.slide)) { - throw new Error(`[Slickgrid-Universal] You cannot override the "change" and/or the "slide" callback methods - since they are used in SliderRange Filter itself, however any other methods can be used for example the "create", "start", "stop" methods.`); - } - const columnId = this.columnDef?.id ?? ''; - const minValue = this.filterProperties.hasOwnProperty('minValue') ? this.filterProperties.minValue : DEFAULT_MIN_VALUE; - const maxValue = this.filterProperties.hasOwnProperty('maxValue') ? this.filterProperties.maxValue : DEFAULT_MAX_VALUE; - const step = this.filterProperties.hasOwnProperty('valueStep') ? this.filterProperties.valueStep : DEFAULT_STEP; - - let defaultStartValue: number = DEFAULT_MIN_VALUE; - let defaultEndValue: number = DEFAULT_MAX_VALUE; - if (Array.isArray(searchTerms) && searchTerms.length > 1) { - defaultStartValue = +searchTerms[0]; - defaultEndValue = +searchTerms[1]; - } else { - defaultStartValue = +(this.filterParams.hasOwnProperty('sliderStartValue') ? this.filterParams.sliderStartValue : minValue); - defaultEndValue = +(this.filterParams.hasOwnProperty('sliderEndValue') ? this.filterParams.sliderEndValue : maxValue); - } - - $(this.filterContainerElm).empty(); - - // create the DOM element & add an ID and filter class - const $lowestSliderValueElm = $(` -
    - ${defaultStartValue} -
    `); - const $highestSliderValueElm = $(` -
    - ${defaultEndValue} -
    `); - this.$filterElm = $(`
    `); - this.$filterContainerElm = $(`
    `); - - if (this.filterParams.hideSliderNumbers) { - this.$filterContainerElm.append(this.$filterElm); - } else { - this.$filterContainerElm.append($lowestSliderValueElm); - this.$filterContainerElm.append(this.$filterElm); - this.$filterContainerElm.append($highestSliderValueElm); - } - - // if we are preloading searchTerms, we'll keep them for reference - this._currentValues = [defaultStartValue, defaultEndValue]; - - const definedOptions: JQueryUiSliderOption = { - range: true, - min: +(minValue || 0), - max: +(maxValue || DEFAULT_MAX_VALUE), - step: +(step || 1), - values: [defaultStartValue, defaultEndValue], - change: (e: Event, ui: JQueryUiSliderResponse) => this.onValueChanged(e, ui), - slide: (_e: Event, ui: JQueryUiSliderResponse) => { - const values = ui.values; - if (!this.filterParams.hideSliderNumbers && Array.isArray(values)) { - this.renderSliderValues(values[0], values[1]); - } - } - }; - - // merge options with optional user's custom options - this._sliderOptions = { ...definedOptions, ...(this.columnFilter.filterOptions as JQueryUiSliderOption) }; - this.$filterElm.slider(this._sliderOptions); - - // if there's a search term, we will add the "filled" class for styling purposes - if (Array.isArray(searchTerms) && searchTerms.length > 0 && searchTerms[0] !== '') { - this.$filterContainerElm.addClass('filled'); - } - - // append the new DOM element to the header row - if (this.$filterContainerElm && typeof this.$filterContainerElm.appendTo === 'function') { - this.$filterContainerElm.appendTo(this.filterContainerElm); - } - - return this.$filterElm; - } - - /** On a value change event triggered */ - protected onValueChanged(e: Event, ui: JQueryUiSliderResponse) { - const values = ui && Array.isArray(ui.values) ? ui.values : []; - const value = values.join('..'); - - if (this._clearFilterTriggered) { - this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); - this.$filterContainerElm.removeClass('filled'); - } else { - value === '' ? this.$filterContainerElm.removeClass('filled') : this.$filterContainerElm.addClass('filled'); - this.callback(e, { columnDef: this.columnDef, operator: this.operator, searchTerms: values, shouldTriggerQuery: this._shouldTriggerQuery }); - } - // reset both flags for next use - this._clearFilterTriggered = false; - this._shouldTriggerQuery = true; - } -} diff --git a/packages/common/src/interfaces/autocompleteOption.interface.ts b/packages/common/src/interfaces/autocompleteOption.interface.ts deleted file mode 100644 index 6871ee21c..000000000 --- a/packages/common/src/interfaces/autocompleteOption.interface.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Column } from './column.interface'; - -export type JQueryAjaxFn = (request: any, response: any) => void; - -export interface AutoCompleteRenderItemDefinition { - /** which custom Layout to use? We created 2 custom styled layouts "twoRows" and "fourCorners", both layouts also support an optional icon on the left. */ - layout: 'twoRows' | 'fourCorners'; - - /** templateCallback must be a callback function returning the renderItem template string that is used to dislay each row of the AutoComplete result */ - templateCallback: (item: any) => string; -} - -export interface AutocompleteOption { - /** If set to true the first item will automatically be focused when the menu is shown. */ - autoFocus?: boolean; - - /** - * The classes option is used to map structural class names to theme-related class names that you define - * For example: classes: { "ui-autocomplete": "custom-red" } - * This means that wherever jQuery UI applies the ui-autocomplete class it should also apply "custom-red" class - */ - classes?: { [className: string]: string }; - - /** Delay to wait before showing the autocomplete list */ - delay?: number; - - /** Disables the autocomplete if set to `true`. */ - disabled?: boolean; - - /** - * Minimum length to type in the input search value before the autocomplete starts showing and querying, - * to avoid queries that would return too many results - */ - minLength?: number; - - /** Position an element relative to another. */ - position?: { - /** Defines which position on the element being positioned to align with the target element: "horizontal vertical" alignment. */ - my?: string; - - /** Defines which position on the target element to align the positioned element against: "horizontal vertical" alignment. */ - at?: string; - - /** Which element to position against. If you provide a selector or jQuery object, the first matching element will be used. If you provide an event object, the pageX and pageY properties will be used. */ - of?: any; - - /** When the positioned element overflows the window in some direction, move it to an alternative position. Similar to `my` and `at`, this accepts a single value or a pair for horizontal/vertical, e.g., "flip", "fit", "fit flip", "fit none". */ - collision?: string; - }; - - /** Source for the autocomplete list */ - source: string | any[] | JQueryAjaxFn; - - // -- - // Extra Option (outside of jQuery UI) - // ----------------------------------- - - /** defaults to false, force the user to start typing a value in the search input */ - forceUserInput?: boolean; - - /** - * Defaults to false, will open the search list (should really only be used with a defined collection list). - * Also note that if you wish to display even when the autoComplete is an empty string, you will need to adjust the "minLength" to 0. - */ - openSearchListOnFocus?: boolean; - - /** - * renderItem option is to simply provide a Template and decide which custom Layout to use - * - * Note that this "renderItem" is just a shortcut and can be done with the following code: - * editor: { editorOptions: { classes: { 'ui-autocomplete': 'autocomplete-custom-2rows', }, callbacks: { _renderItem: (ul: HTMLElement, item: any) => this.renderItemCallbackWith2Rows(ul, item) }} - */ - renderItem?: AutoCompleteRenderItemDefinition; - - // -- - // Events / Methods - // ----------------- - - /** Method that controls the creation of each option in the widget's menu. The method must create a new
  • element, append it to the menu, and return it. */ - _renderItem?: (ul: any, item: any) => any; - - /** Method that controls building the widget's menu. The method is passed an empty
      and an array of items that match the user typed term. */ - _renderMenu?: (ul: any, items: any[]) => any; - - /** Method responsible for sizing the menu before it is displayed. */ - _resizeMenu?: () => any; - - /** - * Which element the menu should be appended to. When the value is null, the parents of the input field will be checked for a class of ui-front. - * If an element with the ui-front class is found, the menu will be appended to that element. - * Regardless of the value, if no element is found, the menu will be appended to the body. - */ - appendTo?: (selector: any) => any; - - /** Triggered when the autocomplete is created. */ - create?: (e: Event, ui: { item: any; }) => boolean; - - /** Triggered when the input value becomes in focus */ - focus?: (e: Event, ui: { item: any; }) => boolean; - - /** - * Triggered when a value is selected from the autocomplete list. - * This is the same as the "select" callback and was created so that user don't overwrite exclusive usage of the "select" callback. - * Also compare to the "select", it has some extra arguments which are: row, cell, column, dataContext - */ - onSelect?: (e: Event, ui: { item: any; }, row: number, cell: number, columnDef: Column, dataContext: any) => boolean; - - /** Triggered when the suggestion menu is opened or updated. */ - open?: (e: Event, ui: { item: any; }) => boolean; - - /** - * Triggered after a search completes, before the menu is shown. Useful for local manipulation of suggestion data, - * where a custom source option callback is not required. This event is always triggered when a search completes, - * even if the menu will not be shown because there are no results or the Autocomplete is disabled. - */ - response?: (e: Event, ui: { item: any; }) => boolean; - - /** Triggered when user enters a search value */ - search?: (e: Event, ui: { item: any; }) => boolean; - - /** - *Triggered when focus is moved to an item (not selecting). The default action is to replace the text field's value with the value of the focused item, though only if the event was triggered by a keyboard interaction. - * Canceling this event prevents the value from being updated, but does not prevent the menu item from being focused. - * NOTE: this method should NOT be used since Slickgrid-Universal will use it exclusively - * and if you do try to use it, what will happen is that it will override and break Slickgrid-Universal internal code. - * Please use the "onSelect" which was added specifically to avoid this problem but still provide exact same result - */ - select?: (e: Event, ui: { item: any; }) => boolean; -} diff --git a/packages/common/src/interfaces/autocompleterOption.interface.ts b/packages/common/src/interfaces/autocompleterOption.interface.ts new file mode 100644 index 000000000..55a954686 --- /dev/null +++ b/packages/common/src/interfaces/autocompleterOption.interface.ts @@ -0,0 +1,44 @@ +import { AutocompleteItem, AutocompleteSettings } from 'autocompleter'; +import { Column } from './column.interface'; + +export interface AutoCompleterRenderItemDefinition { + /** which custom Layout to use? We created 2 custom styled layouts "twoRows" and "fourCorners", both layouts also support an optional icon on the left. */ + layout: 'twoRows' | 'fourCorners'; + + /** templateCallback must be a callback function returning the renderItem template string that is used to dislay each row of the AutoComplete result */ + templateCallback: (item: any) => string; +} + +export type AutocompleteSearchItem = { + [labelName: string]: string; +} | string; + +export interface AutocompleterOption extends Partial> { + /** defaults to false, force the user to start typing a value in the search input */ + forceUserInput?: boolean; + + /** + * renderItem option is to simply provide a Template and decide which custom Layout to use + * + * Note that this "renderItem" is just a shortcut and can be done with the following code: + * editor: { editorOptions: { className: { 'autocomplete': 'autocomplete-custom-2rows', render: (item: any) => this.renderItemCallbackWith2Rows(ul, item) }} + */ + renderItem?: AutoCompleterRenderItemDefinition; + + /** + * defaults to false, do we want to trigger editor/filter callback on every stroke? + * Typically the answer is No, but it can be useful in unit tests + */ + triggerOnEveryKeyStroke?: boolean; + + // -- + // Events / Methods + // ----------------- + + /** + * Triggered when a value is selected from the autocomplete list. + * This is the same as the "select" callback and was created so that user don't overwrite exclusive usage of the "select" callback. + * Also compare to the "select", it has some extra arguments which are: row, cell, column, dataContext + */ + onSelectItem?: (item: any, row: number, cell: number, columnDef: Column, dataContext: any) => void; +} \ No newline at end of file diff --git a/packages/common/src/interfaces/columnEditor.interface.ts b/packages/common/src/interfaces/columnEditor.interface.ts index aa281048c..ebfd42d91 100644 --- a/packages/common/src/interfaces/columnEditor.interface.ts +++ b/packages/common/src/interfaces/columnEditor.interface.ts @@ -20,20 +20,11 @@ export interface ColumnEditor { /** Optionally provide an aria-label for assistive scren reader, defaults to "{inputName} Input Editor" */ ariaLabel?: string; - /** - * Some Editor could support callbacks from their jQuery instance (for now only AutoComplete supports this), for example: - * editor: { model:{ Editors.autoComplete }, callbacks: { _renderItem: (ul, item) => { ... } }} - * - * will be interpreted as $(#element).autocomplete("instance")._renderItem = (ul, item) => { ... } - * from jQuery UI doc: https://jqueryui.com/autocomplete/#custom-data - */ - callbacks?: any; - - /** A collection of items/options that will be loaded asynchronously (commonly used with AutoComplete & Single/Multi-Select Editors) */ + /** A collection of items/options that will be loaded asynchronously (commonly used with Autocompleter & Single/Multi-Select Editors) */ collectionAsync?: Promise | Observable; /** - * A collection of items/options (commonly used with AutoComplete & Single/Multi-Select Editors) + * A collection of items/options (commonly used with Autocompleter & Single/Multi-Select Editors) * It can be a collection of string or label/value pair (the pair can be customized via the "customStructure" option) */ collection?: any[]; @@ -84,7 +75,7 @@ export interface ColumnEditor { /** * Defaults to false, when set it will render any HTML code instead of removing it (sanitized) - * Currently only supported by the following Editors: AutoComplete, MultipleSelect & SingleSelect + * Currently only supported by the following Editors: Autocompleter, MultipleSelect & SingleSelect */ enableRenderHtml?: boolean; @@ -100,13 +91,13 @@ export interface ColumnEditor { */ massUpdate?: boolean; - /** Maximum length of the text value, works only with Editors supporting it (autoComplete, text, longText) */ + /** Maximum length of the text value, works only with Editors supporting it (autocompleter, text, longText) */ maxLength?: number; /** Maximum value of the editor, works only with Editors supporting it (number, float, slider) */ maxValue?: number | string; - /** Minimum length of the text value, works only with Editors supporting it (autoComplete, text, longText) */ + /** Minimum length of the text value, works only with Editors supporting it (autocompleter, text, longText) */ minLength?: number; /** Minimum value of the editor, works only with Editors supporting it (number, float, slider) */ diff --git a/packages/common/src/interfaces/columnFilter.interface.ts b/packages/common/src/interfaces/columnFilter.interface.ts index 3cdcfc66d..de7ea3d93 100644 --- a/packages/common/src/interfaces/columnFilter.interface.ts +++ b/packages/common/src/interfaces/columnFilter.interface.ts @@ -18,15 +18,6 @@ export interface ColumnFilter { /** Do we want to bypass the Backend Query? Commonly used with an OData Backend Service, if we want to filter without calling the regular OData query. */ bypassBackendQuery?: boolean; - /** - * Some Filter could support callbacks from their jQuery instance (for now only AutoComplete supports this), for example: - * filter: { model:{ Filters.autoComplete }, callbacks: { _renderItem: (ul, item) => { ... } }} - * - * will be interpreted as $(#element).autocomplete("instance")._renderItem = (ul, item) => { ... } - * from jQuery UI doc: https://jqueryui.com/autocomplete/#custom-data - */ - callbacks?: any; - /** Column ID */ columnId?: string; diff --git a/packages/common/src/interfaces/domEvent.interface.ts b/packages/common/src/interfaces/domEvent.interface.ts index 356a90c3f..21e596090 100644 --- a/packages/common/src/interfaces/domEvent.interface.ts +++ b/packages/common/src/interfaces/domEvent.interface.ts @@ -3,7 +3,7 @@ export interface DOMEvent extends Event { target: T; relatedTarget: T; } -export interface DOMMouseEvent extends MouseEvent { +export interface DOMMouseOrTouchEvent extends MouseEvent, TouchEvent { currentTarget: T; target: T; relatedTarget: T; diff --git a/packages/common/src/interfaces/draggableGroupingOption.interface.ts b/packages/common/src/interfaces/draggableGroupingOption.interface.ts index 7758ce86d..199b3465c 100644 --- a/packages/common/src/interfaces/draggableGroupingOption.interface.ts +++ b/packages/common/src/interfaces/draggableGroupingOption.interface.ts @@ -2,7 +2,7 @@ import { ColumnReorderFunction } from '../enums/columnReorderFunction.type'; import { GroupingGetterFunction } from './grouping.interface'; export interface DraggableGroupingOption { - /** an extra CSS class to add to the delete button (default undefined), if deleteIconCssClass is undefined then slick-groupby-remove-image class will be added */ + /** an extra CSS class to add to the delete button (default undefined), if deleteIconCssClass is undefined then slick-groupby-remove-icon class will be added */ deleteIconCssClass?: string; /** option to specify set own placeholder note text */ diff --git a/packages/common/src/interfaces/gridOption.interface.ts b/packages/common/src/interfaces/gridOption.interface.ts index 571dc3c21..b7077de69 100644 --- a/packages/common/src/interfaces/gridOption.interface.ts +++ b/packages/common/src/interfaces/gridOption.interface.ts @@ -267,7 +267,11 @@ export interface GridOption { /** Defaults to true, when enabled will give the possibility to do a right+click on any header title which will open the list of column. User can show/hide a column by using the checkbox from that picker list. */ enableColumnPicker?: boolean; - /** Defaults to true, which permits the user to move an entire column from a position to another. */ + /** + * Defaults to true, this option can be a boolean or a Column Reorder function. + * When provided as a boolean, it will permits the user to move an entire column from a position to another. + * We could also provide a Column Reorder function, there's mostly only 1 use for this which is the SlickDraggableGrouping plugin. + */ enableColumnReorder?: boolean | ColumnReorderFunction; /** @@ -307,9 +311,6 @@ export interface GridOption { /** Do we want to enable the Excel Export? (if Yes, it will show up in the Grid Menu) */ enableExcelExport?: boolean; - /** @deprecated Please use "enableTextExport", Do we want to enable the Export to File? (if Yes, it will show up in the Grid Menu) */ - enableExport?: boolean; - /** Do we want to enable Filters? */ enableFiltering?: boolean; @@ -384,9 +385,6 @@ export interface GridOption { /** Some default options to set for the Excel export service */ excelExportOptions?: ExcelExportOption; - /** @deprecated Please use "textExportOptions" Some default options to set for the export service */ - exportOptions?: TextExportOption; - /** * Default to 0, how long to wait between each characters that the user types before processing the filtering process (only applies for local/in-memory grid). * Especially useful when you have a big dataset and you want to limit the amount of search called (by default every keystroke will trigger a search on the dataset and that is sometime slow). diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index b303c5401..71b796561 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -1,5 +1,5 @@ export * from './aggregator.interface'; -export * from './autocompleteOption.interface'; +export * from './autocompleterOption.interface'; export * from './autoResizeOption.interface'; export * from './autoTooltipOption.interface'; export * from './backendService.interface'; @@ -99,8 +99,6 @@ export * from './headerMenuOption.interface'; export * from './hideColumnOption.interface'; export * from './htmlElementPosition.interface'; export * from './itemMetadata.interface'; -export * from './jQueryUiSliderOption.interface'; -export * from './jQueryUiSliderResponse.interface'; export * from './keyTitlePair.interface'; export * from './locale.interface'; export * from './longTextEditorOption.interface'; diff --git a/packages/common/src/interfaces/jQueryUiSliderOption.interface.ts b/packages/common/src/interfaces/jQueryUiSliderOption.interface.ts deleted file mode 100644 index 034c91f53..000000000 --- a/packages/common/src/interfaces/jQueryUiSliderOption.interface.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { JQueryUiSliderResponse } from './jQueryUiSliderResponse.interface'; - -export interface JQueryUiSliderOption { - /** are we using animation? */ - animate?: boolean; - - /** maximum value that can be used by the slider */ - max?: number; - - /** minimum value that can be used by the slider */ - min?: number; - - /** Slider orientation (horizontal or vertical) */ - orientation?: 'horizontal' | 'vertical'; - - /** defaults to false, are we using a range slider (that is with 2 values low/high) */ - range?: boolean | 'max' | 'min'; - - /** step to increment */ - step?: number; - - /** value to preload */ - value?: number; - - /** values which can be preloaded in the slider */ - values?: number[]; - - // -- - // Events / Methods - // ----------------- - - /** Triggered when the slider value changes, only after mouse up */ - change?: (e: Event, ui: JQueryUiSliderResponse) => void; - - /** Triggered when the slider is created */ - create?: (e: Event, ui: JQueryUiSliderResponse) => void; - - /** Triggered when the user starts to slide with the slider handle */ - start?: (e: Event, ui: JQueryUiSliderResponse) => void; - - /** Triggered when the user stops to slide with the slider handle */ - stop?: (e: Event, ui: JQueryUiSliderResponse) => void; - - /** Triggered whenever the slider handle moves, useful to update low/high values when displayed. */ - slide?: (e: Event, ui: JQueryUiSliderResponse) => void; -} diff --git a/packages/common/src/interfaces/jQueryUiSliderResponse.interface.ts b/packages/common/src/interfaces/jQueryUiSliderResponse.interface.ts deleted file mode 100644 index 29f64e2c9..000000000 --- a/packages/common/src/interfaces/jQueryUiSliderResponse.interface.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface JQueryUiSliderResponse { - /** DOM element of the slider handle (the round handle which the user can drag) */ - handle: HTMLElement; - - /** Index of the slider handle, basically which handles is used when dragging the value from the handle. */ - handleIndex: number; - - /** value which the slider returns */ - value?: number; - - /** value which the slider returns when using a range */ - values?: number[]; -} diff --git a/packages/common/src/interfaces/locale.interface.ts b/packages/common/src/interfaces/locale.interface.ts index 88d026297..9170b271f 100644 --- a/packages/common/src/interfaces/locale.interface.ts +++ b/packages/common/src/interfaces/locale.interface.ts @@ -134,6 +134,9 @@ export interface Locale { /** Text "Less than or equal to" shown in Compound Editors/Filters as an Operator */ TEXT_LESS_THAN_OR_EQUAL_TO: string; + /** Text "Notelements found" that could show when nothing is returned in the Autocomplete */ + TEXT_NO_ELEMENTS_FOUND?: string; + /** Text "Not contains" shown in Compound Editors/Filters as an Operator */ TEXT_NOT_CONTAINS: string; diff --git a/packages/common/src/interfaces/slickEventData.interface.ts b/packages/common/src/interfaces/slickEventData.interface.ts index 8758c818c..d976cb808 100644 --- a/packages/common/src/interfaces/slickEventData.interface.ts +++ b/packages/common/src/interfaces/slickEventData.interface.ts @@ -1,4 +1,4 @@ -export interface SlickEventData extends KeyboardEvent, MouseEvent, Event { +export interface SlickEventData extends Event, KeyboardEvent, MouseEvent, TouchEvent { /** Stops event from propagating up the DOM tree. */ stopPropagation: () => void; diff --git a/packages/common/src/services/__tests__/extension.service.spec.ts b/packages/common/src/services/__tests__/extension.service.spec.ts index 7cb4c0578..9c9551814 100644 --- a/packages/common/src/services/__tests__/extension.service.spec.ts +++ b/packages/common/src/services/__tests__/extension.service.spec.ts @@ -1,3 +1,5 @@ +jest.mock('../../extensions/slickDraggableGrouping'); + import 'jest-extended'; import { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; diff --git a/packages/common/src/services/extension.service.ts b/packages/common/src/services/extension.service.ts index a6f3361e4..5c3131f97 100644 --- a/packages/common/src/services/extension.service.ts +++ b/packages/common/src/services/extension.service.ts @@ -316,7 +316,7 @@ export class ExtensionService { if (!this.getCreatedExtensionByName(ExtensionName.draggableGrouping)) { this._draggleGroupingPlugin = new SlickDraggableGrouping(this.extensionUtility, this.pubSubService, this.sharedService); if (this._draggleGroupingPlugin) { - gridOptions.enableColumnReorder = this._draggleGroupingPlugin.setupColumnReorder as ColumnReorderFunction; + gridOptions.enableColumnReorder = this._draggleGroupingPlugin.setupColumnReorder.bind(this._draggleGroupingPlugin) as ColumnReorderFunction; this._extensionCreatedList[ExtensionName.draggableGrouping] = { name: ExtensionName.draggableGrouping, instance: this._draggleGroupingPlugin }; } } diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss index 19d1c39ac..76a66601c 100644 --- a/packages/common/src/styles/_variables.scss +++ b/packages/common/src/styles/_variables.scss @@ -92,6 +92,7 @@ $slick-header-border-left: 0 none !default; $slick-header-column-height: calc(17px * #{$slick-header-row-count}) !default; // header is calculated by rows to show $slick-header-column-background-active: darken($slick-grid-header-background, 5%) !default; $slick-header-column-background-hover: darken($slick-grid-header-background, 2%) !default; +$slick-header-column-sortable-background-hover: #e0e0e0 !default; $slick-header-column-name-margin-right: 5px !default; $slick-header-column-border-top: 0 none !default; // header, column titles, that is without the Filters $slick-header-column-border-right: 0 none !default; @@ -149,32 +150,25 @@ $slick-icon-group-height: 20px !default; $slick-icon-group-vertical-align: middle !default; $slick-icon-group-width: 14px !default; -/* AutoComplete */ -$slick-autocomplete-bg-color: #ffffff !default; -$slick-autocomplete-border: 1px solid rgba(0, 0, 0, 0.15) !default; -$slick-autocomplete-border-radius: 4px !default; -$slick-autocomplete-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175) !default; -$slick-autocomplete-hover-color: #262626 !default; -$slick-autocomplete-hover-bg-color: darken($slick-row-mouse-hover-color, 3%) !default; -$slick-autocomplete-hover-border-color: 1px solid #{$slick-autocomplete-hover-bg-color} !default; -$slick-autocomplete-loading-input-bg-color: transparent !default; -$slick-autocomplete-loading-icon: "\f021" !default; -$slick-autocomplete-loading-icon-color: $slick-icon-color !default; -$slick-autocomplete-loading-icon-width: inherit !default; -$slick-autocomplete-loading-icon-margin: 0 0 0 -16px !default; -$slick-autocomplete-loading-icon-line-height: 0px !default; -$slick-autocomplete-loading-icon-vertical-align: inherit !default; -$slick-autocomplete-max-height: 25vh !default; -$slick-autocomplete-min-height: 75px !default; -$slick-autocomplete-min-width: 50px !default; -$slick-autocomplete-overflow-x: hidden !default; -$slick-autocomplete-overflow-y: auto !default; -$slick-autocomplete-text-color: #333333 !default; -$slick-autocomplete-text-overflow: ellipsis !default; -$slick-autocomplete-text-padding: 3px 15px !default; +/* Kraaden AutoComplete */ +$slick-autocomplete-bg-color: #ffffff !default; +$slick-autocomplete-group-bg-color: #eeeeee !default; +$slick-autocomplete-border: 1px solid rgba(0, 0, 0, 0.15) !default; +$slick-autocomplete-hover-bg-color: darken($slick-row-mouse-hover-color, 3%) !default; +$slick-autocomplete-loading-input-bg-color: transparent !default; +$slick-autocomplete-loading-icon: "\f021" !default; +$slick-autocomplete-loading-icon-color: $slick-icon-color !default; +$slick-autocomplete-loading-icon-width: inherit !default; +$slick-autocomplete-loading-icon-margin: 0 0 0 -16px !default; +$slick-autocomplete-loading-icon-line-height: 0px !default; +$slick-autocomplete-loading-icon-vertical-align: inherit !default; +$slick-autocomplete-max-height: 25vh !default; +$slick-autocomplete-min-height: 75px !default; +$slick-autocomplete-text-color: #333333 !default; +$slick-autocomplete-width: auto !default; $slick-autocomplete-z-index: 9999 !default; -/** AutoComplete with Custom Styling (2 rows) */ +/** Kraaden AutoComplete with Custom Styling (2 rows) */ $slick-autocomplete-tpl2-font-size: 12px !default; $slick-autocomplete-tpl2-width: 285px !default; $slick-autocomplete-tpl2-container-list-width: calc(#{$slick-autocomplete-tpl2-width} - 15px) !default; @@ -194,7 +188,7 @@ $slick-autocomplete-tpl2-top-left-font-style: normal !default; $slick-autocomplete-tpl2-top-left-font-weight: bold !default; $slick-autocomplete-tpl2-top-left-max-width: $slick-autocomplete-tpl2-bottom-left-max-width !default; -/** AutoComplete with Custom Styling (4 corners) */ +/** Kraaden AutoComplete with Custom Styling (4 corners) */ $slick-autocomplete-tpl4-font-size: 12px !default; $slick-autocomplete-tpl4-width: 385px !default; $slick-autocomplete-tpl4-container-list-width: calc(#{$slick-autocomplete-tpl4-width} - 15px) !default; @@ -240,9 +234,12 @@ $slick-sort-indicator-number-top: calc(13px * #{$slick $slick-sort-indicator-hint-opacity: 0.5 !default; /* Grouping Totals Formatter */ +$slick-group-border-bottom: 2px solid silver !default; $slick-group-totals-formatter-color: gray !default; $slick-group-totals-formatter-bgcolor: #fff !default; $slick-group-totals-formatter-font-size: 14px !default; +$slick-group-totals-text-background: white !default; +$slick-group-totals-text-color: gray !default; /** Detail View Plugin */ $slick-detail-view-icon-collapse: "\f056" !default; @@ -611,18 +608,20 @@ $slick-draggable-group-column-border: 1px solid transparen $slick-draggable-group-column-border-radius: 6px !default; $slick-draggable-group-column-padding: 0 5px !default; $slick-draggable-group-column-margin-right: 2px !default; +$slick-draggable-group-drop-border-hover: 1px dashed #ff9e9e !default; $slick-draggable-group-drop-border: 1px solid #e0e0e0 !default; $slick-draggable-group-drop-border-top: $slick-draggable-group-drop-border !default; $slick-draggable-group-drop-border-bottom: $slick-draggable-group-drop-border !default; $slick-draggable-group-drop-border-right: $slick-draggable-group-drop-border !default; $slick-draggable-group-drop-border-left: $slick-draggable-group-drop-border !default; +$slick-draggable-group-drop-placeholder-hover-opacity: 0.6 !default; $slick-draggable-group-drop-bgcolor: #ffffff !default; $slick-draggable-group-drop-height: 35px !default; $slick-draggable-group-drop-padding: 5px 10px !default; $slick-draggable-group-drop-radius: 4px !default; $slick-draggable-group-drop-width: calc(100% - 25px) !default; $slick-draggable-group-droppable-active-bgcolor: #fafafa !default; -$slick-draggable-group-droppable-hover-bgcolor: darken($slick-draggable-group-droppable-active-bgcolor, 5%) !default; +$slick-draggable-group-droppable-hover-bgcolor: #ffffff !default; $slick-draggable-group-placeholder-font-style: italic !default; $slick-draggable-group-placeholder-color: #616161 !default; $slick-draggable-group-delete-color: pink !default; diff --git a/packages/common/src/styles/jquery-ui.scss b/packages/common/src/styles/jquery-ui.scss deleted file mode 100644 index e492f69fa..000000000 --- a/packages/common/src/styles/jquery-ui.scss +++ /dev/null @@ -1,375 +0,0 @@ -$autocomplete-hover-bg-color: #f0f0f0 !default; -$autocomplete-hover-border-color: 1px solid #{$autocomplete-hover-bg-color} !default; - -/*! jQuery UI - v1.12.1 - 2016-09-14 -* http://jqueryui.com -* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css -* To view and modify this theme, visit http://jqueryui.com/themeroller/?bgShadowXPos=&bgOverlayXPos=&bgErrorXPos=&bgHighlightXPos=&bgContentXPos=&bgHeaderXPos=&bgActiveXPos=&bgHoverXPos=&bgDefaultXPos=&bgShadowYPos=&bgOverlayYPos=&bgErrorYPos=&bgHighlightYPos=&bgContentYPos=&bgHeaderYPos=&bgActiveYPos=&bgHoverYPos=&bgDefaultYPos=&bgShadowRepeat=&bgOverlayRepeat=&bgErrorRepeat=&bgHighlightRepeat=&bgContentRepeat=&bgHeaderRepeat=&bgActiveRepeat=&bgHoverRepeat=&bgDefaultRepeat=&iconsHover=url(%22images%2Fui-icons_555555_256x240.png%22)&iconsHighlight=url(%22images%2Fui-icons_777620_256x240.png%22)&iconsHeader=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsError=url(%22images%2Fui-icons_cc0000_256x240.png%22)&iconsDefault=url(%22images%2Fui-icons_777777_256x240.png%22)&iconsContent=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsActive=url(%22images%2Fui-icons_ffffff_256x240.png%22)&bgImgUrlShadow=&bgImgUrlOverlay=&bgImgUrlHover=&bgImgUrlHighlight=&bgImgUrlHeader=&bgImgUrlError=&bgImgUrlDefault=&bgImgUrlContent=&bgImgUrlActive=&opacityFilterShadow=Alpha(Opacity%3D30)&opacityFilterOverlay=Alpha(Opacity%3D30)&opacityShadowPerc=30&opacityOverlayPerc=30&iconColorHover=%23555555&iconColorHighlight=%23777620&iconColorHeader=%23444444&iconColorError=%23cc0000&iconColorDefault=%23777777&iconColorContent=%23444444&iconColorActive=%23ffffff&bgImgOpacityShadow=0&bgImgOpacityOverlay=0&bgImgOpacityError=95&bgImgOpacityHighlight=55&bgImgOpacityContent=75&bgImgOpacityHeader=75&bgImgOpacityActive=65&bgImgOpacityHover=75&bgImgOpacityDefault=75&bgTextureShadow=flat&bgTextureOverlay=flat&bgTextureError=flat&bgTextureHighlight=flat&bgTextureContent=flat&bgTextureHeader=flat&bgTextureActive=flat&bgTextureHover=flat&bgTextureDefault=flat&cornerRadius=3px&fwDefault=normal&ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&cornerRadiusShadow=8px&thicknessShadow=5px&offsetLeftShadow=0px&offsetTopShadow=0px&opacityShadow=.3&bgColorShadow=%23666666&opacityOverlay=.3&bgColorOverlay=%23aaaaaa&fcError=%235f3f3f&borderColorError=%23f1a899&bgColorError=%23fddfdf&fcHighlight=%23777620&borderColorHighlight=%23dad55e&bgColorHighlight=%23fffa90&fcContent=%23333333&borderColorContent=%23dddddd&bgColorContent=%23ffffff&fcHeader=%23333333&borderColorHeader=%23dddddd&bgColorHeader=%23e9e9e9&fcActive=%23ffffff&borderColorActive=%23003eff&bgColorActive=%23007fff&fcHover=%232b2b2b&borderColorHover=%23cccccc&bgColorHover=%23ededed&fcDefault=%23454545&borderColorDefault=%23c5c5c5&bgColorDefault=%23f6f6f6 -* Copyright jQuery Foundation and other contributors; Licensed MIT */ - - -/* Interaction Cues -----------------------------------*/ -.ui-state-disabled { - cursor: default !important; - pointer-events: none; -} - - -/* Misc visuals -----------------------------------*/ - -/* Overlays */ -.ui-widget-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; -} -.ui-autocomplete { - position: absolute; - top: 0; - left: 0; - cursor: default; -} -.ui-menu { - list-style: none; - padding: 0; - margin: 0; - display: block; - outline: 0; -} -.ui-menu .ui-menu { - position: absolute; -} -.ui-menu .ui-menu-item { - margin: 0; - cursor: pointer; -} -.ui-menu .ui-menu-item-wrapper { - position: relative; - padding: 3px 1em 3px .4em; -} -.ui-menu .ui-menu-divider { - margin: 5px 0; - height: 0; - font-size: 0; - line-height: 0; - border-width: 1px 0 0 0; -} -.ui-menu .ui-state-focus, -.ui-menu .ui-state-active { - margin: -1px; -} - -/* icon support */ -.ui-menu-icons { - position: relative; -} -.ui-menu-icons .ui-menu-item-wrapper { - padding-left: 2em; -} - -/* left-aligned */ -.ui-menu .ui-icon { - position: absolute; - top: 0; - bottom: 0; - left: .2em; - margin: auto 0; -} - -/* right-aligned */ -.ui-menu .ui-menu-icon { - left: auto; - right: 0; -} - -/* no icon support for input elements */ -input.ui-button.ui-button-icon-only { - text-indent: 0; -} - -.ui-slider { - position: relative; - text-align: left; -} -.ui-slider .ui-slider-handle { - position: absolute; - z-index: 2; - width: 1.2em; - height: 1.2em; - cursor: default; - -ms-touch-action: none; - touch-action: none; -} -.ui-slider .ui-slider-range { - position: absolute; - z-index: 1; - font-size: .7em; - display: block; - border: 0; - background-position: 0 0; -} - -.ui-slider-horizontal { - height: .8em; -} -.ui-slider-horizontal .ui-slider-handle { - top: -.3em; - margin-left: -.6em; -} -.ui-slider-horizontal .ui-slider-range { - top: 0; - height: 100%; -} -.ui-slider-horizontal .ui-slider-range-min { - left: 0; -} -.ui-slider-horizontal .ui-slider-range-max { - right: 0; -} - -.ui-slider-vertical { - width: .8em; - height: 100px; -} -.ui-slider-vertical .ui-slider-handle { - left: -.3em; - margin-left: 0; - margin-bottom: -.6em; -} -.ui-slider-vertical .ui-slider-range { - left: 0; - width: 100%; -} -.ui-slider-vertical .ui-slider-range-min { - bottom: 0; -} -.ui-slider-vertical .ui-slider-range-max { - top: 0; -} -.ui-tooltip { - padding: 8px; - position: absolute; - z-index: 9999; - max-width: 300px; -} -body .ui-tooltip { - border-width: 2px; -} - -/* Component containers -----------------------------------*/ -.ui-widget { - font-family: Arial,Helvetica,sans-serif; - font-size: 1em; -} -.ui-widget .ui-widget { - font-size: 1em; -} -.ui-widget input, -.ui-widget select, -.ui-widget textarea, -.ui-widget button { - font-family: Arial,Helvetica,sans-serif; - font-size: 1em; -} -.ui-widget.ui-widget-content { - border: 1px solid #c5c5c5; -} -.ui-widget-content { - border: 1px solid #dddddd; - background: #ffffff; - color: #333333; -} -.ui-widget-content a { - color: #333333; -} -.ui-widget-header { - border: 1px solid #dddddd; - background: #e9e9e9; - color: #333333; - font-weight: bold; -} -.ui-widget-header a { - color: #333333; -} - -/* Interaction states -----------------------------------*/ -.ui-state-default, -.ui-widget-content .ui-state-default, -.ui-widget-header .ui-state-default, -.ui-button, - -/* We use html here because we need a greater specificity to make sure disabled -works properly when clicked or hovered */ -html .ui-button.ui-state-disabled:hover, -html .ui-button.ui-state-disabled:active { - background: #f6f6f6; - font-weight: normal; - color: #454545; -} -.ui-state-default a, -.ui-state-default a:link, -.ui-state-default a:visited, -a.ui-button, -a:link.ui-button, -a:visited.ui-button, -.ui-button { - color: #454545; - text-decoration: none; -} -.ui-state-hover, -.ui-widget-content .ui-state-hover, -.ui-widget-header .ui-state-hover, -.ui-state-focus, -.ui-widget-content .ui-state-focus, -.ui-widget-header .ui-state-focus, -.ui-button:hover, -.ui-button:focus { - border: 1px solid #cccccc; - background: #ededed; - font-weight: normal; - color: #2b2b2b; -} -.ui-state-hover a, -.ui-state-hover a:hover, -.ui-state-hover a:link, -.ui-state-hover a:visited, -.ui-state-focus a, -.ui-state-focus a:hover, -.ui-state-focus a:link, -.ui-state-focus a:visited, -a.ui-button:hover, -a.ui-button:focus { - color: #2b2b2b; - text-decoration: none; -} - -.ui-visual-focus { - box-shadow: 0 0 3px 1px rgb(94, 158, 214); -} -.ui-state-active, -.ui-widget-content .ui-state-active, -.ui-widget-header .ui-state-active, -a.ui-button:active, -.ui-button:active, -.ui-button.ui-state-active:hover { - border: var(--slick-autocomplete-hover-border-color, $autocomplete-hover-border-color); - background: var(--slick-autocomplete-hover-bg-color, $autocomplete-hover-bg-color); - font-weight: normal; - color: #ffffff; -} -.ui-icon-background, -.ui-state-active .ui-icon-background { - border: var(--slick-autocomplete-hover-bg-color, $autocomplete-hover-bg-color); - background-color: #ffffff; -} -.ui-state-active a, -.ui-state-active a:link, -.ui-state-active a:visited { - color: #ffffff; - text-decoration: none; -} - -/* Interaction Cues -----------------------------------*/ -.ui-state-highlight, -.ui-widget-content .ui-state-highlight, -.ui-widget-header .ui-state-highlight { - border: 1px solid #dad55e; - background: #fffa90; - color: #777620; -} -.ui-state-checked { - border: 1px solid #dad55e; - background: #fffa90; -} -.ui-state-highlight a, -.ui-widget-content .ui-state-highlight a, -.ui-widget-header .ui-state-highlight a { - color: #777620; -} -.ui-state-error, -.ui-widget-content .ui-state-error, -.ui-widget-header .ui-state-error { - border: 1px solid #f1a899; - background: #fddfdf; - color: #5f3f3f; -} -.ui-state-error a, -.ui-widget-content .ui-state-error a, -.ui-widget-header .ui-state-error a { - color: #5f3f3f; -} -.ui-state-error-text, -.ui-widget-content .ui-state-error-text, -.ui-widget-header .ui-state-error-text { - color: #5f3f3f; -} -.ui-priority-primary, -.ui-widget-content .ui-priority-primary, -.ui-widget-header .ui-priority-primary { - font-weight: bold; -} -.ui-priority-secondary, -.ui-widget-content .ui-priority-secondary, -.ui-widget-header .ui-priority-secondary { - opacity: .7; - filter:Alpha(Opacity=70); /* support: IE8 */ - font-weight: normal; -} -.ui-state-disabled, -.ui-widget-content .ui-state-disabled, -.ui-widget-header .ui-state-disabled { - opacity: .35; - filter:Alpha(Opacity=35); /* support: IE8 */ - background-image: none; -} -.ui-state-disabled .ui-icon { - filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ -} - - -/* Misc visuals -----------------------------------*/ - -/* Corner radius */ -.ui-corner-all, -.ui-corner-top, -.ui-corner-left, -.ui-corner-tl { - border-top-left-radius: 3px; -} -.ui-corner-all, -.ui-corner-top, -.ui-corner-right, -.ui-corner-tr { - border-top-right-radius: 3px; -} -.ui-corner-all, -.ui-corner-bottom, -.ui-corner-left, -.ui-corner-bl { - border-bottom-left-radius: 3px; -} -.ui-corner-all, -.ui-corner-bottom, -.ui-corner-right, -.ui-corner-br { - border-bottom-right-radius: 3px; -} - -/* Overlays */ -.ui-widget-overlay { - background: #aaaaaa; - opacity: .003; - filter: Alpha(Opacity=.3); /* support: IE8 */ -} -.ui-widget-shadow { - -webkit-box-shadow: 0px 0px 5px #666666; - box-shadow: 0px 0px 5px #666666; -} diff --git a/packages/common/src/styles/ui-autocomplete.scss b/packages/common/src/styles/slick-autocomplete.scss similarity index 76% rename from packages/common/src/styles/ui-autocomplete.scss rename to packages/common/src/styles/slick-autocomplete.scss index 729699b64..7a1e80afe 100644 --- a/packages/common/src/styles/ui-autocomplete.scss +++ b/packages/common/src/styles/slick-autocomplete.scss @@ -1,63 +1,48 @@ @import './variables'; -// --------------------------------------------------------- -// jQuery UI AutoComplete for Bootstrap -// --------------------------------------------------------- - -.ui-widget-content { - background: none; -} -.ui-autocomplete { - .ui-menu-item { - color: var(--slick-autocomplete-text-color, $slick-autocomplete-text-color); - .ui-state-active { - color: var(--slick-autocomplete-text-color, $slick-autocomplete-text-color); - &:hover { - margin: 0; - border: 0; - color: var(--slick-autocomplete-text-color, $slick-autocomplete-text-color); - } - } - } -} -.ui-autocomplete { - background: none; - position: absolute; - z-index: var(--slick-autocomplete-z-index, $slick-autocomplete-z-index); - padding: 0; - margin-top: 2px; - list-style: none; +/** + * Kraaden Autocomplete https://github.com/kraaden/autocomplete/blob/master/autocomplete.ts + */ +.slick-autocomplete { background-color: var(--slick-autocomplete-bg-color, $slick-autocomplete-bg-color); - border: var(--slick-autocomplete-border, $slick-autocomplete-border); - border-radius: var(--slick-autocomplete-border-radius, $slick-autocomplete-border-radius); - box-shadow: var(--slick-autocomplete-box-shadow, $slick-autocomplete-box-shadow); - background-clip: padding-box; + color: var(--slick-autocomplete-text-color, $slick-autocomplete-text-color); + border: var(--slick-autocomplete-border, $slick-autocomplete-border); // 1px solid rgba(50, 50, 50, 0.6); + box-sizing: border-box; max-height: var(--slick-autocomplete-max-height, $slick-autocomplete-max-height); min-height: var(--slick-autocomplete-min-height, $slick-autocomplete-min-height); - min-width: var(--slick-autocomplete-min-width, $slick-autocomplete-min-width); - overflow-y: var(--slick-autocomplete-overflow-y, $slick-autocomplete-overflow-y); - overflow-x: var(--slick-autocomplete-overflow-x, $slick-autocomplete-overflow-x); - text-overflow: var(--slick-autocomplete-text-overflow, $slick-autocomplete-text-overflow); - - li { - div { - display: block; - color: var(--slick-autocomplete-text-color, $slick-autocomplete-text-color); - padding: var(--slick-autocomplete-text-padding, $slick-autocomplete-text-padding); - font-weight: normal; - line-height: 1.42857143; - white-space: nowrap; - list-style-image: none; - } + overflow: auto; + width: var(--slick-autocomplete-width, $slick-autocomplete-width) !important; + z-index: var(--slick-autocomplete-z-index, $slick-autocomplete-z-index); + + .empty { + font-style: italic; } } -/* jquery ui loading spinner */ +.slick-autocomplete > div { + padding: 0 4px; +} + +.slick-autocomplete .group { + background: var(--slick-autocomplete-group-bg-color, $slick-autocomplete-group-bg-color); +} + +.slick-autocomplete > div:hover:not(.empty,.group), +.slick-autocomplete > div.selected { + background-color: var(--slick-autocomplete-hover-bg-color, $slick-autocomplete-hover-bg-color); + cursor: pointer; +} + +/* autocomplete loading spinner */ @keyframes md-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } -.ui-autocomplete-loading { + +.autocomplete-filter-container { + display: flex; +} +.slick-autocomplete-loading { background-color: var(--slick-autocomplete-loading-input-bg-color, $slick-autocomplete-loading-input-bg-color) !important; & + span:after { @@ -65,7 +50,7 @@ display: inline-block; font-family: var(--slick-icon-font-family, $slick-icon-font-family); color: var(--slick-autocomplete-loading-icon-color, $slick-autocomplete-loading-icon-color); - content: var(--slick-autocomplete-loading-icon, $slick-autocomplete-loading-icon) !important; /* important is required to override default jquery-ui styling */ + content: var(--slick-autocomplete-loading-icon, $slick-autocomplete-loading-icon); width: var(--slick-autocomplete-loading-icon-width, $slick-autocomplete-loading-icon-width); margin: var(--slick-autocomplete-loading-icon-margin, $slick-autocomplete-loading-icon-margin); line-height: var(--slick-autocomplete-loading-icon-line-height, $slick-autocomplete-loading-icon-line-height); @@ -73,35 +58,15 @@ } } -.ui-state-hover, -.ui-state-active, -.ui-state-focus { - cursor: pointer; - text-decoration: none; - color: var(--slick-autocomplete-hover-color, $slick-autocomplete-hover-color); - background-color: var(--slick-autocomplete-hover-bg-color, $slick-autocomplete-hover-bg-color); -} - -.ui-helper-hidden-accessible { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - position: absolute; - padding: 0; - width: 1px; -} - // --- // AutoComplete Custom Template Styling with 4 corners // --------------------------------------------------- /* autocomplete custom styling */ -.ui-autocomplete.autocomplete-custom-four-corners { +.slick-autocomplete.autocomplete-custom-four-corners { width: var(--slick-autocomplete-tpl4-width, $slick-autocomplete-tpl4-width); } -.ui-autocomplete.autocomplete-custom-four-corners li div.autocomplete-container-list { +.slick-autocomplete.autocomplete-custom-four-corners div div.autocomplete-container-list { width: var(--slick-autocomplete-tpl4-container-list-width, $slick-autocomplete-tpl4-container-list-width); padding: var(--slick-autocomplete-tpl4-container-list-padding, $slick-autocomplete-tpl4-container-list-padding); @@ -179,10 +144,10 @@ // --------------------------------------------------- /* autocomplete custom styling */ -.ui-autocomplete.autocomplete-custom-two-rows { +.slick-autocomplete.autocomplete-custom-two-rows { width: var(--slick-autocomplete-tpl2-width, $slick-autocomplete-tpl2-width); } -.ui-autocomplete.autocomplete-custom-two-rows li div.autocomplete-container-list { +.slick-autocomplete.autocomplete-custom-two-rows div div.autocomplete-container-list { width: var(--slick-autocomplete-tpl2-container-list-width, $slick-autocomplete-tpl2-container-list-width); padding: var(--slick-autocomplete-tpl2-container-list-padding, $slick-autocomplete-tpl2-container-list-padding); diff --git a/packages/common/src/styles/slick-grid.scss b/packages/common/src/styles/slick-grid.scss index 0796a6213..cfbc91678 100644 --- a/packages/common/src/styles/slick-grid.scss +++ b/packages/common/src/styles/slick-grid.scss @@ -87,7 +87,7 @@ } .slick-group { - border-bottom: 2px solid silver; + border-bottom: var(--slick-group-border-bottom, $slick-group-border-bottom); } .slick-group-toggle { @@ -105,12 +105,12 @@ } .slick-group-totals { - color: gray; - background: white; + background: var(--slick-group-totals-text-background, $slick-group-totals-text-background); + color: var(--slick-group-totals-text-color, $slick-group-totals-text-color); } .slick-sortable-placeholder { - background: silver !important; + background: var(--slick-header-column-sortable-background-hover, $slick-header-column-sortable-background-hover); } } @@ -344,7 +344,7 @@ } .slick-sortable-placeholder { - background: silver; + background: var(--slick-header-column-sortable-background-hover, $slick-header-column-sortable-background-hover); } .slick-group-toggle { diff --git a/packages/common/src/styles/slick-plugins.scss b/packages/common/src/styles/slick-plugins.scss index 85b128f36..b60da4ce4 100644 --- a/packages/common/src/styles/slick-plugins.scss +++ b/packages/common/src/styles/slick-plugins.scss @@ -861,20 +861,20 @@ input.flatpickr.form-control { // ---------------------------------------------- .slick-preheader-panel { - .ui-droppable, .ui-droppable-hover { + .slick-dropzone, .slick-dropzone-hover { display: flex; align-items: center; padding: var(--slick-draggable-group-drop-padding, $slick-draggable-group-drop-padding); height: var(--slick-draggable-group-drop-height, $slick-draggable-group-drop-height); - border-top: var(--slick-draggable-group-drop-border-top, $slick-draggable-group-drop-border-top) !important; - border-left: var(--slick-draggable-group-drop-border-left, $slick-draggable-group-drop-border-left) !important; - border-right: var(--slick-draggable-group-drop-border-right, $slick-draggable-group-drop-border-right) !important; - border-bottom: var(--slick-draggable-group-drop-border-bottom, $slick-draggable-group-drop-border-bottom) !important; + border-top: var(--slick-draggable-group-drop-border-top, $slick-draggable-group-drop-border-top); + border-left: var(--slick-draggable-group-drop-border-left, $slick-draggable-group-drop-border-left); + border-right: var(--slick-draggable-group-drop-border-right, $slick-draggable-group-drop-border-right); + border-bottom: var(--slick-draggable-group-drop-border-bottom, $slick-draggable-group-drop-border-bottom); width: var(--slick-draggable-group-drop-width, $slick-draggable-group-drop-width) !important; border-radius: var(--slick-draggable-group-drop-radius, $slick-draggable-group-drop-radius); background-color: var(--slick-draggable-group-drop-bgcolor, $slick-draggable-group-drop-bgcolor); - .slick-draggable-dropbox-toggle-placeholder { + .slick-draggable-dropzone-placeholder { font-style: var(--slick-draggable-group-placeholder-font-style, $slick-draggable-group-placeholder-font-style); color: var(--slick-draggable-group-placeholder-color, $slick-draggable-group-placeholder-color); } @@ -919,11 +919,13 @@ input.flatpickr.form-control { border-radius: var(--slick-draggable-group-column-border-radius, $slick-draggable-group-column-border-radius); padding: var(--slick-draggable-group-column-padding, $slick-draggable-group-column-padding); margin-right: var(--slick-draggable-group-column-margin-right, $slick-draggable-group-column-margin-right); + z-index: 1; } .slick-groupby-remove { cursor: pointer; display: inline-flex; + margin-left: 5px; color: var(--slick-draggable-group-delete-color, $slick-draggable-group-delete-color); font-size: var(--slick-draggable-group-delete-font-size, $slick-draggable-group-delete-font-size); padding-left: var(--slick-draggable-group-delete-padding-left, $slick-draggable-group-delete-padding-left); @@ -934,11 +936,16 @@ input.flatpickr.form-control { } } } - .ui-droppable-active { - background-color: var(--slick-draggable-group-droppable-active-bgcolor, $slick-draggable-group-droppable-active-bgcolor); - } - .ui-droppable-hover { + .slick-dropzone-hover { background-color: var(--slick-draggable-group-droppable-hover-bgcolor, $slick-draggable-group-droppable-hover-bgcolor); + border: var(--slick-draggable-group-drop-border-hover, $slick-draggable-group-drop-border-hover); + } + .slick-dropzone-placeholder-hover { + opacity: var(--slick-draggable-group-drop-placeholder-hover-opacity, $slick-draggable-group-drop-placeholder-hover-opacity); + } + .slick-header-column-active { + // do not display column clone when dragging a column hover dropzone because it adds to the far right and isn't needed + display: none !important; } } diff --git a/packages/common/src/styles/slickgrid-theme-bootstrap.scss b/packages/common/src/styles/slickgrid-theme-bootstrap.scss index 1620c8aaa..79db975ff 100644 --- a/packages/common/src/styles/slickgrid-theme-bootstrap.scss +++ b/packages/common/src/styles/slickgrid-theme-bootstrap.scss @@ -11,4 +11,4 @@ @import './slick-component'; @import './slickgrid-examples'; @import './slick-bootstrap'; -@import './ui-autocomplete'; +@import './slick-autocomplete'; diff --git a/packages/common/src/styles/slickgrid-theme-material.bare.scss b/packages/common/src/styles/slickgrid-theme-material.bare.scss index ec944da09..f251caa6b 100644 --- a/packages/common/src/styles/slickgrid-theme-material.bare.scss +++ b/packages/common/src/styles/slickgrid-theme-material.bare.scss @@ -17,4 +17,4 @@ @import './slick-component'; @import './slickgrid-examples'; @import './slick-bootstrap'; -@import './ui-autocomplete'; +@import './slick-autocomplete'; diff --git a/packages/common/src/styles/slickgrid-theme-material.lite.scss b/packages/common/src/styles/slickgrid-theme-material.lite.scss index 3ea5f1f20..c3f2bef6c 100644 --- a/packages/common/src/styles/slickgrid-theme-material.lite.scss +++ b/packages/common/src/styles/slickgrid-theme-material.lite.scss @@ -19,7 +19,7 @@ @import './slick-component'; @import './slickgrid-examples'; @import './slick-bootstrap'; -@import './ui-autocomplete'; +@import './slick-autocomplete'; @import './material-svg-icons'; @import './material-svg-utilities'; diff --git a/packages/common/src/styles/slickgrid-theme-material.scss b/packages/common/src/styles/slickgrid-theme-material.scss index ae2b784fd..e59e0a067 100644 --- a/packages/common/src/styles/slickgrid-theme-material.scss +++ b/packages/common/src/styles/slickgrid-theme-material.scss @@ -21,7 +21,7 @@ @import './slick-component'; @import './slickgrid-examples'; @import './slick-bootstrap'; - @import './ui-autocomplete'; + @import './slick-autocomplete'; @import './material-svg-icons'; @import './material-svg-utilities'; diff --git a/packages/common/src/styles/slickgrid-theme-salesforce.bare.scss b/packages/common/src/styles/slickgrid-theme-salesforce.bare.scss index 8b81bfc9b..656f5baf4 100644 --- a/packages/common/src/styles/slickgrid-theme-salesforce.bare.scss +++ b/packages/common/src/styles/slickgrid-theme-salesforce.bare.scss @@ -17,4 +17,4 @@ @import './slick-component'; @import './slickgrid-examples'; @import './slick-bootstrap'; -@import './ui-autocomplete'; +@import './slick-autocomplete'; diff --git a/packages/common/src/styles/slickgrid-theme-salesforce.lite.scss b/packages/common/src/styles/slickgrid-theme-salesforce.lite.scss index 764aaec3b..0cd73da49 100644 --- a/packages/common/src/styles/slickgrid-theme-salesforce.lite.scss +++ b/packages/common/src/styles/slickgrid-theme-salesforce.lite.scss @@ -18,7 +18,7 @@ @import './slick-component'; @import './slickgrid-examples'; @import './slick-bootstrap'; -@import './ui-autocomplete'; +@import './slick-autocomplete'; @import './material-svg-icons'; @import './material-svg-utilities'; diff --git a/packages/common/src/styles/slickgrid-theme-salesforce.scss b/packages/common/src/styles/slickgrid-theme-salesforce.scss index 7f8281e15..b4494e8c2 100644 --- a/packages/common/src/styles/slickgrid-theme-salesforce.scss +++ b/packages/common/src/styles/slickgrid-theme-salesforce.scss @@ -11,7 +11,6 @@ @import './flatpickr.min'; @import './multiple-select'; -@import './jquery-ui'; @import './sass-utilities'; @import './variables-theme-salesforce'; @@ -23,7 +22,7 @@ @import './slickgrid-examples'; @import './slick-bootstrap'; @import './slick-filters'; -@import './ui-autocomplete'; +@import './slick-autocomplete'; @import './material-svg-icons'; @import './material-svg-utilities'; diff --git a/packages/composite-editor-component/CHANGELOG.md b/packages/composite-editor-component/CHANGELOG.md index 8350f34cc..83e7b3c2b 100644 --- a/packages/composite-editor-component/CHANGELOG.md +++ b/packages/composite-editor-component/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/composite-editor-component + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) **Note:** Version bump only for package @slickgrid-universal/composite-editor-component diff --git a/packages/composite-editor-component/package.json b/packages/composite-editor-component/package.json index 1e9dcacd7..e65682150 100644 --- a/packages/composite-editor-component/package.json +++ b/packages/composite-editor-component/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/composite-editor-component", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Slick Composite Editor Component - Vanilla Implementation of a Composite Editor Modal Window Component", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/custom-footer-component/CHANGELOG.md b/packages/custom-footer-component/CHANGELOG.md index 911ad6dd7..4861ed3f0 100644 --- a/packages/custom-footer-component/CHANGELOG.md +++ b/packages/custom-footer-component/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/custom-footer-component + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) ### Bug Fixes diff --git a/packages/custom-footer-component/package.json b/packages/custom-footer-component/package.json index 1a8f66ba3..25e8e7dd6 100644 --- a/packages/custom-footer-component/package.json +++ b/packages/custom-footer-component/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/custom-footer-component", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Slick Custom Footer Component - Vanilla Implementation of a Custom Footer Component", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/custom-tooltip-plugin/CHANGELOG.md b/packages/custom-tooltip-plugin/CHANGELOG.md index 27bdbd3a8..ee93f9ee6 100644 --- a/packages/custom-tooltip-plugin/CHANGELOG.md +++ b/packages/custom-tooltip-plugin/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/custom-tooltip-plugin + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) **Note:** Version bump only for package @slickgrid-universal/custom-tooltip-plugin diff --git a/packages/custom-tooltip-plugin/package.json b/packages/custom-tooltip-plugin/package.json index e4cc6a4d6..10c78f9ca 100644 --- a/packages/custom-tooltip-plugin/package.json +++ b/packages/custom-tooltip-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/custom-tooltip-plugin", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "A plugin to add Custom Tooltip when hovering a cell, it subscribes to the cell", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/empty-warning-component/CHANGELOG.md b/packages/empty-warning-component/CHANGELOG.md index 059f1ef04..2e58c0c75 100644 --- a/packages/empty-warning-component/CHANGELOG.md +++ b/packages/empty-warning-component/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/empty-warning-component + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) **Note:** Version bump only for package @slickgrid-universal/empty-warning-component diff --git a/packages/empty-warning-component/package.json b/packages/empty-warning-component/package.json index 2d94f11b0..d0174e1d6 100644 --- a/packages/empty-warning-component/package.json +++ b/packages/empty-warning-component/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/empty-warning-component", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Slick Empty Warning Component - Vanilla Implementation of an Empty Dataset Warning Component", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/event-pub-sub/CHANGELOG.md b/packages/event-pub-sub/CHANGELOG.md index f793f41e2..0af92e1f3 100644 --- a/packages/event-pub-sub/CHANGELOG.md +++ b/packages/event-pub-sub/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/event-pub-sub + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) **Note:** Version bump only for package @slickgrid-universal/event-pub-sub diff --git a/packages/event-pub-sub/package.json b/packages/event-pub-sub/package.json index 59262d099..4e3a36fb2 100644 --- a/packages/event-pub-sub/package.json +++ b/packages/event-pub-sub/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/event-pub-sub", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Simple Vanilla Implementation of an Event PubSub Service to do simply publish/subscribe inter-communication while optionally providing data in the event", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/excel-export/CHANGELOG.md b/packages/excel-export/CHANGELOG.md index 62efffa85..fdd4ced38 100644 --- a/packages/excel-export/CHANGELOG.md +++ b/packages/excel-export/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/excel-export + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) ### Bug Fixes diff --git a/packages/excel-export/package.json b/packages/excel-export/package.json index 47368351c..714260f6d 100644 --- a/packages/excel-export/package.json +++ b/packages/excel-export/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/excel-export", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Excel Export (xls/xlsx) Service.", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index cb3fed5e3..dc0ce92f3 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/graphql + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) ### Bug Fixes diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 8a8871ad2..9527f5686 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/graphql", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "GraphQL Service to sync a grid with a GraphQL backend server", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/odata/CHANGELOG.md b/packages/odata/CHANGELOG.md index 7c4e7e8ec..f4fc80b79 100644 --- a/packages/odata/CHANGELOG.md +++ b/packages/odata/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/odata + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) **Note:** Version bump only for package @slickgrid-universal/odata diff --git a/packages/odata/package.json b/packages/odata/package.json index 2ecfa9930..289a1b7cc 100644 --- a/packages/odata/package.json +++ b/packages/odata/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/odata", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Grid OData Service to sync a grid with an OData backend server", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/pagination-component/CHANGELOG.md b/packages/pagination-component/CHANGELOG.md index b7cb55610..754dcf717 100644 --- a/packages/pagination-component/CHANGELOG.md +++ b/packages/pagination-component/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/pagination-component + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) **Note:** Version bump only for package @slickgrid-universal/pagination-component diff --git a/packages/pagination-component/package.json b/packages/pagination-component/package.json index 624e321a1..29d677069 100644 --- a/packages/pagination-component/package.json +++ b/packages/pagination-component/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/pagination-component", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Slick Pagination Component - Vanilla Implementation of a Pagination Component", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/row-detail-view-plugin/CHANGELOG.md b/packages/row-detail-view-plugin/CHANGELOG.md index 73cdf5e7d..58f6c8c74 100644 --- a/packages/row-detail-view-plugin/CHANGELOG.md +++ b/packages/row-detail-view-plugin/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/row-detail-view-plugin + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) **Note:** Version bump only for package @slickgrid-universal/row-detail-view-plugin diff --git a/packages/row-detail-view-plugin/package.json b/packages/row-detail-view-plugin/package.json index 124263bc5..2166b8fae 100644 --- a/packages/row-detail-view-plugin/package.json +++ b/packages/row-detail-view-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/row-detail-view-plugin", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "SlickRowDetail plugin - A plugin to add Row Detail Panel", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/row-detail-view-plugin/src/slickRowDetailView.ts b/packages/row-detail-view-plugin/src/slickRowDetailView.ts index b517fa29e..3752094f4 100644 --- a/packages/row-detail-view-plugin/src/slickRowDetailView.ts +++ b/packages/row-detail-view-plugin/src/slickRowDetailView.ts @@ -1,6 +1,6 @@ import { Column, - DOMMouseEvent, + DOMMouseOrTouchEvent, ExternalResource, FormatterResultObject, GridOption, @@ -653,7 +653,7 @@ export class SlickRowDetailView implements ExternalResource, UniversalRowDetailV } /** Handle mouse click event */ - protected handleClick(e: DOMMouseEvent, args: { row: number; cell: number; }) { + protected handleClick(e: DOMMouseOrTouchEvent, args: { row: number; cell: number; }) { const dataContext = this._grid.getDataItem(args.row); if (this.checkExpandableOverride(args.row, dataContext, this._grid)) { // clicking on a row select checkbox diff --git a/packages/rxjs-observable/CHANGELOG.md b/packages/rxjs-observable/CHANGELOG.md index d394be4aa..aa994e8f3 100644 --- a/packages/rxjs-observable/CHANGELOG.md +++ b/packages/rxjs-observable/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/rxjs-observable + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) **Note:** Version bump only for package @slickgrid-universal/rxjs-observable diff --git a/packages/rxjs-observable/package.json b/packages/rxjs-observable/package.json index 1400497f3..41c2e64c6 100644 --- a/packages/rxjs-observable/package.json +++ b/packages/rxjs-observable/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/rxjs-observable", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "RxJS Observable Wrapper", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/text-export/CHANGELOG.md b/packages/text-export/CHANGELOG.md index 0bf300d86..a57ca3bbb 100644 --- a/packages/text-export/CHANGELOG.md +++ b/packages/text-export/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/text-export + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) **Note:** Version bump only for package @slickgrid-universal/text-export diff --git a/packages/text-export/README.md b/packages/text-export/README.md index 4a2b90954..f6693bbc3 100644 --- a/packages/text-export/README.md +++ b/packages/text-export/README.md @@ -39,7 +39,7 @@ export class MyExample { initializeGrid { this.gridOptions = { enableTextExport: true, - exportOptions: { + textExportOptions: { sanitizeDataExport: true }, registerExternalResources: [new TextExportService()], @@ -62,7 +62,7 @@ export class MyExample { initializeGrid { this.gridOptions = { enableTextExport: true, - exportOptions: { + textExportOptions: { sanitizeDataExport: true }, registerExternalResources: [this.exportService], diff --git a/packages/text-export/package.json b/packages/text-export/package.json index 8e9667ce6..cb52cad5a 100644 --- a/packages/text-export/package.json +++ b/packages/text-export/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/text-export", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Export to Text File (csv/txt) Service.", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/text-export/src/textExport.service.ts b/packages/text-export/src/textExport.service.ts index f8abaad1a..84d7c0503 100644 --- a/packages/text-export/src/textExport.service.ts +++ b/packages/text-export/src/textExport.service.ts @@ -111,7 +111,7 @@ export class TextExportService implements ExternalResource, BaseTextExportServic return new Promise(resolve => { this._pubSubService?.publish(`onBeforeExportToTextFile`, true); - this._exportOptions = deepCopy({ ...DEFAULT_EXPORT_OPTIONS, ...this._gridOptions.exportOptions, ...this._gridOptions.textExportOptions, ...options }); + this._exportOptions = deepCopy({ ...DEFAULT_EXPORT_OPTIONS, ...this._gridOptions.textExportOptions, ...options }); this._delimiter = this._exportOptions.delimiterOverride || this._exportOptions.delimiter || ''; this._fileFormat = this._exportOptions.format || FileType.csv; diff --git a/packages/tsconfig.base.json b/packages/tsconfig.base.json index 858fdb80b..2f9be6803 100644 --- a/packages/tsconfig.base.json +++ b/packages/tsconfig.base.json @@ -17,6 +17,7 @@ ], "pretty": true, "importHelpers": true, + "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "noImplicitReturns": true, diff --git a/packages/tsconfig.bundle.json b/packages/tsconfig.bundle.json index 4100d7711..67096e144 100644 --- a/packages/tsconfig.bundle.json +++ b/packages/tsconfig.bundle.json @@ -19,6 +19,7 @@ "strictNullChecks": true, "declaration": true, "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "noEmitHelpers": false, "stripInternal": true, diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 492067ed1..0847238ff 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/utils + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) ### Bug Fixes diff --git a/packages/utils/package.json b/packages/utils/package.json index ac9efae7a..c017258e7 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/utils", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Common set of small utilities", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/packages/utils/src/utils.spec.ts b/packages/utils/src/utils.spec.ts index 46d81a8b5..9e648db28 100644 --- a/packages/utils/src/utils.spec.ts +++ b/packages/utils/src/utils.spec.ts @@ -10,6 +10,7 @@ import { hasData, isEmptyObject, isNumber, + isPrimmitive, isObject, isObjectEmpty, parseBoolean, @@ -169,6 +170,38 @@ describe('Service/Utilies', () => { }); }); + describe('isPrimmitive method', () => { + it('should return True when input is undefined', () => { + const result = isPrimmitive(undefined); + expect(result).toBeTrue(); + }); + + it('should return True when input is null', () => { + const result = isPrimmitive(null); + expect(result).toBeTrue(); + }); + + it('should return True when input is a number', () => { + const result = isPrimmitive(0); + expect(result).toBeTrue(); + }); + + it('should return True when input is a string', () => { + const result = isPrimmitive(''); + expect(result).toBeTrue(); + }); + + it('should return False when input is an empty object', () => { + const result = isPrimmitive({}); + expect(result).toBeFalsy(); + }); + + it('should return False when input is a function', () => { + const result = isPrimmitive(() => true); + expect(result).toBeFalsy(); + }); + }); + describe('isNumber method', () => { it('should return True when comparing a number from a number/string variable when strict mode is disable', () => { const result1 = isNumber(22); diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts index bc0585d39..56814e4f6 100644 --- a/packages/utils/src/utils.ts +++ b/packages/utils/src/utils.ts @@ -170,6 +170,15 @@ export function isObject(item: any) { return item !== null && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Date); } +/** + * Simple check to detect if the value is a primitive type + * @param val + * @returns {boolean} + */ +export function isPrimmitive(val: any) { + return val === null || val === undefined || typeof val === 'boolean' || typeof val === 'number' || typeof val === 'string'; +} + /** * Check if a value has any data (undefined, null or empty string will return False...) * NOTE: a `false` boolean is consider as having data so it will return True diff --git a/packages/vanilla-bundle/CHANGELOG.md b/packages/vanilla-bundle/CHANGELOG.md index ddca7a163..d40e5561c 100644 --- a/packages/vanilla-bundle/CHANGELOG.md +++ b/packages/vanilla-bundle/CHANGELOG.md @@ -1,8 +1,15 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +### Features + +* **common:** replace jQueryUI Autocomplete with Kradeen Autocomplete ([#752](https://github.com/ghiscoding/slickgrid-universal/issues/752)) ([991d29c](https://github.com/ghiscoding/slickgrid-universal/commit/991d29c4c8c85d800d69c4ba16d608d7a20d2a90)) - by @ghiscoding + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) ### Bug Fixes diff --git a/packages/vanilla-bundle/package.json b/packages/vanilla-bundle/package.json index be52fb414..3b2335c89 100644 --- a/packages/vanilla-bundle/package.json +++ b/packages/vanilla-bundle/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/vanilla-bundle", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Vanilla Slick Grid Bundle - Framework agnostic the output is to be used in vanilla JS/TS - Written in TypeScript and we also use WebPack to bundle everything into 1 JS file.", "main": "dist/commonjs/index.js", "browser": "src/index.ts", @@ -53,12 +53,13 @@ "dequal": "^2.0.3", "flatpickr": "^4.6.13", "jquery": "^3.6.1", - "jquery-ui": "^1.13.2", - "slickgrid": "^2.4.45", + "slickgrid": "^3.0.0", + "sortablejs": "^1.15.0", "whatwg-fetch": "^3.6.2" }, "devDependencies": { "@types/jquery": "^3.5.14", + "@types/sortablejs": "^1.13.0", "cross-env": "^7.0.3", "npm-run-all2": "^6.0.2", "rimraf": "^3.0.2" diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts index d1654e882..281f4cb30 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts @@ -721,7 +721,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () http.returnKey = 'date'; http.returnValue = '6/24/1984'; http.responseHeaders = { accept: 'json' }; - const collectionAsync = http.fetch('/api', { method: 'GET' }); + const collectionAsync = http.fetch('http://locahost/api', { method: 'GET' }); const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync } }] as Column[]; component.columnDefinitions = mockColDefs; @@ -759,7 +759,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () http.returnKey = 'date'; http.returnValue = '6/24/1984'; http.responseHeaders = { accept: 'json' }; - const collectionAsync = http.fetch('invalid-url', { method: 'GET' }); + const collectionAsync = http.fetch('http://invalid-url', { method: 'GET' }); const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync } }] as Column[]; component.columnDefinitions = mockColDefs; @@ -1127,7 +1127,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () }); it('should invoke "updateFilters" method with filters returned from "getColumnFilters" of the Filter Service when there is no Presets defined', () => { - const mockColumnFilter = { name: { columnId: 'name', columnDef: { id: 'name', field: 'name', filter: { model: Filters.autoComplete } }, operator: 'EQ', searchTerms: ['john'] } }; + const mockColumnFilter = { name: { columnId: 'name', columnDef: { id: 'name', field: 'name', filter: { model: Filters.autocompleter } }, operator: 'EQ', searchTerms: ['john'] } }; jest.spyOn(filterServiceStub, 'getColumnFilters').mockReturnValue(mockColumnFilter as unknown as ColumnFilters); const backendSpy = jest.spyOn(mockGraphqlService, 'updateFilters'); diff --git a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts index 3873d49c3..fe929610e 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -1,17 +1,16 @@ import { dequal } from 'dequal/lite'; import 'jquery'; -import 'jquery-ui/ui/widgets/draggable'; -import 'jquery-ui/ui/widgets/droppable'; -import 'jquery-ui/ui/widgets/sortable'; import 'flatpickr/dist/l10n/fr'; -import 'slickgrid/lib/jquery.event.drag-2.3.0'; -import 'slickgrid/lib/jquery.mousewheel'; import 'slickgrid/slick.core'; +import 'slickgrid/slick.interactions'; import 'slickgrid/slick.grid'; import 'slickgrid/slick.dataview'; +import SortableInstance, * as Sortable_ from 'sortablejs'; +const Sortable = ((Sortable_ as any)?.['default'] ?? Sortable_); // patch for rollup + import { autoAddEditorFormatterToColumnsWithEditor, - AutoCompleteEditor, + AutocompleterEditor, BackendServiceApi, BackendServiceOption, Column, @@ -68,6 +67,9 @@ import { UniversalContainerService } from '../services/universalContainer.servic // using external non-typed js libraries declare const Slick: SlickNamespace; +// add Sortable to the window object so that SlickGrid lib can use globally +(window as any).Sortable = Sortable as SortableInstance; + export class SlickVanillaGridBundle { protected _currentDatasetLength = 0; protected _eventPubSubService!: EventPubSubService; @@ -1442,7 +1444,7 @@ export class SlickVanillaGridBundle { } // get current Editor, remove it from the DOm then re-enable it and re-render it with the new collection. - const currentEditor = this.slickGrid.getCellEditor() as AutoCompleteEditor | SelectEditor; + const currentEditor = this.slickGrid.getCellEditor() as AutocompleterEditor | SelectEditor; if (currentEditor?.disable && currentEditor?.renderDomElement) { currentEditor.destroy(); currentEditor.disable(false); diff --git a/packages/vanilla-force-bundle/CHANGELOG.md b/packages/vanilla-force-bundle/CHANGELOG.md index 635ef0c6f..f928f0183 100644 --- a/packages/vanilla-force-bundle/CHANGELOG.md +++ b/packages/vanilla-force-bundle/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log +## All-in-One SlickGrid framework agnostic wrapper, 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. +# [2.0.0-alpha.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.4.0...v2.0.0-alpha.0) (2022-10-15) + +**Note:** Version bump only for package @slickgrid-universal/vanilla-force-bundle + # [1.4.0](https://github.com/ghiscoding/slickgrid-universal/compare/v1.3.7...v1.4.0) (2022-08-15) **Note:** Version bump only for package @slickgrid-universal/vanilla-force-bundle diff --git a/packages/vanilla-force-bundle/package.json b/packages/vanilla-force-bundle/package.json index b451977b6..6b32ac7ab 100644 --- a/packages/vanilla-force-bundle/package.json +++ b/packages/vanilla-force-bundle/package.json @@ -1,6 +1,6 @@ { "name": "@slickgrid-universal/vanilla-force-bundle", - "version": "1.4.0", + "version": "2.0.0-alpha.0", "description": "Vanilla Slick Grid Bundle (mostly exist for our Salesforce implementation) - Similar to Vanilla Bundle, the only difference is that it adds extra packages within its bundle (CustomTooltip, CompositeEditor & TextExport)", "main": "dist/commonjs/index.js", "browser": "src/index.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06ca64879..fd3abce5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,7 @@ importers: .: specifiers: + '@4tw/cypress-drag-drop': ^2.2.1 '@jest/types': ^29.1.2 '@lerna-lite/cli': ^1.12.0 '@lerna-lite/run': ^1.12.0 @@ -31,7 +32,8 @@ importers: ts-jest: ^29.0.3 typescript: ^4.8.4 devDependencies: - '@jest/types': 29.1.2 + '@4tw/cypress-drag-drop': 2.2.1_cypress@10.10.0 + '@jest/types': 29.2.0 '@lerna-lite/cli': 1.12.0 '@lerna-lite/run': 1.12.0 '@types/jest': 29.1.2 @@ -44,10 +46,10 @@ importers: eslint: 8.25.0 eslint-plugin-import: 2.26.0_zb5prbqp7qzcgafjm73dfpyyvm eslint-plugin-prefer-arrow: 1.2.3_eslint@8.25.0 - jest: 29.1.2_@types+node@18.7.16 - jest-cli: 29.1.2_@types+node@18.7.16 - jest-environment-jsdom: 29.1.2 - jest-extended: 3.1.0_jest@29.1.2 + jest: 29.2.0_@types+node@18.7.16 + jest-cli: 29.2.0_@types+node@18.7.16 + jest-environment-jsdom: 29.2.0 + jest-extended: 3.1.0_jest@29.2.0 jsdom: 20.0.1 jsdom-global: 3.0.2_jsdom@20.0.1 moment-mini: 2.29.4 @@ -55,7 +57,7 @@ importers: rimraf: 3.0.2 rxjs: 7.5.6 serve: 14.0.1 - ts-jest: 29.0.3_tgejc6cnv2pbkgcnmxq3dkgmme + ts-jest: 29.0.3_tffzzkezgwx2pkcwct6fbmkuui typescript: 4.8.4 examples/webpack-demo-vanilla-bundle: @@ -169,6 +171,8 @@ importers: '@types/dompurify': ^2.3.4 '@types/jquery': ^3.5.14 '@types/moment': ^2.13.0 + '@types/sortablejs': ^1.13.0 + autocompleter: ^6.1.3 autoprefixer: ^10.4.12 copyfiles: ^2.4.1 cross-env: ^7.0.3 @@ -176,7 +180,6 @@ importers: dompurify: ^2.4.0 flatpickr: ^4.6.13 jquery: ^3.6.1 - jquery-ui: ^1.13.2 moment-mini: ^2.29.4 multiple-select-modified: ^1.3.17 nodemon: ^2.0.20 @@ -185,24 +188,27 @@ importers: postcss-cli: ^10.0.0 rimraf: ^3.0.2 sass: ^1.55.0 - slickgrid: ^2.4.45 + slickgrid: ^3.0.0 + sortablejs: ^1.15.0 un-flatten-tree: ^2.0.12 dependencies: '@slickgrid-universal/event-pub-sub': link:../event-pub-sub '@slickgrid-universal/utils': link:../utils + autocompleter: 6.1.3 dequal: 2.0.3 dompurify: 2.4.0 flatpickr: 4.6.13 jquery: 3.6.1 - jquery-ui: 1.13.2 moment-mini: 2.29.4 multiple-select-modified: 1.3.17 - slickgrid: 2.4.45 + slickgrid: 3.0.0 + sortablejs: 1.15.0 un-flatten-tree: 2.0.12 devDependencies: '@types/dompurify': 2.3.4 '@types/jquery': 3.5.14 '@types/moment': 2.13.0 + '@types/sortablejs': 1.13.0 autoprefixer: 10.4.12_postcss@8.4.18 copyfiles: 2.4.1 cross-env: 7.0.3 @@ -431,14 +437,15 @@ importers: '@slickgrid-universal/pagination-component': workspace:~ '@slickgrid-universal/utils': workspace:~ '@types/jquery': ^3.5.14 + '@types/sortablejs': ^1.13.0 cross-env: ^7.0.3 dequal: ^2.0.3 flatpickr: ^4.6.13 jquery: ^3.6.1 - jquery-ui: ^1.13.2 npm-run-all2: ^6.0.2 rimraf: ^3.0.2 - slickgrid: ^2.4.45 + slickgrid: ^3.0.0 + sortablejs: ^1.15.0 whatwg-fetch: ^3.6.2 dependencies: '@slickgrid-universal/binding': link:../binding @@ -451,11 +458,12 @@ importers: dequal: 2.0.3 flatpickr: 4.6.13 jquery: 3.6.1 - jquery-ui: 1.13.2 - slickgrid: 2.4.45 + slickgrid: 3.0.0 + sortablejs: 1.15.0 whatwg-fetch: 3.6.2 devDependencies: '@types/jquery': 3.5.14 + '@types/sortablejs': 1.13.0 cross-env: 7.0.3 npm-run-all2: 6.0.2 rimraf: 3.0.2 @@ -513,6 +521,14 @@ importers: packages: + /@4tw/cypress-drag-drop/2.2.1_cypress@10.10.0: + resolution: {integrity: sha512-+ioJSnEwx70IiMyb4pLEjOS5u6AMWRIVCV20toCY7lb0YcvA0ipbjQBa9DdxEI7Zg2E2jtcIj7Rx0e3WNUbk/w==} + peerDependencies: + cypress: ^2.1.0 || ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + dependencies: + cypress: 10.10.0 + dev: true + /@ampproject/remapping/2.2.0: resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} engines: {node: '>=6.0.0'} @@ -930,8 +946,8 @@ packages: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} dev: true - /@humanwhocodes/config-array/0.10.6: - resolution: {integrity: sha512-U/piU+VwXZsIgwnl+N+nRK12jCpHdc3s0UAc6zc1+HUgiESJxClpvYao/x9JwaN7onNeVb7kTlxlAvuEoaJ3ig==} + /@humanwhocodes/config-array/0.10.7: + resolution: {integrity: sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==} engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 @@ -975,20 +991,20 @@ packages: engines: {node: '>=8'} dev: true - /@jest/console/29.1.2: - resolution: {integrity: sha512-ujEBCcYs82BTmRxqfHMQggSlkUZP63AE5YEaTPj7eFyJOzukkTorstOUC7L6nE3w5SYadGVAnTsQ/ZjTGL0qYQ==} + /@jest/console/29.2.0: + resolution: {integrity: sha512-Xz1Wu+ZZxcB3RS8U3HdkFxlRJ7kLXI/by9X7d2/gvseIWPwYu/c1EsYy77cB5iyyHGOy3whS2HycjcuzIF4Jow==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.1.2 + '@jest/types': 29.2.0 '@types/node': 18.7.16 chalk: 4.1.2 - jest-message-util: 29.1.2 - jest-util: 29.1.2 + jest-message-util: 29.2.0 + jest-util: 29.2.0 slash: 3.0.0 dev: true - /@jest/core/29.1.2: - resolution: {integrity: sha512-sCO2Va1gikvQU2ynDN8V4+6wB7iVrD2CvT0zaRst4rglf56yLly0NQ9nuRRAWFeimRf+tCdFsb1Vk1N9LrrMPA==} + /@jest/core/29.2.0: + resolution: {integrity: sha512-+gyJ3bX+kGEW/eqt/0kI7fLjqiFr3AN8O+rlEl1fYRf7D8h4Sj4tBGo9YOSirvWgvemoH2EPRya35bgvcPFzHQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -996,32 +1012,32 @@ packages: node-notifier: optional: true dependencies: - '@jest/console': 29.1.2 - '@jest/reporters': 29.1.2 - '@jest/test-result': 29.1.2 - '@jest/transform': 29.1.2 - '@jest/types': 29.1.2 + '@jest/console': 29.2.0 + '@jest/reporters': 29.2.0 + '@jest/test-result': 29.2.0 + '@jest/transform': 29.2.0 + '@jest/types': 29.2.0 '@types/node': 18.7.16 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.3.2 exit: 0.1.2 graceful-fs: 4.2.10 - jest-changed-files: 29.0.0 - jest-config: 29.1.2_@types+node@18.7.16 - jest-haste-map: 29.1.2 - jest-message-util: 29.1.2 - jest-regex-util: 29.0.0 - jest-resolve: 29.1.2 - jest-resolve-dependencies: 29.1.2 - jest-runner: 29.1.2 - jest-runtime: 29.1.2 - jest-snapshot: 29.1.2 - jest-util: 29.1.2 - jest-validate: 29.1.2 - jest-watcher: 29.1.2 + jest-changed-files: 29.2.0 + jest-config: 29.2.0_@types+node@18.7.16 + jest-haste-map: 29.2.0 + jest-message-util: 29.2.0 + jest-regex-util: 29.2.0 + jest-resolve: 29.2.0 + jest-resolve-dependencies: 29.2.0 + jest-runner: 29.2.0 + jest-runtime: 29.2.0 + jest-snapshot: 29.2.0 + jest-util: 29.2.0 + jest-validate: 29.2.0 + jest-watcher: 29.2.0 micromatch: 4.0.5 - pretty-format: 29.1.2 + pretty-format: 29.2.0 slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: @@ -1029,59 +1045,66 @@ packages: - ts-node dev: true - /@jest/environment/29.1.2: - resolution: {integrity: sha512-rG7xZ2UeOfvOVzoLIJ0ZmvPl4tBEQ2n73CZJSlzUjPw4or1oSWC0s0Rk0ZX+pIBJ04aVr6hLWFn1DFtrnf8MhQ==} + /@jest/environment/29.2.0: + resolution: {integrity: sha512-foaVv1QVPB31Mno3LlL58PxEQQOLZd9zQfCpyQQCQIpUAtdFP1INBjkphxrCfKT13VxpA0z5jFGIkmZk0DAg2Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/fake-timers': 29.1.2 - '@jest/types': 29.1.2 + '@jest/fake-timers': 29.2.0 + '@jest/types': 29.2.0 '@types/node': 18.7.16 - jest-mock: 29.1.2 + jest-mock: 29.2.0 dev: true - /@jest/expect-utils/29.1.2: - resolution: {integrity: sha512-4a48bhKfGj/KAH39u0ppzNTABXQ8QPccWAFUFobWBaEMSMp+sB31Z2fK/l47c4a/Mu1po2ffmfAIPxXbVTXdtg==} + /@jest/expect-utils/29.0.3: + resolution: {integrity: sha512-i1xUkau7K/63MpdwiRqaxgZOjxYs4f0WMTGJnYwUKubsNRZSeQbLorS7+I4uXVF9KQ5r61BUPAUMZ7Lf66l64Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.0.0 + jest-get-type: 29.2.0 + dev: true + + /@jest/expect-utils/29.2.0: + resolution: {integrity: sha512-nz2IDF7nb1qmj9hx8Ja3MFab2q9Ml8QbOaaeJNyX5JQJHU8QUvEDiMctmhGEkk3Kzr8w8vAqz4hPk/ogJSrUhg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.2.0 dev: true - /@jest/expect/29.1.2: - resolution: {integrity: sha512-FXw/UmaZsyfRyvZw3M6POgSNqwmuOXJuzdNiMWW9LCYo0GRoRDhg+R5iq5higmRTHQY7hx32+j7WHwinRmoILQ==} + /@jest/expect/29.2.0: + resolution: {integrity: sha512-+3lxcYL9e0xPJGOR33utxxejn+Mulz40kY0oy0FVsmIESW87NZDJ7B1ovaIqeX0xIgPX4laS5SGlqD2uSoBMcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.1.2 - jest-snapshot: 29.1.2 + expect: 29.2.0 + jest-snapshot: 29.2.0 transitivePeerDependencies: - supports-color dev: true - /@jest/fake-timers/29.1.2: - resolution: {integrity: sha512-GppaEqS+QQYegedxVMpCe2xCXxxeYwQ7RsNx55zc8f+1q1qevkZGKequfTASI7ejmg9WwI+SJCrHe9X11bLL9Q==} + /@jest/fake-timers/29.2.0: + resolution: {integrity: sha512-mX0V0uQsgeSLTt0yTqanAhhpeUKMGd2uq+PSLAfO40h72bvfNNQ7pIEl9vIwNMFxRih1ENveEjSBsLjxGGDPSw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.1.2 + '@jest/types': 29.2.0 '@sinonjs/fake-timers': 9.1.2 '@types/node': 18.7.16 - jest-message-util: 29.1.2 - jest-mock: 29.1.2 - jest-util: 29.1.2 + jest-message-util: 29.2.0 + jest-mock: 29.2.0 + jest-util: 29.2.0 dev: true - /@jest/globals/29.1.2: - resolution: {integrity: sha512-uMgfERpJYoQmykAd0ffyMq8wignN4SvLUG6orJQRe9WAlTRc9cdpCaE/29qurXixYJVZWUqIBXhSk8v5xN1V9g==} + /@jest/globals/29.2.0: + resolution: {integrity: sha512-JQxtEVNWiai1p3PIzAJZSyEqQdAJGvNKvinZDPfu0mhiYEVx6E+PiBuDWj1sVUW8hzu+R3DVqaWC9K2xcLRIAA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.1.2 - '@jest/expect': 29.1.2 - '@jest/types': 29.1.2 - jest-mock: 29.1.2 + '@jest/environment': 29.2.0 + '@jest/expect': 29.2.0 + '@jest/types': 29.2.0 + jest-mock: 29.2.0 transitivePeerDependencies: - supports-color dev: true - /@jest/reporters/29.1.2: - resolution: {integrity: sha512-X4fiwwyxy9mnfpxL0g9DD0KcTmEIqP0jUdnc2cfa9riHy+I6Gwwp5vOZiwyg0vZxfSDxrOlK9S4+340W4d+DAA==} + /@jest/reporters/29.2.0: + resolution: {integrity: sha512-BXoAJatxTZ18U0cwD7C8qBo8V6vef8AXYRBZdhqE5DF9CmpqmhMfw9c7OUvYqMTnBBK9A0NgXGO4Lc9EJzdHvw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -1090,10 +1113,10 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.1.2 - '@jest/test-result': 29.1.2 - '@jest/transform': 29.1.2 - '@jest/types': 29.1.2 + '@jest/console': 29.2.0 + '@jest/test-result': 29.2.0 + '@jest/transform': 29.2.0 + '@jest/types': 29.2.0 '@jridgewell/trace-mapping': 0.3.15 '@types/node': 18.7.16 chalk: 4.1.2 @@ -1106,13 +1129,12 @@ packages: istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.4 - jest-message-util: 29.1.2 - jest-util: 29.1.2 - jest-worker: 29.1.2 + jest-message-util: 29.2.0 + jest-util: 29.2.0 + jest-worker: 29.2.0 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 - terminal-link: 2.1.1 v8-to-istanbul: 9.0.1 transitivePeerDependencies: - supports-color @@ -1125,8 +1147,8 @@ packages: '@sinclair/typebox': 0.24.19 dev: true - /@jest/source-map/29.0.0: - resolution: {integrity: sha512-nOr+0EM8GiHf34mq2GcJyz/gYFyLQ2INDhAylrZJ9mMWoW21mLBfZa0BUVPPMxVYrLjeiRe2Z7kWXOGnS0TFhQ==} + /@jest/source-map/29.2.0: + resolution: {integrity: sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jridgewell/trace-mapping': 0.3.15 @@ -1134,41 +1156,41 @@ packages: graceful-fs: 4.2.10 dev: true - /@jest/test-result/29.1.2: - resolution: {integrity: sha512-jjYYjjumCJjH9hHCoMhA8PCl1OxNeGgAoZ7yuGYILRJX9NjgzTN0pCT5qAoYR4jfOP8htIByvAlz9vfNSSBoVg==} + /@jest/test-result/29.2.0: + resolution: {integrity: sha512-l76EPJ6QqtzsCLS4aimJqWO53pxZ82o3aE+Brcmo1HJ/phb9+MR7gPhyDdN6VSGaLJCRVJBZgWEhAEz+qON0Fw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.1.2 - '@jest/types': 29.1.2 + '@jest/console': 29.2.0 + '@jest/types': 29.2.0 '@types/istanbul-lib-coverage': 2.0.4 collect-v8-coverage: 1.0.1 dev: true - /@jest/test-sequencer/29.1.2: - resolution: {integrity: sha512-fU6dsUqqm8sA+cd85BmeF7Gu9DsXVWFdGn9taxM6xN1cKdcP/ivSgXh5QucFRFz1oZxKv3/9DYYbq0ULly3P/Q==} + /@jest/test-sequencer/29.2.0: + resolution: {integrity: sha512-NCnjZcGnVdva6IDqF7TCuFsXs2F1tohiNF9sasSJNzD7VfN5ic9XgcS/oPDalGiPLxCmGKj4kewqqrKAqBACcQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.1.2 + '@jest/test-result': 29.2.0 graceful-fs: 4.2.10 - jest-haste-map: 29.1.2 + jest-haste-map: 29.2.0 slash: 3.0.0 dev: true - /@jest/transform/29.1.2: - resolution: {integrity: sha512-2uaUuVHTitmkx1tHF+eBjb4p7UuzBG7SXIaA/hNIkaMP6K+gXYGxP38ZcrofzqN0HeZ7A90oqsOa97WU7WZkSw==} + /@jest/transform/29.2.0: + resolution: {integrity: sha512-NXMujGHy+B4DAj4dGnVPD0SIXlR2Z/N8Gp9h3mF66kcIRult1WWqY3/CEIrJcKviNWaFPYhZjCG2L3fteWzcUw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.18.6 - '@jest/types': 29.1.2 + '@jest/types': 29.2.0 '@jridgewell/trace-mapping': 0.3.15 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 1.8.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.10 - jest-haste-map: 29.1.2 - jest-regex-util: 29.0.0 - jest-util: 29.1.2 + jest-haste-map: 29.2.0 + jest-regex-util: 29.2.0 + jest-util: 29.2.0 micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 @@ -1177,8 +1199,8 @@ packages: - supports-color dev: true - /@jest/types/29.1.2: - resolution: {integrity: sha512-DcXGtoTykQB5jiwCmVr8H4vdg2OJhQex3qPkG+ISyDO7xQXbt/4R6dowcRyPemRnkH7JoHvZuxPBdlq+9JxFCg==} + /@jest/types/29.2.0: + resolution: {integrity: sha512-mfgpQz4Z2xGo37m6KD8xEpKelaVzvYVRijmLPePn9pxgaPEtX+SqIyPNzzoeCPXKYbB4L/wYSgXDL8o3Gop78Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.0.0 @@ -1227,6 +1249,13 @@ packages: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} dev: true + /@jridgewell/trace-mapping/0.3.14: + resolution: {integrity: sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==} + dependencies: + '@jridgewell/resolve-uri': 3.0.8 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + /@jridgewell/trace-mapping/0.3.15: resolution: {integrity: sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==} dependencies: @@ -1946,8 +1975,8 @@ packages: /@types/jest/29.1.2: resolution: {integrity: sha512-y+nlX0h87U0R+wsGn6EBuoRWYyv3KFtwRNP3QWp9+k2tJ2/bqcGS3UxD7jgT+tiwJWWq3UsyV4Y+T6rsMT4XMg==} dependencies: - expect: 29.1.2 - pretty-format: 29.1.2 + expect: 29.0.3 + pretty-format: 29.0.3 dev: true /@types/jquery/3.5.14: @@ -2054,6 +2083,10 @@ packages: '@types/node': 18.7.16 dev: true + /@types/sortablejs/1.13.0: + resolution: {integrity: sha512-C3064MH72iEfeGCYEGCt7FCxXoAXaMPG0QPnstcxvPmbl54erpISu06d++FY37Smja64iWy5L8wOyHHBghWbJQ==} + dev: true + /@types/stack-utils/2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true @@ -2775,6 +2808,10 @@ packages: engines: {node: '>= 4.0.0'} dev: true + /autocompleter/6.1.3: + resolution: {integrity: sha512-Pjb5R5r+S0/zDFudLP9a8CW7/xMc7O/uVCtaTf3f+RdNLAQQ5oUG018c3IRdDJMRVvT+OeZ1NYQoUH5GHlORKQ==} + dev: false + /autoprefixer/10.4.12_postcss@8.4.18: resolution: {integrity: sha512-WrCGV9/b97Pa+jtwf5UGaRjgQIg7OK3D06GnoYoZNcG1Xb8Gt3EfuKjlhh9i/VtT16g6PYjZ69jdJ2g8FxSC4Q==} engines: {node: ^10 || ^12 || >=14} @@ -2783,7 +2820,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.21.4 - caniuse-lite: 1.0.30001409 + caniuse-lite: 1.0.30001419 fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -2799,17 +2836,17 @@ packages: resolution: {integrity: sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==} dev: true - /babel-jest/29.1.2_@babel+core@7.18.6: - resolution: {integrity: sha512-IuG+F3HTHryJb7gacC7SQ59A9kO56BctUsT67uJHp1mMCHUOMXpDwOHWGifWqdWVknN2WNkCVQELPjXx0aLJ9Q==} + /babel-jest/29.2.0_@babel+core@7.18.6: + resolution: {integrity: sha512-c8FkrW1chgcbyBqOo7jFGpQYfVnb43JqjQGV+C2r94k2rZJOukYOZ6+csAqKE4ms+PHc+yevnONxs27jQIxylw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: '@babel/core': 7.18.6 - '@jest/transform': 29.1.2 + '@jest/transform': 29.2.0 '@types/babel__core': 7.1.19 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.0.2_@babel+core@7.18.6 + babel-preset-jest: 29.2.0_@babel+core@7.18.6 chalk: 4.1.2 graceful-fs: 4.2.10 slash: 3.0.0 @@ -2830,8 +2867,8 @@ packages: - supports-color dev: true - /babel-plugin-jest-hoist/29.0.2: - resolution: {integrity: sha512-eBr2ynAEFjcebVvu8Ktx580BD1QKCrBG1XwEUTXJe285p9HA/4hOhfWCFRQhTKSyBV0VzjhG7H91Eifz9s29hg==} + /babel-plugin-jest-hoist/29.2.0: + resolution: {integrity: sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.18.6 @@ -2860,14 +2897,14 @@ packages: '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.18.6 dev: true - /babel-preset-jest/29.0.2_@babel+core@7.18.6: - resolution: {integrity: sha512-BeVXp7rH5TK96ofyEnHjznjLMQ2nAeDJ+QzxKnHAAMs0RgrQsCywjAN8m4mOm5Di0pxU//3AoEeJJrerMH5UeA==} + /babel-preset-jest/29.2.0_@babel+core@7.18.6: + resolution: {integrity: sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.18.6 - babel-plugin-jest-hoist: 29.0.2 + babel-plugin-jest-hoist: 29.2.0 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.18.6 dev: true @@ -3002,10 +3039,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001409 - electron-to-chromium: 1.4.257 + caniuse-lite: 1.0.30001399 + electron-to-chromium: 1.4.210 node-releases: 2.0.6 - update-browserslist-db: 1.0.9_browserslist@4.21.1 + update-browserslist-db: 1.0.5_browserslist@4.21.1 dev: true /browserslist/4.21.4: @@ -3013,10 +3050,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001409 - electron-to-chromium: 1.4.257 + caniuse-lite: 1.0.30001419 + electron-to-chromium: 1.4.283 node-releases: 2.0.6 - update-browserslist-db: 1.0.9_browserslist@4.21.4 + update-browserslist-db: 1.0.10_browserslist@4.21.4 dev: true /bs-logger/0.2.6: @@ -3175,8 +3212,12 @@ packages: engines: {node: '>=14.16'} dev: true - /caniuse-lite/1.0.30001409: - resolution: {integrity: sha512-V0mnJ5dwarmhYv8/MzhJ//aW68UpvnQBXv8lJ2QUsvn2pHcmAuNtu8hQEDz37XnA1iE+lRR9CIfGWWpgJ5QedQ==} + /caniuse-lite/1.0.30001399: + resolution: {integrity: sha512-4vQ90tMKS+FkvuVWS5/QY1+d805ODxZiKFzsU8o/RsVJz49ZSRR8EjykLJbqhzdPgadbX6wB538wOzle3JniRA==} + dev: true + + /caniuse-lite/1.0.30001419: + resolution: {integrity: sha512-aFO1r+g6R7TW+PNQxKzjITwLOyDhVRLjW0LcwS/HCZGUUKTGNp9+IwLC4xyDSZBygVL/mxaFR3HIV6wEKQuSzw==} dev: true /caseless/0.12.0: @@ -3447,8 +3488,8 @@ packages: engines: {node: '>= 12'} dev: true - /commander/9.4.0: - resolution: {integrity: sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==} + /commander/9.4.1: + resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==} engines: {node: ^12.20.0 || >=14} dev: true @@ -3944,8 +3985,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /decimal.js/10.4.1: - resolution: {integrity: sha512-F29o+vci4DodHYT9UrR5IEbfBw9pE5eSapIJdTqXK5+6hq+t8VRxwQyKlW2i+KDKFkkJQRvFyI/QXD83h8LyQw==} + /decimal.js/10.4.2: + resolution: {integrity: sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==} dev: true /dedent/0.7.0: @@ -4074,6 +4115,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff-sequences/29.2.0: + resolution: {integrity: sha512-413SY5JpYeSBZxmenGEmCVQ8mCgtFJF0w9PROdaS6z987XC2Pd2GOKqOITLtMftmyFZqgtCOb/QA7/Z3ZXfzIw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /dir-glob/3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -4188,8 +4234,12 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: true - /electron-to-chromium/1.4.257: - resolution: {integrity: sha512-C65sIwHqNnPC2ADMfse/jWTtmhZMII+x6ADI9gENzrOiI7BpxmfKFE84WkIEl5wEg+7+SfIkwChDlsd1Erju2A==} + /electron-to-chromium/1.4.210: + resolution: {integrity: sha512-kSiX4tuyZijV7Cz0MWVmGT8K2siqaOA4Z66K5dCttPPRh0HicOcOAEj1KlC8O8J1aOS/1M8rGofOzksLKaHWcQ==} + dev: true + + /electron-to-chromium/1.4.283: + resolution: {integrity: sha512-g6RQ9zCOV+U5QVHW9OpFR7rdk/V7xfopNXnyAamdpFgCHgZ1sjI8VuR1+zG2YG/TZk+tQ8mpNkug4P8FU0fuOA==} dev: true /emittery/0.10.2: @@ -4248,6 +4298,11 @@ packages: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} dev: true + /entities/4.3.1: + resolution: {integrity: sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg==} + engines: {node: '>=0.12'} + dev: true + /entities/4.4.0: resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} engines: {node: '>=0.12'} @@ -4698,7 +4753,7 @@ packages: hasBin: true dependencies: '@eslint/eslintrc': 1.3.3 - '@humanwhocodes/config-array': 0.10.6 + '@humanwhocodes/config-array': 0.10.7 '@humanwhocodes/module-importer': 1.0.1 ajv: 6.12.6 chalk: 4.1.2 @@ -4856,15 +4911,26 @@ packages: engines: {node: '>= 0.8.0'} dev: true - /expect/29.1.2: - resolution: {integrity: sha512-AuAGn1uxva5YBbBlXb+2JPxJRuemZsmlGcapPXWNSBNsQtAULfjioREGBWuI0EOvYUKjDnrCy8PW5Zlr1md5mw==} + /expect/29.0.3: + resolution: {integrity: sha512-t8l5DTws3212VbmPL+tBFXhjRHLmctHB0oQbL8eUc6S7NzZtYUhycrFO9mkxA0ZUC6FAWdNi7JchJSkODtcu1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/expect-utils': 29.1.2 + '@jest/expect-utils': 29.0.3 jest-get-type: 29.0.0 - jest-matcher-utils: 29.1.2 - jest-message-util: 29.1.2 - jest-util: 29.1.2 + jest-matcher-utils: 29.0.3 + jest-message-util: 29.0.3 + jest-util: 29.2.0 + dev: true + + /expect/29.2.0: + resolution: {integrity: sha512-03ClF3GWwUqd9Grgkr9ZSdaCJGMRA69PQ8jT7o+Bx100VlGiAFf9/8oIm9Qve7ZVJhuJxFftqFhviZJRxxNfvg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.2.0 + jest-get-type: 29.2.0 + jest-matcher-utils: 29.2.0 + jest-message-util: 29.2.0 + jest-util: 29.2.0 dev: true /express/4.18.1: @@ -5618,11 +5684,11 @@ packages: dependencies: camel-case: 4.1.2 clean-css: 5.2.0 - commander: 9.4.0 - entities: 4.4.0 + commander: 9.4.1 + entities: 4.3.1 param-case: 3.0.4 relateurl: 0.2.7 - terser: 5.15.0 + terser: 5.15.1 dev: true /html-webpack-plugin/5.5.0_webpack@5.74.0: @@ -6157,7 +6223,7 @@ packages: dev: true /isarray/0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + resolution: {integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=} dev: true /isarray/1.0.0: @@ -6222,43 +6288,43 @@ packages: istanbul-lib-report: 3.0.0 dev: true - /jest-changed-files/29.0.0: - resolution: {integrity: sha512-28/iDMDrUpGoCitTURuDqUzWQoWmOmOKOFST1mi2lwh62X4BFf6khgH3uSuo1e49X/UDjuApAj3w0wLOex4VPQ==} + /jest-changed-files/29.2.0: + resolution: {integrity: sha512-qPVmLLyBmvF5HJrY7krDisx6Voi8DmlV3GZYX0aFNbaQsZeoz1hfxcCMbqDGuQCxU1dJy9eYc2xscE8QrCCYaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: execa: 5.1.1 p-limit: 3.1.0 dev: true - /jest-circus/29.1.2: - resolution: {integrity: sha512-ajQOdxY6mT9GtnfJRZBRYS7toNIJayiiyjDyoZcnvPRUPwJ58JX0ci0PKAKUo2C1RyzlHw0jabjLGKksO42JGA==} + /jest-circus/29.2.0: + resolution: {integrity: sha512-bpJRMe+VtvYlF3q8JNx+/cAo4FYvNCiR5s7Z0Scf8aC+KJ2ineSjZKtw1cIZbythlplkiro0My8nc65pfCqJ3A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.1.2 - '@jest/expect': 29.1.2 - '@jest/test-result': 29.1.2 - '@jest/types': 29.1.2 + '@jest/environment': 29.2.0 + '@jest/expect': 29.2.0 + '@jest/test-result': 29.2.0 + '@jest/types': 29.2.0 '@types/node': 18.7.16 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 is-generator-fn: 2.1.0 - jest-each: 29.1.2 - jest-matcher-utils: 29.1.2 - jest-message-util: 29.1.2 - jest-runtime: 29.1.2 - jest-snapshot: 29.1.2 - jest-util: 29.1.2 + jest-each: 29.2.0 + jest-matcher-utils: 29.2.0 + jest-message-util: 29.2.0 + jest-runtime: 29.2.0 + jest-snapshot: 29.2.0 + jest-util: 29.2.0 p-limit: 3.1.0 - pretty-format: 29.1.2 + pretty-format: 29.2.0 slash: 3.0.0 stack-utils: 2.0.5 transitivePeerDependencies: - supports-color dev: true - /jest-cli/29.1.2_@types+node@18.7.16: - resolution: {integrity: sha512-vsvBfQ7oS2o4MJdAH+4u9z76Vw5Q8WBQF5MchDbkylNknZdrPTX1Ix7YRJyTlOWqRaS7ue/cEAn+E4V1MWyMzw==} + /jest-cli/29.2.0_@types+node@18.7.16: + resolution: {integrity: sha512-/581TzbXeO+5kbtSlhXEthGiVJCC8AP0jgT0iZINAAMW+tTFj2uWU7z+HNUH5yIYdHV7AvRr0fWLrmHJGIruHg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -6267,16 +6333,16 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.1.2 - '@jest/test-result': 29.1.2 - '@jest/types': 29.1.2 + '@jest/core': 29.2.0 + '@jest/test-result': 29.2.0 + '@jest/types': 29.2.0 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 import-local: 3.1.0 - jest-config: 29.1.2_@types+node@18.7.16 - jest-util: 29.1.2 - jest-validate: 29.1.2 + jest-config: 29.2.0_@types+node@18.7.16 + jest-util: 29.2.0 + jest-validate: 29.2.0 prompts: 2.4.2 yargs: 17.6.0 transitivePeerDependencies: @@ -6285,8 +6351,8 @@ packages: - ts-node dev: true - /jest-config/29.1.2_@types+node@18.7.16: - resolution: {integrity: sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA==} + /jest-config/29.2.0_@types+node@18.7.16: + resolution: {integrity: sha512-IkdCsrHIoxDPZAyFcdtQrCQ3uftLqns6Joj0tlbxiAQW4k/zTXmIygqWBmPNxO9FbFkDrhtYZiLHXjaJh9rS+Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' @@ -6298,26 +6364,26 @@ packages: optional: true dependencies: '@babel/core': 7.18.6 - '@jest/test-sequencer': 29.1.2 - '@jest/types': 29.1.2 + '@jest/test-sequencer': 29.2.0 + '@jest/types': 29.2.0 '@types/node': 18.7.16 - babel-jest: 29.1.2_@babel+core@7.18.6 + babel-jest: 29.2.0_@babel+core@7.18.6 chalk: 4.1.2 ci-info: 3.3.2 deepmerge: 4.2.2 glob: 7.2.3 graceful-fs: 4.2.10 - jest-circus: 29.1.2 - jest-environment-node: 29.1.2 - jest-get-type: 29.0.0 - jest-regex-util: 29.0.0 - jest-resolve: 29.1.2 - jest-runner: 29.1.2 - jest-util: 29.1.2 - jest-validate: 29.1.2 + jest-circus: 29.2.0 + jest-environment-node: 29.2.0 + jest-get-type: 29.2.0 + jest-regex-util: 29.2.0 + jest-resolve: 29.2.0 + jest-runner: 29.2.0 + jest-util: 29.2.0 + jest-validate: 29.2.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.1.2 + pretty-format: 29.2.0 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -6331,75 +6397,89 @@ packages: chalk: 4.1.2 diff-sequences: 29.0.0 jest-get-type: 29.0.0 - pretty-format: 29.1.2 + pretty-format: 29.0.3 dev: true - /jest-diff/29.1.2: - resolution: {integrity: sha512-4GQts0aUopVvecIT4IwD/7xsBaMhKTYoM4/njE/aVw9wpw+pIUVp8Vab/KnSzSilr84GnLBkaP3JLDnQYCKqVQ==} + /jest-diff/29.0.3: + resolution: {integrity: sha512-+X/AIF5G/vX9fWK+Db9bi9BQas7M9oBME7egU7psbn4jlszLFCu0dW63UgeE6cs/GANq4fLaT+8sGHQQ0eCUfg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 diff-sequences: 29.0.0 - jest-get-type: 29.0.0 - pretty-format: 29.1.2 + jest-get-type: 29.2.0 + pretty-format: 29.2.0 dev: true - /jest-docblock/29.0.0: - resolution: {integrity: sha512-s5Kpra/kLzbqu9dEjov30kj1n4tfu3e7Pl8v+f8jOkeWNqM6Ds8jRaJfZow3ducoQUrf2Z4rs2N5S3zXnb83gw==} + /jest-diff/29.2.0: + resolution: {integrity: sha512-GsH07qQL+/D/GxlnU+sSg9GL3fBOcuTlmtr3qr2pnkiODCwubNN2/7slW4m3CvxDsEus/VEOfQKRFLyXsUlnZw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.2.0 + jest-get-type: 29.2.0 + pretty-format: 29.2.0 + dev: true + + /jest-docblock/29.2.0: + resolution: {integrity: sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: detect-newline: 3.1.0 dev: true - /jest-each/29.1.2: - resolution: {integrity: sha512-AmTQp9b2etNeEwMyr4jc0Ql/LIX/dhbgP21gHAizya2X6rUspHn2gysMXaj6iwWuOJ2sYRgP8c1P4cXswgvS1A==} + /jest-each/29.2.0: + resolution: {integrity: sha512-h4LeC3L/R7jIMfTdYowevPIssvcPYQ7Qzs+pCSYsJgPztIizXwKmnfhZXBA4WVqdmvMcpmseYEXb67JT7IJ2eg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.1.2 + '@jest/types': 29.2.0 chalk: 4.1.2 - jest-get-type: 29.0.0 - jest-util: 29.1.2 - pretty-format: 29.1.2 + jest-get-type: 29.2.0 + jest-util: 29.2.0 + pretty-format: 29.2.0 dev: true - /jest-environment-jsdom/29.1.2: - resolution: {integrity: sha512-D+XNIKia5+uDjSMwL/G1l6N9MCb7LymKI8FpcLo7kkISjc/Sa9w+dXXEa7u1Wijo3f8sVLqfxdGqYtRhmca+Xw==} + /jest-environment-jsdom/29.2.0: + resolution: {integrity: sha512-DgHbBxC4RmHpDLFLMt00NjXXimGvtNALRyxQYOo3e6vwq1qsIDqXsEZiuEpjTg0BueENE1mx8BKFKHXArEdRQQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true dependencies: - '@jest/environment': 29.1.2 - '@jest/fake-timers': 29.1.2 - '@jest/types': 29.1.2 + '@jest/environment': 29.2.0 + '@jest/fake-timers': 29.2.0 + '@jest/types': 29.2.0 '@types/jsdom': 20.0.0 '@types/node': 18.7.16 - jest-mock: 29.1.2 - jest-util: 29.1.2 + jest-mock: 29.2.0 + jest-util: 29.2.0 jsdom: 20.0.1 transitivePeerDependencies: - bufferutil - - canvas - supports-color - utf-8-validate dev: true - /jest-environment-node/29.1.2: - resolution: {integrity: sha512-C59yVbdpY8682u6k/lh8SUMDJPbOyCHOTgLVVi1USWFxtNV+J8fyIwzkg+RJIVI30EKhKiAGNxYaFr3z6eyNhQ==} + /jest-environment-node/29.2.0: + resolution: {integrity: sha512-b4qQGVStPMvtZG97Ac0rvnmSIjCZturFU7MQRMp4JDFl7zoaDLTtXmFjFP1tNmi9te6kR8d+Htbv3nYeoaIz6g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.1.2 - '@jest/fake-timers': 29.1.2 - '@jest/types': 29.1.2 + '@jest/environment': 29.2.0 + '@jest/fake-timers': 29.2.0 + '@jest/types': 29.2.0 '@types/node': 18.7.16 - jest-mock: 29.1.2 - jest-util: 29.1.2 + jest-mock: 29.2.0 + jest-util: 29.2.0 dev: true - /jest-extended/3.1.0_jest@29.1.2: + /jest-extended/3.1.0_jest@29.2.0: resolution: {integrity: sha512-BbuAVUb2dchgwm7euayVt/7hYlkKaknQItKyzie7Li8fmXCglgf21XJeRIdOITZ/cMOTTj5Oh5IjQOxQOe/hfQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: jest: '>=27.2.5' dependencies: - jest: 29.1.2_@types+node@18.7.16 + jest: 29.2.0_@types+node@18.7.16 jest-diff: 29.0.0 jest-get-type: 29.0.0 dev: true @@ -6409,68 +6489,98 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-haste-map/29.1.2: - resolution: {integrity: sha512-xSjbY8/BF11Jh3hGSPfYTa/qBFrm3TPM7WU8pU93m2gqzORVLkHFWvuZmFsTEBPRKndfewXhMOuzJNHyJIZGsw==} + /jest-get-type/29.2.0: + resolution: {integrity: sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map/29.2.0: + resolution: {integrity: sha512-qu9lGFi7qJ8v37egS1phZZUJYiMyWnKwu83NlNT1qs50TbedIX2hFl+9ztsJ7U/ENaHwk1/Bs8fqOIQsScIRwg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.1.2 + '@jest/types': 29.2.0 '@types/graceful-fs': 4.1.5 '@types/node': 18.7.16 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 - jest-regex-util: 29.0.0 - jest-util: 29.1.2 - jest-worker: 29.1.2 + jest-regex-util: 29.2.0 + jest-util: 29.2.0 + jest-worker: 29.2.0 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 dev: true - /jest-leak-detector/29.1.2: - resolution: {integrity: sha512-TG5gAZJpgmZtjb6oWxBLf2N6CfQ73iwCe6cofu/Uqv9iiAm6g502CAnGtxQaTfpHECBdVEMRBhomSXeLnoKjiQ==} + /jest-leak-detector/29.2.0: + resolution: {integrity: sha512-FXT9sCFdct42+oOqGIr/9kmUw3RbhvpkwidCBT5ySHHoWNGd3c9n7HXpFKjEz9UnUITRCGdn0q2s6Sxrq36kwg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.0.0 - pretty-format: 29.1.2 + jest-get-type: 29.2.0 + pretty-format: 29.2.0 dev: true - /jest-matcher-utils/29.1.2: - resolution: {integrity: sha512-MV5XrD3qYSW2zZSHRRceFzqJ39B2z11Qv0KPyZYxnzDHFeYZGJlgGi0SW+IXSJfOewgJp/Km/7lpcFT+cgZypw==} + /jest-matcher-utils/29.0.3: + resolution: {integrity: sha512-RsR1+cZ6p1hDV4GSCQTg+9qjeotQCgkaleIKLK7dm+U4V/H2bWedU3RAtLm8+mANzZ7eDV33dMar4pejd7047w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 - jest-diff: 29.1.2 - jest-get-type: 29.0.0 - pretty-format: 29.1.2 + jest-diff: 29.0.3 + jest-get-type: 29.2.0 + pretty-format: 29.2.0 + dev: true + + /jest-matcher-utils/29.2.0: + resolution: {integrity: sha512-FcEfKZ4vm28yCdBsvC69EkrEhcfex+IYlRctNJXsRG9+WC3WxgBNORnECIgqUtj7o/h1d8o7xB/dFUiLi4bqtw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.2.0 + jest-get-type: 29.2.0 + pretty-format: 29.2.0 + dev: true + + /jest-message-util/29.0.3: + resolution: {integrity: sha512-7T8JiUTtDfppojosORAflABfLsLKMLkBHSWkjNQrjIltGoDzNGn7wEPOSfjqYAGTYME65esQzMJxGDjuLBKdOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.18.6 + '@jest/types': 29.2.0 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.10 + micromatch: 4.0.5 + pretty-format: 29.2.0 + slash: 3.0.0 + stack-utils: 2.0.5 dev: true - /jest-message-util/29.1.2: - resolution: {integrity: sha512-9oJ2Os+Qh6IlxLpmvshVbGUiSkZVc2FK+uGOm6tghafnB2RyjKAxMZhtxThRMxfX1J1SOMhTn9oK3/MutRWQJQ==} + /jest-message-util/29.2.0: + resolution: {integrity: sha512-arBfk5yMFMTnMB22GyG601xGSGthA02vWSewPaxoFo0F9wBqDOyxccPbCcYu8uibw3kduSHXdCOd1PsLSgdomg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/code-frame': 7.18.6 - '@jest/types': 29.1.2 + '@jest/types': 29.2.0 '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.10 micromatch: 4.0.5 - pretty-format: 29.1.2 + pretty-format: 29.2.0 slash: 3.0.0 stack-utils: 2.0.5 dev: true - /jest-mock/29.1.2: - resolution: {integrity: sha512-PFDAdjjWbjPUtQPkQufvniXIS3N9Tv7tbibePEjIIprzjgo0qQlyUiVMrT4vL8FaSJo1QXifQUOuPH3HQC/aMA==} + /jest-mock/29.2.0: + resolution: {integrity: sha512-aiWGR0P8ivssIO17xkehLGFtCcef2ZwQFNPwEer1jQLHxPctDlIg3Hs6QMq1KpPz5dkCcgM7mwGif4a9IPznlg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.1.2 + '@jest/types': 29.2.0 '@types/node': 18.7.16 - jest-util: 29.1.2 + jest-util: 29.2.0 dev: true - /jest-pnp-resolver/1.2.2_jest-resolve@29.1.2: + /jest-pnp-resolver/1.2.2_jest-resolve@29.2.0: resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} engines: {node: '>=6'} peerDependencies: @@ -6479,100 +6589,100 @@ packages: jest-resolve: optional: true dependencies: - jest-resolve: 29.1.2 + jest-resolve: 29.2.0 dev: true - /jest-regex-util/29.0.0: - resolution: {integrity: sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug==} + /jest-regex-util/29.2.0: + resolution: {integrity: sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-resolve-dependencies/29.1.2: - resolution: {integrity: sha512-44yYi+yHqNmH3OoWZvPgmeeiwKxhKV/0CfrzaKLSkZG9gT973PX8i+m8j6pDrTYhhHoiKfF3YUFg/6AeuHw4HQ==} + /jest-resolve-dependencies/29.2.0: + resolution: {integrity: sha512-Cd0Z39sDntEnfR9PoUdFHUAGDvtKI0/7Wt73l3lt03A3yQ+A6Qi3XmBuqGjdFl2QbXaPa937oLhilG612P8HGQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-regex-util: 29.0.0 - jest-snapshot: 29.1.2 + jest-regex-util: 29.2.0 + jest-snapshot: 29.2.0 transitivePeerDependencies: - supports-color dev: true - /jest-resolve/29.1.2: - resolution: {integrity: sha512-7fcOr+k7UYSVRJYhSmJHIid3AnDBcLQX3VmT9OSbPWsWz1MfT7bcoerMhADKGvKCoMpOHUQaDHtQoNp/P9JMGg==} + /jest-resolve/29.2.0: + resolution: {integrity: sha512-f5c0ljNg2guDBCC7wi92vAhNuA0BtAG5vkY7Fob0c7sUMU1g87mTXqRmjrVFe2XvdwP5m5T/e5KJsCKu9hRvBA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 graceful-fs: 4.2.10 - jest-haste-map: 29.1.2 - jest-pnp-resolver: 1.2.2_jest-resolve@29.1.2 - jest-util: 29.1.2 - jest-validate: 29.1.2 + jest-haste-map: 29.2.0 + jest-pnp-resolver: 1.2.2_jest-resolve@29.2.0 + jest-util: 29.2.0 + jest-validate: 29.2.0 resolve: 1.22.1 resolve.exports: 1.1.0 slash: 3.0.0 dev: true - /jest-runner/29.1.2: - resolution: {integrity: sha512-yy3LEWw8KuBCmg7sCGDIqKwJlULBuNIQa2eFSVgVASWdXbMYZ9H/X0tnXt70XFoGf92W2sOQDOIFAA6f2BG04Q==} + /jest-runner/29.2.0: + resolution: {integrity: sha512-VPBrCwl9fM2mc5yk6yZhNrgXzRJMD5jfLmntkMLlrVq4hQPWbRK998iJlR+DOGCO04TC9PPYLntOJ001Vnf28g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.1.2 - '@jest/environment': 29.1.2 - '@jest/test-result': 29.1.2 - '@jest/transform': 29.1.2 - '@jest/types': 29.1.2 + '@jest/console': 29.2.0 + '@jest/environment': 29.2.0 + '@jest/test-result': 29.2.0 + '@jest/transform': 29.2.0 + '@jest/types': 29.2.0 '@types/node': 18.7.16 chalk: 4.1.2 emittery: 0.10.2 graceful-fs: 4.2.10 - jest-docblock: 29.0.0 - jest-environment-node: 29.1.2 - jest-haste-map: 29.1.2 - jest-leak-detector: 29.1.2 - jest-message-util: 29.1.2 - jest-resolve: 29.1.2 - jest-runtime: 29.1.2 - jest-util: 29.1.2 - jest-watcher: 29.1.2 - jest-worker: 29.1.2 + jest-docblock: 29.2.0 + jest-environment-node: 29.2.0 + jest-haste-map: 29.2.0 + jest-leak-detector: 29.2.0 + jest-message-util: 29.2.0 + jest-resolve: 29.2.0 + jest-runtime: 29.2.0 + jest-util: 29.2.0 + jest-watcher: 29.2.0 + jest-worker: 29.2.0 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color dev: true - /jest-runtime/29.1.2: - resolution: {integrity: sha512-jr8VJLIf+cYc+8hbrpt412n5jX3tiXmpPSYTGnwcvNemY+EOuLNiYnHJ3Kp25rkaAcTWOEI4ZdOIQcwYcXIAZw==} + /jest-runtime/29.2.0: + resolution: {integrity: sha512-+GDmzCrswQF+mvI0upTYMe/OPYnlRRNLLDHM9AFLp2y7zxWoDoYgb8DL3WwJ8d9m743AzrnvBV9JQHi/0ed7dg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.1.2 - '@jest/fake-timers': 29.1.2 - '@jest/globals': 29.1.2 - '@jest/source-map': 29.0.0 - '@jest/test-result': 29.1.2 - '@jest/transform': 29.1.2 - '@jest/types': 29.1.2 + '@jest/environment': 29.2.0 + '@jest/fake-timers': 29.2.0 + '@jest/globals': 29.2.0 + '@jest/source-map': 29.2.0 + '@jest/test-result': 29.2.0 + '@jest/transform': 29.2.0 + '@jest/types': 29.2.0 '@types/node': 18.7.16 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 glob: 7.2.3 graceful-fs: 4.2.10 - jest-haste-map: 29.1.2 - jest-message-util: 29.1.2 - jest-mock: 29.1.2 - jest-regex-util: 29.0.0 - jest-resolve: 29.1.2 - jest-snapshot: 29.1.2 - jest-util: 29.1.2 + jest-haste-map: 29.2.0 + jest-message-util: 29.2.0 + jest-mock: 29.2.0 + jest-regex-util: 29.2.0 + jest-resolve: 29.2.0 + jest-snapshot: 29.2.0 + jest-util: 29.2.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color dev: true - /jest-snapshot/29.1.2: - resolution: {integrity: sha512-rYFomGpVMdBlfwTYxkUp3sjD6usptvZcONFYNqVlaz4EpHPnDvlWjvmOQ9OCSNKqYZqLM2aS3wq01tWujLg7gg==} + /jest-snapshot/29.2.0: + resolution: {integrity: sha512-YCKrOR0PLRXROmww73fHO9oeY4tL+LPQXWR3yml1+hKbQDR8j1VUrVzB65hKSJJgxBOr1vWx+hmz2by8JjAU5w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.18.6 @@ -6581,33 +6691,33 @@ packages: '@babel/plugin-syntax-typescript': 7.18.6_@babel+core@7.18.6 '@babel/traverse': 7.18.6 '@babel/types': 7.18.7 - '@jest/expect-utils': 29.1.2 - '@jest/transform': 29.1.2 - '@jest/types': 29.1.2 + '@jest/expect-utils': 29.2.0 + '@jest/transform': 29.2.0 + '@jest/types': 29.2.0 '@types/babel__traverse': 7.17.1 '@types/prettier': 2.6.3 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.18.6 chalk: 4.1.2 - expect: 29.1.2 + expect: 29.2.0 graceful-fs: 4.2.10 - jest-diff: 29.1.2 - jest-get-type: 29.0.0 - jest-haste-map: 29.1.2 - jest-matcher-utils: 29.1.2 - jest-message-util: 29.1.2 - jest-util: 29.1.2 + jest-diff: 29.2.0 + jest-get-type: 29.2.0 + jest-haste-map: 29.2.0 + jest-matcher-utils: 29.2.0 + jest-message-util: 29.2.0 + jest-util: 29.2.0 natural-compare: 1.4.0 - pretty-format: 29.1.2 + pretty-format: 29.2.0 semver: 7.3.8 transitivePeerDependencies: - supports-color dev: true - /jest-util/29.1.0: - resolution: {integrity: sha512-5haD8egMAEAq/e8ritN2Gr1WjLYtXi4udAIZB22GnKlv/2MHkbCjcyjgDBmyezAMMeQKGfoaaDsWCmVlnHZ1WQ==} + /jest-util/29.2.0: + resolution: {integrity: sha512-8M1dx12ujkBbnhwytrezWY0Ut79hbflwodE+qZKjxSRz5qt4xDp6dQQJaOCFvCmE0QJqp9KyEK33lpPNjnhevw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.1.2 + '@jest/types': 29.2.0 '@types/node': 18.7.16 chalk: 4.1.2 ci-info: 3.3.2 @@ -6615,41 +6725,29 @@ packages: picomatch: 2.3.1 dev: true - /jest-util/29.1.2: - resolution: {integrity: sha512-vPCk9F353i0Ymx3WQq3+a4lZ07NXu9Ca8wya6o4Fe4/aO1e1awMMprZ3woPFpKwghEOW+UXgd15vVotuNN9ONQ==} + /jest-validate/29.2.0: + resolution: {integrity: sha512-4Vl51bPNeFeDok9aJiOnrC6tqJbOp4iMCYlewoC2ZzYJZ5+6pfr3KObAdx5wP8auHcg2MRaguiqj5OdScZa72g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.1.2 - '@types/node': 18.7.16 - chalk: 4.1.2 - ci-info: 3.3.2 - graceful-fs: 4.2.10 - picomatch: 2.3.1 - dev: true - - /jest-validate/29.1.2: - resolution: {integrity: sha512-k71pOslNlV8fVyI+mEySy2pq9KdXdgZtm7NHrBX8LghJayc3wWZH0Yr0mtYNGaCU4F1OLPXRkwZR0dBm/ClshA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.1.2 + '@jest/types': 29.2.0 camelcase: 6.3.0 chalk: 4.1.2 - jest-get-type: 29.0.0 + jest-get-type: 29.2.0 leven: 3.1.0 - pretty-format: 29.1.2 + pretty-format: 29.2.0 dev: true - /jest-watcher/29.1.2: - resolution: {integrity: sha512-6JUIUKVdAvcxC6bM8/dMgqY2N4lbT+jZVsxh0hCJRbwkIEnbr/aPjMQ28fNDI5lB51Klh00MWZZeVf27KBUj5w==} + /jest-watcher/29.2.0: + resolution: {integrity: sha512-bRh0JdUeN+cl9XfK7tMnXLm4Mv70hG2SZlqbkFe5CTs7oeCkbwlGBk/mEfEJ63mrxZ8LPbnfaMpfSmkhEQBEGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.1.2 - '@jest/types': 29.1.2 + '@jest/test-result': 29.2.0 + '@jest/types': 29.2.0 '@types/node': 18.7.16 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.10.2 - jest-util: 29.1.2 + jest-util: 29.2.0 string-length: 4.0.2 dev: true @@ -6662,18 +6760,18 @@ packages: supports-color: 8.1.1 dev: true - /jest-worker/29.1.2: - resolution: {integrity: sha512-AdTZJxKjTSPHbXT/AIOjQVmoFx0LHFcVabWu0sxI7PAy7rFf8c0upyvgBKgguVXdM4vY74JdwkyD4hSmpTW8jA==} + /jest-worker/29.2.0: + resolution: {integrity: sha512-mluOlMbRX1H59vGVzPcVg2ALfCausbBpxC8a2KWOzInhYHZibbHH8CB0C1JkmkpfurrkOYgF7FPmypuom1OM9A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 18.7.16 - jest-util: 29.1.2 + jest-util: 29.2.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest/29.1.2_@types+node@18.7.16: - resolution: {integrity: sha512-5wEIPpCezgORnqf+rCaYD1SK+mNN7NsstWzIsuvsnrhR/hSxXWd82oI7DkrbJ+XTD28/eG8SmxdGvukrGGK6Tw==} + /jest/29.2.0_@types+node@18.7.16: + resolution: {integrity: sha512-6krPemKUXCEu5Fh3j6ZVoLMjpTQVm0OCU+7f3K/9gllX8wNIE6NSCQ6s0q2RDoiKLRaQlVRHyscjSPRPqCI0Fg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -6682,10 +6780,10 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.1.2 - '@jest/types': 29.1.2 + '@jest/core': 29.2.0 + '@jest/types': 29.2.0 import-local: 3.1.0 - jest-cli: 29.1.2_@types+node@18.7.16 + jest-cli: 29.2.0_@types+node@18.7.16 transitivePeerDependencies: - '@types/node' - supports-color @@ -6697,12 +6795,6 @@ packages: engines: {node: '>=10'} dev: true - /jquery-ui/1.13.2: - resolution: {integrity: sha512-wBZPnqWs5GaYJmo1Jj0k/mrSkzdQzKDwhXNtHKcBdAcKVxMM3KNYFq+iJ2i1rwiG53Z8M4mTn3Qxrm17uH1D4Q==} - dependencies: - jquery: 3.6.1 - dev: false - /jquery/3.6.1: resolution: {integrity: sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==} dev: false @@ -6757,7 +6849,7 @@ packages: cssom: 0.5.0 cssstyle: 2.3.0 data-urls: 3.0.2 - decimal.js: 10.4.1 + decimal.js: 10.4.2 domexception: 4.0.0 escodegen: 2.0.0 form-data: 4.0.0 @@ -8162,7 +8254,7 @@ packages: /parse5/7.0.0: resolution: {integrity: sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==} dependencies: - entities: 4.4.0 + entities: 4.3.1 dev: true /parse5/7.1.1: @@ -8212,7 +8304,7 @@ packages: dev: true /path-to-regexp/0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + resolution: {integrity: sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=} dev: true /path-to-regexp/2.2.1: @@ -8447,8 +8539,17 @@ packages: renderkid: 3.0.0 dev: true - /pretty-format/29.1.2: - resolution: {integrity: sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg==} + /pretty-format/29.0.3: + resolution: {integrity: sha512-cHudsvQr1K5vNVLbvYF/nv3Qy/F/BcEKxGuIeMiVMRHxPOO1RxXooP8g/ZrwAp7Dx+KdMZoOc7NxLHhMrP2f9Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.0.0 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + + /pretty-format/29.2.0: + resolution: {integrity: sha512-QCSUFdwOi924g24czhOH5eTkXxUCqlLGZBRCySlwDYHIXRJkdGyjJc9nZaqhlFBZws8dq5Dvk0lCilsmlfsPxw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.0.0 @@ -8524,7 +8625,7 @@ packages: dev: true /proxy-from-env/1.0.0: - resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + resolution: {integrity: sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=} dev: true /psl/1.9.0: @@ -9221,11 +9322,11 @@ packages: is-fullwidth-code-point: 3.0.0 dev: true - /slickgrid/2.4.45: - resolution: {integrity: sha512-WvygGTaLU9LnMWZSxqW1agwq8IcDGeCZ289vP1E07eh3ffyLm5TGQsYHXvTwWCHtriZBw+L9RFK/h7TsIMcoLQ==} + /slickgrid/3.0.0: + resolution: {integrity: sha512-6uhXqGitOYy2pheO7E77tqpf/DZ9stXjkyltcsbxg2GYZp+daDSh89dmlkvYlStO3gA8EmcG0WmxrAXDBFMSvA==} dependencies: jquery: 3.6.1 - jquery-ui: 1.13.2 + sortablejs: 1.15.0 dev: false /smart-buffer/4.2.0: @@ -9274,6 +9375,10 @@ packages: is-plain-obj: 2.1.0 dev: true + /sortablejs/1.15.0: + resolution: {integrity: sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==} + dev: false + /source-list-map/2.0.1: resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} dev: true @@ -9556,14 +9661,6 @@ packages: has-flag: 4.0.0 dev: true - /supports-hyperlinks/2.2.0: - resolution: {integrity: sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - supports-color: 7.2.0 - dev: true - /supports-preserve-symlinks-flag/1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -9606,14 +9703,6 @@ packages: engines: {node: '>=4'} dev: true - /terminal-link/2.1.1: - resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} - engines: {node: '>=8'} - dependencies: - ansi-escapes: 4.3.2 - supports-hyperlinks: 2.2.0 - dev: true - /terser-webpack-plugin/5.3.3_webpack@5.74.0: resolution: {integrity: sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==} engines: {node: '>= 10.13.0'} @@ -9630,11 +9719,11 @@ packages: uglify-js: optional: true dependencies: - '@jridgewell/trace-mapping': 0.3.15 + '@jridgewell/trace-mapping': 0.3.14 jest-worker: 27.5.1 schema-utils: 3.1.1 serialize-javascript: 6.0.0 - terser: 5.15.0 + terser: 5.14.1 webpack: 5.74.0 dev: true @@ -9649,8 +9738,8 @@ packages: source-map-support: 0.5.21 dev: true - /terser/5.15.0: - resolution: {integrity: sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==} + /terser/5.15.1: + resolution: {integrity: sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==} engines: {node: '>=10'} hasBin: true dependencies: @@ -9792,7 +9881,7 @@ packages: engines: {node: '>=8'} dev: true - /ts-jest/29.0.3_tgejc6cnv2pbkgcnmxq3dkgmme: + /ts-jest/29.0.3_tffzzkezgwx2pkcwct6fbmkuui: resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -9813,11 +9902,11 @@ packages: esbuild: optional: true dependencies: - '@jest/types': 29.1.2 + '@jest/types': 29.2.0 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.1.2_@types+node@18.7.16 - jest-util: 29.1.0 + jest: 29.2.0_@types+node@18.7.16 + jest-util: 29.2.0 json5: 2.2.1 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -9994,7 +10083,7 @@ packages: dev: true /unpipe/1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + resolution: {integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=} engines: {node: '>= 0.8'} dev: true @@ -10008,24 +10097,24 @@ packages: engines: {node: '>=4'} dev: true - /update-browserslist-db/1.0.9_browserslist@4.21.1: - resolution: {integrity: sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==} + /update-browserslist-db/1.0.10_browserslist@4.21.4: + resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.21.1 + browserslist: 4.21.4 escalade: 3.1.1 picocolors: 1.0.0 dev: true - /update-browserslist-db/1.0.9_browserslist@4.21.4: - resolution: {integrity: sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==} + /update-browserslist-db/1.0.5_browserslist@4.21.1: + resolution: {integrity: sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.21.4 + browserslist: 4.21.1 escalade: 3.1.1 picocolors: 1.0.0 dev: true @@ -10081,7 +10170,7 @@ packages: dev: true /utils-merge/1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + resolution: {integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=} engines: {node: '>= 0.4.0'} dev: true @@ -10124,7 +10213,7 @@ packages: dev: true /verror/1.10.0: - resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=} engines: {'0': node >=0.6.0} dependencies: assert-plus: 1.0.0 diff --git a/test/cypress/e2e/example03.cy.js b/test/cypress/e2e/example03.cy.js index c726aa9f5..0893d277c 100644 --- a/test/cypress/e2e/example03.cy.js +++ b/test/cypress/e2e/example03.cy.js @@ -81,15 +81,13 @@ describe('Example 03 - Draggable Grouping', { retries: 1 }, () => { cy.get('.slick-dropped-grouping:nth(1) div').contains('Effort-Driven'); }); - it('should be able to drag and swap grouped column titles inside the pre-header', () => { + it('should be able to drag and swap pre-header grouped column titles (Effort-Driven, Duration)', () => { cy.get('.slick-dropped-grouping:nth(0) div') .contains('Duration') - .trigger('mousedown', 'center', { which: 1 }); + .drag('.slick-dropped-grouping:nth(1) div'); - cy.get('.slick-dropped-grouping:nth(1) div') - .contains('Effort-Driven') - .trigger('mousemove', 'bottomRight') - .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + cy.get('.slick-dropped-grouping:nth(0) div').contains('Effort-Driven'); + cy.get('.slick-dropped-grouping:nth(1) div').contains('Duration'); }); it('should expect the grouping to be swapped as well in the grid', () => { @@ -193,7 +191,7 @@ describe('Example 03 - Draggable Grouping', { retries: 1 }, () => { .should('be.hidden'); cy.get('.grid3') - .find('.slick-draggable-dropbox-toggle-placeholder') + .find('.slick-draggable-dropzone-placeholder') .should('be.visible') .should('have.text', 'Drop a column header here to group by the column'); }); diff --git a/test/cypress/e2e/example04.cy.js b/test/cypress/e2e/example04.cy.js index e84f485d3..c8f361df8 100644 --- a/test/cypress/e2e/example04.cy.js +++ b/test/cypress/e2e/example04.cy.js @@ -261,4 +261,46 @@ describe('Example 04 - Frozen Grid', { retries: 1 }, () => { cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(1)`).should('contain', 'Task 4'); }); + + it('should filter autocomplete by typing Vancouver in the "City of Origin" and expect only filtered rows to show up', () => { + cy.get('.search-filter.filter-cityOfOrigin') + .type('Vancouver') + + cy.get('.slick-autocomplete').should('be.visible'); + cy.get('.slick-autocomplete div').should('have.length', 2); + cy.get('.slick-autocomplete').find('div:nth(0)').click(); + + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(1)`).should('contain', 'Task 1'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(1)`).should('contain', 'Task 5'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('contain', 'Task 7'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(1)`).should('contain', 'Task 9'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 4}px"] > .slick-cell:nth(1)`).should('contain', 'Task 11'); + }); + + it('should Clear all Filters', () => { + cy.get('.grid4') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + }); + + it('should edit first row (Task 1) and change its city by choosing it inside the autocomplete editor list', () => { + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(7)`).click(); + cy.get('input.autocomplete.editor-cityOfOrigin') + .type('Sydney') + + cy.get('.slick-autocomplete').should('be.visible'); + cy.get('.slick-autocomplete div').should('have.length', 3); + cy.get('.slick-autocomplete').find('div:nth(1)').click(); + + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(7)`).should('contain', 'Sydney, NS, Australia'); + }); }); diff --git a/test/cypress/e2e/example07.cy.js b/test/cypress/e2e/example07.cy.js index 77936dd7b..cc16b8467 100644 --- a/test/cypress/e2e/example07.cy.js +++ b/test/cypress/e2e/example07.cy.js @@ -303,14 +303,10 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries cy.get('.slick-header-columns') .children('.slick-header-column:nth(4)') - .should('contain', 'Duration') - .trigger('mousedown', 'center', { which: 1 }); + .contains('Duration') + .drag('.slick-header-column:nth(7)'); - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(7)') - .should('contain', 'Finish') - .trigger('mousemove', 'bottomRight') - .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + cy.get('.slick-header-column:nth(7)').contains('Duration'); cy.get('.grid7') .find('.slick-header-columns') @@ -726,14 +722,10 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', { retries cy.get('.slick-header-columns') .children('.slick-header-column:nth(5)') - .should('contain', 'Start') - .trigger('mousedown', 'bottom', { which: 1 }); + .contains('Start') + .drag('.slick-header-column:nth(8)'); - cy.get('.slick-header-columns') - .children('.slick-header-column:nth(8)') - .should('contain', 'Completed') - .trigger('mousemove', 'bottomRight') - .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + cy.get('.slick-header-column:nth(8)').contains('Start'); cy.get('.grid7') .find('.slick-header-columns') diff --git a/test/cypress/e2e/example12.cy.js b/test/cypress/e2e/example12.cy.js index 827ed4753..70277cae8 100644 --- a/test/cypress/e2e/example12.cy.js +++ b/test/cypress/e2e/example12.cy.js @@ -244,8 +244,8 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get('.item-details-container.editor-completed .modified').should('have.length', 1); cy.get('.item-details-container.editor-product .autocomplete').type('granite'); - cy.get('.ui-menu.ui-autocomplete.autocomplete-custom-four-corners').should('be.visible'); - cy.get('.ui-menu.ui-autocomplete.autocomplete-custom-four-corners').find('li.ui-menu-item:nth(0)').click(); + cy.get('.slick-autocomplete.autocomplete-custom-four-corners').should('be.visible'); + cy.get('.slick-autocomplete.autocomplete-custom-four-corners').find('div:nth(0)').click(); cy.get('.item-details-container.editor-product .modified').should('have.length', 1); cy.get('.item-details-container.editor-duration .editor-text').type('22'); @@ -257,7 +257,7 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').type('c'); - cy.get('.ui-menu.ui-autocomplete:visible').find('li.ui-menu-item:nth(1)').click(); + cy.get('.slick-autocomplete:visible').find('div:nth(1)').click(); cy.get('.item-details-container.editor-origin .autocomplete').invoke('val').then(text => expect(text).to.eq('Antarctica')); cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); @@ -332,7 +332,7 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').type('bel'); - cy.get('.ui-menu.ui-autocomplete:visible').find('li.ui-menu-item:nth(1)').click(); + cy.get('.slick-autocomplete:visible').find('div:nth(1)').click(); cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').invoke('val').then(text => expect(text).to.eq('Belgium')); @@ -392,7 +392,7 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').type('bel'); - cy.get('.ui-menu.ui-autocomplete:visible').find('li.ui-menu-item:nth(1)').click(); + cy.get('.slick-autocomplete:visible').find('div:nth(1)').click(); cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').invoke('val').then(text => expect(text).to.eq('Belgium')); }); @@ -444,7 +444,7 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').type('ze'); - cy.get('.ui-menu.ui-autocomplete:visible').find('li.ui-menu-item:nth(1)').click(); + cy.get('.slick-autocomplete:visible').find('div:nth(1)').click(); cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').invoke('val').then(text => expect(text).to.eq('Belize')); diff --git a/test/cypress/e2e/example17.cy.js b/test/cypress/e2e/example17.cy.js index 03d64660b..c466adf2f 100644 --- a/test/cypress/e2e/example17.cy.js +++ b/test/cypress/e2e/example17.cy.js @@ -35,7 +35,7 @@ describe('Example 17 - Auto-Scroll with Range Selector', { retries: 1 }, () => { .dragStart(); cy.get('.grid17-1 .slick-range-decorator').should('be.exist').and('have.css', 'border-color').and('not.equal', 'none'); cy.get('@cell1') - .drag(0, 5) + .dragCell(0, 5) .dragEnd('.grid17-1'); cy.get('.grid17-1 .slick-range-decorator').should('not.be.exist'); cy.get('.grid17-1 .slick-cell.selected').should('have.length', 6); @@ -45,7 +45,7 @@ describe('Example 17 - Auto-Scroll with Range Selector', { retries: 1 }, () => { .dragStart(); cy.get('.grid17-2 .slick-range-decorator').should('be.exist').and('have.css', 'border-style').and('equal', 'none'); cy.get('@cell2') - .drag(5, 1) + .dragCell(5, 1) .dragEnd('.grid17-2'); cy.get('.grid17-2 .slick-range-decorator').should('not.be.exist'); cy.get('.grid17-2 .slick-row:nth-child(-n+6)') @@ -175,14 +175,14 @@ describe('Example 17 - Auto-Scroll with Range Selector', { retries: 1 }, () => { cy.get('[data-test="delay-cursor-input"]').type('{selectall}50'); // 5ms/px -> 50ms/px cy.get('[data-test="set-options-btn"]').click(); - // Ideally if we scrolling to same row, and set cursor to 17px, the new interval will be set to MIN interval (Math.max(30, 600 - 50 * 17) = 30ms), + // Ideally if we are scrolling to same row, and set cursor to 17px, the new interval will be set to MIN interval (Math.max(30, 600 - 50 * 17) = 30ms), // and the used time should be around 17 times faster than default. // Considering the threshold, 5 times faster than default is expected testInterval(SCROLLBAR_DIMENSION).then(newInterval => { // scrolling speed is quicker than before - expect(3.0 * newInterval.cell).to.be.lessThan(defaultInterval.cell); - expect(3.0 * newInterval.row).to.be.lessThan(defaultInterval.row); + expect(2.0 * newInterval.cell).to.be.lessThan(defaultInterval.cell); + expect(2.0 * newInterval.row).to.be.lessThan(defaultInterval.row); cy.get('[data-test="default-options-btn"]').click(); cy.get('[data-test="delay-cursor-input"]').should('have.value', '5'); diff --git a/test/cypress/e2e/example18.cy.js b/test/cypress/e2e/example18.cy.js index 934e31ad5..5fc933b22 100644 --- a/test/cypress/e2e/example18.cy.js +++ b/test/cypress/e2e/example18.cy.js @@ -47,13 +47,9 @@ describe('Example 18 - Real-Time Trading Platform', { retries: 1 }, () => { }); it('should Group by 1st column "Currency" and expect 2 groups with Totals when collapsed', () => { - cy.get('.slick-column-name') - .first() - .trigger('mousedown', { which: 1, force: true }) - - cy.get('.slick-draggable-dropbox-toggle-placeholder') - .trigger('mousemove', 'center') - .trigger('mouseup', 'center', { which: 1, force: true }); + cy.get('.slick-header-column:nth(0)') + .contains('Currency') + .drag('.slick-dropzone', { force: true }); cy.get('.slick-group-toggle-all') .click(); diff --git a/test/cypress/jsconfig.json b/test/cypress/jsconfig.json index 9e99cf86d..07289cbea 100644 --- a/test/cypress/jsconfig.json +++ b/test/cypress/jsconfig.json @@ -1,6 +1,7 @@ { "typeAcquisition": { "include": [ + "@4tw/cypress-drag-drop", "cypress" ] } diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index af7299a75..147b362b3 100644 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -23,7 +23,7 @@ // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) - +import '@4tw/cypress-drag-drop'; import { convertPosition } from './common'; // convert position like 'topLeft' to the object { x: 'left|right', y: 'top|bottom' } diff --git a/test/cypress/support/drag.js b/test/cypress/support/drag.js index 5d88655dd..b107728f4 100644 --- a/test/cypress/support/drag.js +++ b/test/cypress/support/drag.js @@ -4,11 +4,12 @@ Cypress.Commands.add("dragStart", { prevSubject: true }, (subject, { cellWidth = return cy.wrap(subject).click({ force: true }) .trigger('mousedown', { which: 1 }, { force: true }) .trigger('mousemove', cellWidth / 3, cellHeight / 3); -}) +}); -Cypress.Commands.add("drag", { prevSubject: true }, (subject, addRow, addCell, { cellWidth = 90, cellHeight = 35 } = {}) => { +// use a different command name than "drag" so that it doesn't conflict with the "@4tw/cypress-drag-drop" lib +Cypress.Commands.add("dragCell", { prevSubject: true }, (subject, addRow, addCell, { cellWidth = 90, cellHeight = 35 } = {}) => { return cy.wrap(subject).trigger('mousemove', cellWidth * (addCell + 0.5), cellHeight * (addRow + 0.5), { force: true }); -}) +}); Cypress.Commands.add("dragOutside", (viewport = 'topLeft', ms = 0, px = 0, { parentSelector = 'div[class^="slickgrid_"]', scrollbarDimension = 17 } = {}) => { const $parent = cy.$$(parentSelector); @@ -33,12 +34,12 @@ Cypress.Commands.add("dragOutside", (viewport = 'topLeft', ms = 0, px = 0, { par cy.wait(ms); } return; -}) +}); Cypress.Commands.add("dragEnd", { prevSubject: 'optional' }, (_subject, gridSelector = 'div[class^="slickgrid_"]') => { cy.get(gridSelector).trigger('mouseup', { force: true }); return; -}) +}); export function getScrollDistanceWhenDragOutsideGrid(selector, viewport, dragDirection, fromRow, fromCol, px = 140) { return cy.convertPosition(viewport).then(_viewportPosition => { diff --git a/test/cypress/support/index.js b/test/cypress/support/index.js index 78eb0c85f..cfd2529c0 100644 --- a/test/cypress/support/index.js +++ b/test/cypress/support/index.js @@ -14,7 +14,7 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands' +import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/test/httpClientStub.ts b/test/httpClientStub.ts index edad3185c..aac5ae5d3 100644 --- a/test/httpClientStub.ts +++ b/test/httpClientStub.ts @@ -39,7 +39,7 @@ export class HttpStub { } else { const data = JSON.stringify(this.object); const response = new Response(data, responseInit); - if (input === 'invalid-url') { + if (input.includes('invalid-url')) { Object.defineProperty(response, 'bodyUsed', { writable: true, configurable: true, value: true }); } return this.status >= 200 && this.status < 300 ? Promise.resolve(response) : Promise.reject(response); diff --git a/test/jest-pretest.ts b/test/jest-pretest.ts index bccd2990e..98e38b137 100644 --- a/test/jest-pretest.ts +++ b/test/jest-pretest.ts @@ -1,4 +1,5 @@ import 'jsdom-global/register'; +import Sortable from 'sortablejs'; import 'whatwg-fetch'; import * as jQuery from 'jquery'; @@ -7,9 +8,9 @@ import * as jQuery from 'jquery'; // (global as any).Storage = window.localStorage; (global as any).navigator = { userAgent: 'node.js' }; (global as any).Slick = (window as any).Slick = {}; +(global as any).Sortable = (window as any).Sortable = Sortable; -require('jquery-ui/dist/jquery-ui.js'); -require('slickgrid/lib/jquery.event.drag-2.3.0'); require('slickgrid/slick.core'); require('slickgrid/slick.dataview'); +require('slickgrid/slick.interactions'); require('slickgrid/slick.grid'); diff --git a/test/tsconfig.spec.json b/test/tsconfig.spec.json index 4390519e5..475d0d127 100644 --- a/test/tsconfig.spec.json +++ b/test/tsconfig.spec.json @@ -8,6 +8,7 @@ "es2018" ], "types": [ + "@4tw/cypress-drag-drop", "cypress", "jest", "jest-extended",