From fb6e950f429b4abd868fca86d9c304580a745b1c Mon Sep 17 00:00:00 2001 From: Ghislain B Date: Sat, 27 Apr 2024 02:20:57 -0400 Subject: [PATCH] feat(common)!: migrate from Flatpickr to Vanilla-Calendar (#1466) * feat!: migrate from Flatpickr to Vanilla-Calendar --- .vscode/settings.json | 3 +- docs/TOC.md | 1 + docs/column-functionalities/Editors.md | 12 +- .../editors/Date-Editor-(flatpickr).md | 4 +- .../editors/date-editor-(vanilla-calendar).md | 70 ++++ .../filters/Compound-Filters.md | 21 +- .../filters/Range-Filters.md | 29 +- .../filters/Styling-Filled-Filters.md | 78 ++--- .../Composite-Editor-Modal.md | 11 +- eslint.config.mjs | 2 - .../vite-demo-vanilla-bundle/package.json | 2 +- .../src/examples/example03.ts | 6 +- .../src/examples/example11.ts | 3 +- .../src/examples/example12.ts | 22 +- .../src/examples/example14.ts | 10 +- .../src/examples/example16.ts | 3 +- examples/vite-demo-vanilla-bundle/src/main.ts | 5 - .../src/material-styles.scss | 4 +- .../vite-demo-vanilla-bundle/vite.config.mts | 5 - package.json | 2 +- packages/binding/src/bindingEvent.service.ts | 2 +- packages/common/package.json | 4 +- .../commonEditorFilterUtils.ts | 31 +- .../src/core/__tests__/slickDataView.spec.ts | 1 - .../src/editors/__tests__/dateEditor.spec.ts | 254 ++++++++------ packages/common/src/editors/dateEditor.ts | 265 ++++++++------- packages/common/src/editors/editors.index.ts | 2 +- .../slickCellExcelCopyManager.spec.ts | 4 +- .../slickCellExternalCopyManager.spec.ts | 2 - .../__tests__/slickCellRangeDecorator.spec.ts | 2 - .../__tests__/slickCellRangeSelector.spec.ts | 1 - .../__tests__/slickCellSelectionModel.spec.ts | 1 - .../__tests__/slickGridMenu.spec.ts | 2 - .../__tests__/slickRowMoveManager.spec.ts | 1 - .../__tests__/slickRowSelectionModel.spec.ts | 1 - .../__tests__/compoundDateFilter.spec.ts | 309 ++++++++++------- .../filters/__tests__/dateRangeFilter.spec.ts | 247 +++++++------- packages/common/src/filters/dateFilter.ts | 312 +++++++++++------- .../collectionEditorFormatter.spec.ts | 2 - .../__tests__/collectionFormatter.spec.ts | 2 - .../interfaces/flatpickrOption.interface.ts | 178 ---------- .../src/interfaces/gridOption.interface.ts | 10 +- packages/common/src/interfaces/index.ts | 2 +- .../vanillaCalendarOption.interface.ts | 8 + .../__tests__/backend-utilities.spec.ts | 2 - .../__tests__/extension.service.spec.ts | 1 - .../services/__tests__/grid.service.spec.ts | 3 - .../services/__tests__/shared.service.spec.ts | 2 - .../src/services/__tests__/utilities.spec.ts | 112 ------- packages/common/src/services/domUtilities.ts | 2 +- packages/common/src/services/utilities.ts | 117 +------ packages/common/src/styles/_variables.scss | 12 +- .../common/src/styles/flatpickr-dark.scss | 107 ------ packages/common/src/styles/flatpickr.min.scss | 13 - packages/common/src/styles/slick-editors.scss | 32 +- packages/common/src/styles/slick-filters.scss | 2 +- packages/common/src/styles/slick-grid.scss | 4 - packages/common/src/styles/slick-plugins.scss | 41 +-- .../src/styles/slickgrid-theme-bootstrap.scss | 2 +- .../styles/slickgrid-theme-material.bare.scss | 1 - .../styles/slickgrid-theme-material.lite.scss | 3 +- .../src/styles/slickgrid-theme-material.scss | 6 +- .../slickgrid-theme-salesforce.bare.scss | 1 - .../slickgrid-theme-salesforce.lite.scss | 3 +- .../styles/slickgrid-theme-salesforce.scss | 5 +- .../src/compositeEditor.factory.spec.ts | 2 +- packages/vanilla-bundle/package.json | 2 +- .../components/slick-vanilla-grid-bundle.ts | 1 - pnpm-lock.yaml | 30 +- test/cypress/e2e/example07.cy.ts | 4 +- test/cypress/e2e/example10.cy.ts | 12 +- test/cypress/e2e/example11.cy.ts | 22 +- test/cypress/e2e/example12.cy.ts | 37 ++- test/cypress/e2e/example16.cy.ts | 4 +- test/jest-global-mocks.ts | 18 +- 75 files changed, 1142 insertions(+), 1397 deletions(-) create mode 100644 docs/column-functionalities/editors/date-editor-(vanilla-calendar).md delete mode 100644 packages/common/src/interfaces/flatpickrOption.interface.ts create mode 100644 packages/common/src/interfaces/vanillaCalendarOption.interface.ts delete mode 100644 packages/common/src/styles/flatpickr-dark.scss delete mode 100644 packages/common/src/styles/flatpickr.min.scss diff --git a/.vscode/settings.json b/.vscode/settings.json index 2568ea7b3..bd6e1b612 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "javascript", "typescript" ], - "typescript.format.semicolons": "insert" + "typescript.format.semicolons": "insert", + "typescript.tsdk": "node_modules\\typescript\\lib" } \ No newline at end of file diff --git a/docs/TOC.md b/docs/TOC.md index 44b1695b1..745e78d52 100644 --- a/docs/TOC.md +++ b/docs/TOC.md @@ -18,6 +18,7 @@ * [Editors](column-functionalities/Editors.md) * [new Autocomplete (Kraaden-lib)](column-functionalities/editors/Autocomplete-Editor-(Kraaden-lib).md) * [Date Picker (flatpickr)](column-functionalities/editors/Date-Editor-(flatpickr).md) + * [Date Picker (vanilla-calendar)](column-functionalities/editors/date-editor-(vanilla-calendar).md) * [LongText (textarea)](column-functionalities/editors/LongText-Editor-(textarea).md) * [Select Dropdown Editor (single/multiple)](column-functionalities/editors/Select-Dropdown-Editor-(single,multiple).md) * [Filters](column-functionalities/filters/README.md) diff --git a/docs/column-functionalities/Editors.md b/docs/column-functionalities/Editors.md index 935b4cf96..eec15e57f 100644 --- a/docs/column-functionalities/Editors.md +++ b/docs/column-functionalities/Editors.md @@ -108,7 +108,7 @@ this.columnDefinitions = [ ]; ``` -So to make it more clear, the `saveOutputType` is the format that will be sent to the `onCellChange` event, then the `outputType` is how the date will show up in the date picker (Flatpickr) and finally the `type` is basically the input format (coming from your dataset). Note however that each property are cascading, if 1 property is missing it will go to the next one until 1 is found... for example, on the `onCellChange` if you aren't defining `saveOutputType`, it will try to use `outputType`, if again none is provided it will try to use `type` and finally if none is provided it will use `FieldType.dateIso` as the default. +So to make it more clear, the `saveOutputType` is the format that will be sent to the `onCellChange` event, then the `outputType` is how the date will show up in the date picker (Vanilla-Calendar) and finally the `type` is basically the input format (coming from your dataset). Note however that each property are cascading, if 1 property is missing it will go to the next one until 1 is found... for example, on the `onCellChange` if you aren't defining `saveOutputType`, it will try to use `outputType`, if again none is provided it will try to use `type` and finally if none is provided it will use `FieldType.dateIso` as the default. ## Perform an action After Inline Edit #### Recommended way @@ -201,9 +201,9 @@ Some of the Editors could receive extra options, which is mostly the case for Ed ```ts this.columnDefinitions = [{ id: 'start', name: 'Start Date', field: 'start', - editor: { + editor: { model: Editors.date, - editorOptions: { minDate: 'today' } + editorOptions: { range: { min: 'today' } } as VanillaCalendarOption } }]; ``` @@ -213,10 +213,10 @@ You could also define certain options as a global level (for the entire grid or ```ts this.gridOptions = { - defaultEditorOptions: { + defaultEditorOptions: { autocompleter: { debounceWaitMs: 150 }, // typed as AutocompleterOption - date: { minDate: 'today' }, - longText: { cols: 50, rows: 5 } + date: { range: { min: 'today' } }, + longText: { cols: 50, rows: 5 } } } ``` diff --git a/docs/column-functionalities/editors/Date-Editor-(flatpickr).md b/docs/column-functionalities/editors/Date-Editor-(flatpickr).md index e3b430a1a..743c6e35d 100644 --- a/docs/column-functionalities/editors/Date-Editor-(flatpickr).md +++ b/docs/column-functionalities/editors/Date-Editor-(flatpickr).md @@ -41,8 +41,8 @@ You could also define certain options as a global level (for the entire grid or ```ts this.gridOptions = { - defaultEditorOptions: { - date: { minDate: 'today' }, // typed as FlatpickrOption + defaultEditorOptions: { + date: { range: { min: 'today' } }, // typed as FlatpickrOption } } ``` diff --git a/docs/column-functionalities/editors/date-editor-(vanilla-calendar).md b/docs/column-functionalities/editors/date-editor-(vanilla-calendar).md new file mode 100644 index 000000000..df1ecbec5 --- /dev/null +++ b/docs/column-functionalities/editors/date-editor-(vanilla-calendar).md @@ -0,0 +1,70 @@ +##### index +- [Editor Options](#editor-options) +- [Custom Validator](#custom-validator) +- See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...) + +### Information +The Date Editor is provided through an external library named [Vanilla-Calendar-Picker](https://github.com/ghiscoding/vanilla-calendar-picker) (a fork of [Vanilla-Calendar-Pro](https://vanilla-calendar.pro)) and all options from that library can be added to your `editorOptions` (see below), so in order to add things like minimum date, disabling dates, ... just review all the [Vanilla-Calendar-Pro](https://vanilla-calendar.pro/docs/reference/additionally/settings) and then add them into `editorOptions`. Also just so you know, `editorOptions` is use by all other editors as well to expose external library like Autocompleter, Multiple-Select, etc... + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example12) | [Demo Component](https://github.com/ghiscoding/slickgrid-universal/blob/master/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts) + +### Editor Options +You can use any of the Vanilla-Calendar [settings](https://vanilla-calendar.pro/docs/reference/additionally/settings) by adding them to `editorOptions` as shown below. + +> **Note** for easier implementation, you should import `VanillaCalendarOption` from Slickgrid-Universal common package. + +```ts +import { type VanillaCalendarOption } from '@slickgrid-universal/common'; + +prepareGrid() { + this.columnDefinitions = [ + { + id: 'title', name: 'Title', field: 'title', + editor: { + model: Editors.date, + editorOptions: { + range: { + max: 'today', + disabled: ['2022-08-15', '2022-08-20'], + } + } as VanillaCalendarOption, + }, + }, + ]; +} +``` + +#### Grid Option `defaultEditorOptions +You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultEditorOptions` Grid Option. Note that they are set via the editor type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `editorOptions` (also note that each key is already typed with the correct editor option interface), for example + +```ts +this.gridOptions = { + defaultEditorOptions: { + date: { range: { min: 'today' } }, // typed as VanillaCalendarOption + } +} +``` + +### Custom Validator +You can add a Custom Validator from an external function or inline (inline is shown below and comes from [Example 12](https://ghiscoding.github.io/slickgrid-universal/#/example12)) +```ts +initializeGrid() { + this.columnDefinitions = [ + { + id: 'title', name: 'Title', field: 'title', + editor: { + model: Editors.date, + required: true, + validator: (value, args) => { + const dataContext = args && args.item; + if (dataContext && (dataContext.completed && !value)) { + return { valid: false, msg: 'You must provide a "Finish" date when "Completed" is checked.' }; + } + return { valid: true, msg: '' }; + } + }, + }, + ]; +} +``` \ No newline at end of file diff --git a/docs/column-functionalities/filters/Compound-Filters.md b/docs/column-functionalities/filters/Compound-Filters.md index 8c5caebd8..adfa04e1d 100644 --- a/docs/column-functionalities/filters/Compound-Filters.md +++ b/docs/column-functionalities/filters/Compound-Filters.md @@ -3,7 +3,6 @@ - [SASS Styling](#sass-styling) - [Compound Input Filter](#how-to-use-compoundinput-filter) - [Compound Date Filter](#how-to-use-compounddate-filter) - - [Filter Options (`FlatpickrOption` interface)](#filter-options-flatpickroption-interface) - [Compound Operator List (custom list)](#compound-operator-list-custom-list) - [Compound Operator Alternate Texts](#compound-operator-alternate-texts) - [Filter Complex Object](../Input-Filter.md#how-to-filter-complex-objects) @@ -11,7 +10,7 @@ - [How to avoid filtering when only Operator dropdown is changed?](#how-to-avoid-filtering-when-only-operator-dropdown-is-changed) ### Description -Compound filters are a combination of 2 elements (Operator Select + Input Filter) used as a filter on a column. This is very useful to make it obvious to the user that there are Operator available and even more useful with a date picker (`Flatpickr`). +Compound filters are a combination of 2 elements (Operator Select + Input Filter) used as a filter on a column. This is very useful to make it obvious to the user that there are Operator available and even more useful with a date picker (`Vanilla-Calendar`). ### Demo [Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example02) / [Demo Component](https://github.com/ghiscoding/slickgrid-universal/blob/master/examples/webpack-demo-vanilla-bundle/src/examples/example02.ts) @@ -21,11 +20,11 @@ There are multiple types of compound filters available 1. `Filters.compoundInputText` adds an Operator combine to an Input of type `text` (alias to `Filters.compoundInput`). 2. `Filters.compoundInputNumber` adds an Operator combine to an Input of type `number`. 3. `Filters.compoundInputPassword` adds an Operator combine to an Input of type `password. -4. `Filters.compoundDate` adds an Operator combine to a Date Picker (flatpickr). +4. `Filters.compoundDate` adds an Operator combine to a Date Picker (Vanilla-Calendar). 5. `Filters.compoundSlider` adds an Operator combine to a Slider Filter. ### SASS Styling -You can change the `$flatpickr-bgcolor` and any of the `$compound-filter-X` SASS [variables](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/styles/_variables.scss#L660) for styling. For more info on how to use SASS in your project, read the [Wiki - Styling](../../styling/styling.md) +You can change some of the SASS [variables](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/styles/_variables.scss#L648) for styling. For more info on how to use SASS in your project, read the [Wiki - Styling](../../styling/styling.md) ### How to use CompoundInput Filter Simply set the flag `filterable` to True and use the filter type `Filters.compoundInput`. Here is an example with a full column definition: @@ -125,15 +124,15 @@ this.gridOptions = { }; ``` -#### Filter Options (`FlatpickrOption` interface) -All the available options that can be provided as `filterOptions` to your column definitions can be found under this [FlatpickrOption interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/flatpickrOption.interface.ts) and you should cast your `filterOptions` to that interface to make sure that you use only valid options of the [Flatpickr](https://flatpickr.js.org/) library. +#### Filter Options (`VanillaCalendarOption` interface) +All the available options that can be provided as `filterOptions` to your column definitions can be found under this [VanillaCalendarOption interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/vanillaCalendarOption.interface.ts) and you should cast your `filterOptions` with the expected interface to make sure that you use only valid settings of the [Vanilla-Calendar](https://vanilla-calendar.pro/docs/reference/additionally/settings) library. ```ts filter: { model: Filters.compoundDate, filterOptions: { - minDate: 'today' - } as FlatpickrOption + range: { min: 'today' } + } as VanillaCalendarOption } ``` @@ -142,10 +141,10 @@ You could also define certain options as a global level (for the entire grid or ```ts this.gridOptions = { - defaultFilterOptions: { + defaultFilterOptions: { // Note: that `date`, `select` and `slider` are combining both compound & range filters together - date: { minDate: 'today' }, - select: { minHeight: 350 }, // typed as MultipleSelectOption + date: { range: { min: 'today' } }, // typed as VanillaCalendarOption + select: { minHeight: 350 }, // typed as MultipleSelectOption slider: { sliderStartValue: 10 } } } diff --git a/docs/column-functionalities/filters/Range-Filters.md b/docs/column-functionalities/filters/Range-Filters.md index 0c1a26d19..f0b3f37ad 100644 --- a/docs/column-functionalities/filters/Range-Filters.md +++ b/docs/column-functionalities/filters/Range-Filters.md @@ -4,11 +4,10 @@ - [Using a Slider Range](#using-a-slider-range-filter) - [Filter Options](#filter-options) - [Using a Date Range](#using-a-date-range-filter) - - [Filter Options (`FlatpickrOption` interface)](#filter-options-flatpickroption-interface) - [Update Filters Dynamically](Input-Filter.md#update-filters-dynamically) ### Introduction -Range filters allows you to search for a value between 2 min/max values, the 2 most common use case would be to filter between 2 numbers or dates, you can do that with the new Slider & Date Range Filters. The range can also be defined as inclusive (`>= 0 and <= 10`) or exclusive (`> 0 and < 10`), the default is exclusive but you can change that, see below for more info. +Range filters allows you to search for a value between 2 min/max values, the 2 most common use case would be to filter between 2 numbers or dates, you can do that with the Slider & Date Range Filters. The range can also be defined as inclusive (`>= 0 and <= 10`) or exclusive (`> 0 and < 10`), the default is exclusive but you can change that, see below for more info. ### Using an Inclusive Range (default is Exclusive) By default all the range filters are with exclusive range, which mean between value `x` and `y` but without including them. If you wish to include the `x` and `y` values, you can change that through the `operator` property. @@ -123,9 +122,9 @@ You could also define certain options as a global level (for the entire grid or ```ts this.gridOptions = { - defaultFilterOptions: { + defaultFilterOptions: { // Note: that `date`, `select` and `slider` are combining both compound & range filters together - date: { minDate: 'today' }, + date: { range: { min: 'today' } }, select: { minHeight: 350 }, // typed as MultipleSelectOption slider: { sliderStartValue: 10 } } @@ -133,10 +132,10 @@ this.gridOptions = { ``` ### Using a Date Range Filter -The date range filter allows you to search data between 2 dates (it uses [Flatpickr Range](https://flatpickr.js.org/examples/#range-calendar) feature). +The date range filter allows you to search data between 2 dates (it uses [Vanilla-Calendar Range](https://vanilla-calendar.pro/) feature). ##### Component -import { Filters, FlatpickrOption, Formatters, GridOption, OperatorType } from '@slickgrid-universal/common'; +import { Filters, Formatters, GridOption, OperatorType, VanillaCalendarOption } from '@slickgrid-universal/common'; ```typescript export class GridBasicComponent { @@ -148,14 +147,16 @@ export class GridBasicComponent { // your columns definition this.columnDefinitions = [ { - id: 'finish', name: 'Finish', field: 'finish', headerKey: 'FINISH', formatter: Formatters.dateIso, sortable: true, minWidth: 75, width: 120, exportWithFormatter: true, + id: 'finish', name: 'Finish', field: 'finish', headerKey: 'FINISH', + minWidth: 75, width: 120, exportWithFormatter: true, + formatter: Formatters.dateIso, sortable: true, type: FieldType.date, filterable: true, filter: { model: Filters.dateRange, - // override any of the Flatpickr options through "filterOptions" - editorOptions: { minDate: 'today' } as FlatpickrOption + // override any of the Vanilla-Calendar options through "filterOptions" + editorOptions: { range: { min: 'today' } } as VanillaCalendarOption } }, ]; @@ -167,14 +168,14 @@ export class GridBasicComponent { } ``` -##### Filter Options (`FlatpickrOption` interface) -All the available options that can be provided as `filterOptions` to your column definitions can be found under this [FlatpickrOption interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/flatpickrOption.interface.ts) and you should cast your `filterOptions` to that interface to make sure that you use only valid options of the [Flatpickr](https://flatpickr.js.org/) library. +#### Filter Options (`VanillaCalendarOption` interface) +All the available options that can be provided as `filterOptions` to your column definitions can be found under this [VanillaCalendarOption interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/vanillaCalendarOption.interface.ts) and you should cast your `filterOptions` with the expected interface to make sure that you use only valid settings of the [Vanilla-Calendar](https://vanilla-calendar.pro/docs/reference/additionally/settings) library. ```ts filter: { - model: Filters.dateRange, + model: Filters.compoundDate, filterOptions: { - minDate: 'today' - } as FlatpickrOption + range: { min: 'today' } + } as VanillaCalendarOption } ``` diff --git a/docs/column-functionalities/filters/Styling-Filled-Filters.md b/docs/column-functionalities/filters/Styling-Filled-Filters.md index 8ac7c970b..895d103f0 100644 --- a/docs/column-functionalities/filters/Styling-Filled-Filters.md +++ b/docs/column-functionalities/filters/Styling-Filled-Filters.md @@ -1,55 +1,45 @@ -You can style any (or all) of the Filter(s) when they have value, the lib will automatically add a `filled` CSS class which you can style as your wish. There is no style by default, if you wish to add styling, you will be required to add your own. +You can style any (or all) of the Filter(s) when they have value, the lib will automatically add a `filled` CSS class which you can style as your wish. There is no style by default, if you wish to add styling, you will be required to add your own. -## Styled Example +## Styled Example ![grid_filled_styling](https://user-images.githubusercontent.com/643976/51334569-14306d00-1a4e-11e9-816c-439796eb8a59.png) ## Code example -For example, the print screen shown earlier was styled using this piece of SASS (`.scss`) code. Also note that the demo adds a Font-Awesome icon which can be used with `font-family: "FontAwesome"` and the relevent unicode character, for example the filter icon is `content: " \f0b0"`. You can basically add a lot of different styling to your populated filters. +For example, the print screen shown earlier was styled using this piece of SASS (`.scss`) code. You can customize the styling to your liking. ```scss -$filter-filled-bg-color: darkorange; - -.search-filter.filled { - // color: rgb(189, 104, 1); - // font-weight: bold; - background-color: $filter-filled-bg-color; - .ms-choice { - // color: rgb(189, 104, 1); - // font-weight: bold; - background-color: $filter-filled-bg-color; - } - - - input, input.flatpickr-input { - // border: 1px solid darken(rgb(204, 204, 204), 15%) !important; - // color: rgb(189, 104, 1); - // font-weight: bold; - background-color: $filter-filled-bg-color !important; +$slick-filled-filter-color: $slick-form-control-focus-border-color; +$slick-filled-filter-border: $slick-form-control-border; +$slick-filled-filter-box-shadow: $slick-form-control-focus-border-color; +$slick-filled-filter-font-weight: 400; + +.slick-headerrow { + input.search-filter.filled, + .search-filter.filled input, + .search-filter.filled .date-picker input, + .search-filter.filled .input-group-addon.slider-value, + .search-filter.filled .input-group-addon.slider-range-value, + .search-filter.filled .input-group-addon select { + color: $slick-filled-filter-color; + font-weight: $slick-filled-filter-font-weight; + border: $slick-filled-filter-border; + box-shadow: $slick-filled-filter-box-shadow; + &.input-group-prepend { + border-right: 0; + } + &.input-group-append { + border-left: 0; + } } - /* - &.ms-parent, .flatpickr > input, .input-group > input { - border: 1px solid darken(rgb(204, 204, 204), 15%) !important; - } - */ - - div.flatpickr:after, button > div:after, & + span:after, .input-group > span:after { - font-family: "FontAwesome"; - font-size: 75%; - content: " \f0b0"; - position: absolute; - top: 2px; - right: 5px; - z-index: 1000; - color: #ca880f; - } - - .ms-choice > div:after { - top: -4px; - right: 16px; + .search-filter.filled .input-group-prepend select { + border-right: 0; } - & + span:after { - top: 6px; - right: 10px; + .search-filter.filled .ms-choice { + box-shadow: $slick-filled-filter-box-shadow; + border:$slick-filled-filter-border; + span { + font-weight: $slick-filled-filter-font-weight; + color: $slick-filled-filter-color; + } } } ``` diff --git a/docs/grid-functionalities/Composite-Editor-Modal.md b/docs/grid-functionalities/Composite-Editor-Modal.md index 19aa2f983..f6a7577fe 100644 --- a/docs/grid-functionalities/Composite-Editor-Modal.md +++ b/docs/grid-functionalities/Composite-Editor-Modal.md @@ -496,9 +496,9 @@ export class GridExample { // you can also change some editor options // not all Editors supports this functionality, so far only these Editors are supported: AutoComplete, Date, Single/Multiple Select if (columnDef.id === 'completed') { - this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select.js, show filter in dropdown - this.compositeEditorInstance.changeFormEditorOption('product', 'minLength', 3); // autocomplete, change minLength char to type - this.compositeEditorInstance.changeFormEditorOption('finish', 'minDate', 'today'); // flatpickr date picker, change minDate to today + 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', 'range', { min: 'today' }); // vanilla calendar date picker, change minDate to today } } } @@ -619,7 +619,7 @@ Disabling field(s) is done through the exact same way that you would do it in th ```ts handleOnBeforeEditCell(event) { const eventData = event.detail.eventData; - const args = event && event.detail && event.detail.args; + const args = event?.detail?.args; const { column, item, grid } = args; if (column && item) { @@ -649,13 +649,14 @@ checkItemIsEditable(dataContext: any, columnDef: Column, grid: SlickGrid) { return isEditable; } ``` + #### Disabling Form Inputs but only in Composite Editor What if you want to disable certain form inputs but only in the Composite Editor, or use different logic in the grid. For that we added an extra `target` (`target` will return either "grid" or "composite") in the returned `args`, so you could apply different logic based on the target being the grid or the composite editor. For example: ```ts handleOnBeforeEditCell(event) { const eventData = event.detail.eventData; - const args = event && event.detail && event.detail.args; + const args = event?.detail?.args; const { column, item, grid, target } = args; if (column && item) { diff --git a/eslint.config.mjs b/eslint.config.mjs index 06bd9f965..1bbdfe615 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,8 +41,6 @@ export default tseslint.config( languageOptions: { globals: { // ...globals, - flatpickr: true, - Slick: true, Sortable: true, }, parser: tsParser, diff --git a/examples/vite-demo-vanilla-bundle/package.json b/examples/vite-demo-vanilla-bundle/package.json index 507bbb290..cff29a19a 100644 --- a/examples/vite-demo-vanilla-bundle/package.json +++ b/examples/vite-demo-vanilla-bundle/package.json @@ -26,11 +26,11 @@ "@slickgrid-universal/vanilla-force-bundle": "workspace:~", "bulma": "^1.0.0", "fetch-jsonp": "^1.3.0", - "flatpickr": "^4.6.13", "isomorphic-dompurify": "^2.7.0", "moment-mini": "^2.29.4", "multiple-select-vanilla": "^3.1.0", "rxjs": "^7.8.1", + "vanilla-calendar-picker": "^2.11.2", "whatwg-fetch": "^3.6.20" }, "devDependencies": { diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example03.ts b/examples/vite-demo-vanilla-bundle/src/examples/example03.ts index 182a60e2b..f768f64ab 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example03.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example03.ts @@ -15,6 +15,7 @@ import { SlickGlobalEditorLock, SortComparers, SortDirectionNumber, + type VanillaCalendarOption, } from '@slickgrid-universal/common'; import { BindingEventService } from '@slickgrid-universal/binding'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; @@ -177,7 +178,10 @@ export default class Example03 { }, { id: 'finish', name: 'Finish', field: 'finish', sortable: true, - editor: { model: Editors.date, editorOptions: { minDate: 'today' }, }, + editor: { + model: Editors.date, + editorOptions: { range: { min: 'today' } } as VanillaCalendarOption + }, // formatter: Formatters.dateIso, type: FieldType.date, outputType: FieldType.dateIso, formatter: Formatters.dateIso, diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example11.ts b/examples/vite-demo-vanilla-bundle/src/examples/example11.ts index 40af7bf03..cc84f13d1 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example11.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example11.ts @@ -18,6 +18,7 @@ import { SlickGlobalEditorLock, type SliderOption, SortComparers, + type VanillaCalendarOption, // utilities deepCopy, @@ -177,7 +178,7 @@ export default class Example11 { }, { id: 'finish', name: 'Finish', field: 'finish', sortable: true, minWidth: 80, - editor: { model: Editors.date, massUpdate: true, editorOptions: { minDate: 'today' }, }, + editor: { model: Editors.date, massUpdate: true, editorOptions: { range: { min: 'today' } } as VanillaCalendarOption }, formatter: Formatters.dateIso, type: FieldType.date, outputType: FieldType.dateIso, filterable: true, filter: { model: Filters.compoundDate }, diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example12.ts b/examples/vite-demo-vanilla-bundle/src/examples/example12.ts index e7c799ea0..0c9057a36 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example12.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example12.ts @@ -1,4 +1,3 @@ -// import { Instance as FlatpickrInstance } from 'flatpickr/dist/types/instance'; import { type AutocompleterOption, type Column, @@ -8,7 +7,6 @@ import { EventNamingStyle, FieldType, Filters, - type FlatpickrOption, type Formatter, Formatters, type GridOption, @@ -18,6 +16,7 @@ import { SlickGlobalEditorLock, type SliderOption, SortComparers, + type VanillaCalendarOption, // utilities formatNumber, @@ -271,16 +270,17 @@ export default class Example12 { editor: { model: Editors.date, editorOptions: { - minDate: 'today', + range: { min: 'today' }, // set minimum date as today // if we want to preload the date picker with a different date, - // we could toggle the `closeOnSelect: false`, set the date in the picker and re-toggle `closeOnSelect: true` - // closeOnSelect: false, - // onOpen: (selectedDates: Date[] | Date, dateStr: string, instance: FlatpickrInstance) => { - // instance.setDate('2021-06-04', true); - // instance.set('closeOnSelect', true); - // }, - } as FlatpickrOption, + // we could do it by assigning settings.seleted.dates + // NOTE: vanilla-calendar doesn't automatically focus the picker to the year/month and you need to do it yourself + // selected: { + // dates: ['2021-06-04'], + // month: 6 - 1, // Note: JS Date month (only) is zero index based, so June is 6-1 => 5 + // year: 2021 + // } + } as VanillaCalendarOption, massUpdate: true, validator: (value, args) => { const dataContext = args && args.item; @@ -626,7 +626,7 @@ export default class Example12 { /* if (columnDef.id === 'completed') { this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select.js, show filter in dropdown - this.compositeEditorInstance.changeFormEditorOption('finish', 'minDate', 'today'); // flatpickr, change minDate to today + this.compositeEditorInstance.changeFormEditorOption('finish', 'range', { min: 'today' }); // calendar picker, change minDate to today } */ } diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example14.ts b/examples/vite-demo-vanilla-bundle/src/examples/example14.ts index f8fadce2b..ab24afb01 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example14.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example14.ts @@ -6,7 +6,6 @@ import { EventNamingStyle, FieldType, Filters, - type FlatpickrOption, type Formatter, Formatters, type GridOption, @@ -15,6 +14,7 @@ import { SlickGlobalEditorLock, type SliderRangeOption, SortComparers, + type VanillaCalendarOption, // utilities formatNumber, @@ -220,7 +220,7 @@ export default class Example14 { exportCustomFormatter: Formatters.dateUs, type: FieldType.date, outputType: FieldType.dateUs, saveOutputType: FieldType.dateUtc, filterable: true, filter: { model: Filters.compoundDate }, - editor: { model: Editors.date, editorOptions: { hideClearButton: false } as FlatpickrOption }, + editor: { model: Editors.date, editorOptions: { hideClearButton: false } as VanillaCalendarOption }, }, { id: 'completed', name: 'Completed', field: 'completed', width: 80, minWidth: 75, maxWidth: 100, @@ -242,7 +242,7 @@ export default class Example14 { exportCustomFormatter: Formatters.dateUs, editor: { model: Editors.date, - editorOptions: { minDate: 'today' }, + editorOptions: { range: { min: 'today' } } as VanillaCalendarOption, validator: (value, args) => { const dataContext = args && args.item; if (dataContext && (dataContext.completed && !value)) { @@ -578,7 +578,7 @@ export default class Example14 { } handleOnBeforeEditCell(event) { - const args = event && event.detail && event.detail.args; + const args = event?.detail?.args; const { column, item, grid } = args; if (column && item) { @@ -591,7 +591,7 @@ export default class Example14 { } handleOnCellChange(event) { - const args = event && event.detail && event.detail.args; + const args = event?.detail?.args; const dataContext = args && args.item; // when the field "completed" changes to false, we also need to blank out the "finish" date diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example16.ts b/examples/vite-demo-vanilla-bundle/src/examples/example16.ts index 4f6e37153..69df5a31c 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example16.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example16.ts @@ -10,6 +10,7 @@ import { OperatorType, type SliderOption, type SliderRangeOption, + type VanillaCalendarOption, } from '@slickgrid-universal/common'; import { BindingEventService } from '@slickgrid-universal/binding'; import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; @@ -197,7 +198,7 @@ export default class Example16 { }, { id: 'finish', name: 'Finish', field: 'finish', sortable: true, - editor: { model: Editors.date, editorOptions: { minDate: 'today' }, }, + editor: { model: Editors.date, editorOptions: { range: { min: 'today' } } as VanillaCalendarOption }, // formatter: Formatters.dateIso, type: FieldType.date, outputType: FieldType.dateIso, formatter: Formatters.dateIso, diff --git a/examples/vite-demo-vanilla-bundle/src/main.ts b/examples/vite-demo-vanilla-bundle/src/main.ts index 4ae989561..bed8ff868 100644 --- a/examples/vite-demo-vanilla-bundle/src/main.ts +++ b/examples/vite-demo-vanilla-bundle/src/main.ts @@ -1,5 +1,3 @@ -// import all CSS required by Slickgrid-Universal -import 'flatpickr/dist/flatpickr.min.css'; import './styles.scss'; import { Renderer } from './renderer'; @@ -8,9 +6,6 @@ import { App } from './app'; import AppView from './app.html?raw'; import { TranslateService } from './translate.service'; -// load necessary Flatpickr Locale(s), but make sure it's imported AFTER the SlickerModule import -import 'flatpickr/dist/l10n/fr'; - class Main { app!: App; constructor(private renderer: Renderer) { } diff --git a/examples/vite-demo-vanilla-bundle/src/material-styles.scss b/examples/vite-demo-vanilla-bundle/src/material-styles.scss index ae3dd1ef1..850c9693e 100644 --- a/examples/vite-demo-vanilla-bundle/src/material-styles.scss +++ b/examples/vite-demo-vanilla-bundle/src/material-styles.scss @@ -92,7 +92,7 @@ .slick-headerrow { input.search-filter.filled, .search-filter.filled input, - .search-filter.filled input.flatpickr-input, + .search-filter.filled input.date-picker, .search-filter.filled .input-group-addon.slider-value, .search-filter.filled .input-group-addon.slider-range-value, .search-filter.filled .input-group-addon select { @@ -161,7 +161,7 @@ } input.search-filter.filled, .search-filter.filled input, - .search-filter.filled input.flatpickr-input { + .search-filter.filled .date-picker input { color: var(--slick-text-color); } } diff --git a/examples/vite-demo-vanilla-bundle/vite.config.mts b/examples/vite-demo-vanilla-bundle/vite.config.mts index 3bfc9b9ea..e9d9d53be 100644 --- a/examples/vite-demo-vanilla-bundle/vite.config.mts +++ b/examples/vite-demo-vanilla-bundle/vite.config.mts @@ -10,11 +10,6 @@ export default defineConfig(() => { chunkSizeWarningLimit: 6000, emptyOutDir: true, outDir: '../../website', - rollupOptions: { - external: [ - './node_modules/flatpickr/dist/l10n/fr', - ], - }, }, preview: { port: 8888 diff --git a/package.json b/package.json index 51db0cbba..8638b4398 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^28.2.0", "eslint-plugin-n": "^17.3.1", - "flatpickr": "^4.6.13", "husky": "^9.0.11", "jest": "^29.7.0", "jest-cli": "^29.7.0", @@ -93,6 +92,7 @@ "ts-node": "^10.9.2", "typescript": "^5.4.5", "typescript-eslint": "^7.7.1", + "vanilla-calendar-picker": "^2.11.2", "whatwg-fetch": "^3.6.20" }, "funding": { diff --git a/packages/binding/src/bindingEvent.service.ts b/packages/binding/src/bindingEvent.service.ts index 3e7450d04..c85217a95 100644 --- a/packages/binding/src/bindingEvent.service.ts +++ b/packages/binding/src/bindingEvent.service.ts @@ -18,7 +18,7 @@ export class BindingEventService { } /** Bind an event listener to any element */ - bind(elementOrElements: Element | NodeListOf | Window, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject, listenerOptions?: boolean | AddEventListenerOptions, groupName = '') { + bind(elementOrElements: Document | Element | NodeListOf | Window, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject, listenerOptions?: boolean | AddEventListenerOptions, groupName = '') { // convert to array for looping in next task const eventNames = (Array.isArray(eventNameOrNames)) ? eventNameOrNames : [eventNameOrNames]; diff --git a/packages/common/package.json b/packages/common/package.json index 79d6bd3e9..08fb73bef 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -75,12 +75,12 @@ "autocompleter": "^9.2.1", "dequal": "^2.0.3", "excel-builder-vanilla": "3.0.1", - "flatpickr": "^4.6.13", "isomorphic-dompurify": "^2.7.0", "moment-mini": "^2.29.4", "multiple-select-vanilla": "^3.1.0", "sortablejs": "^1.15.2", - "un-flatten-tree": "^2.0.12" + "un-flatten-tree": "^2.0.12", + "vanilla-calendar-picker": "^2.11.2" }, "devDependencies": { "autoprefixer": "^10.4.19", diff --git a/packages/common/src/commonEditorFilter/commonEditorFilterUtils.ts b/packages/common/src/commonEditorFilter/commonEditorFilterUtils.ts index 9133e6780..149de8f4c 100644 --- a/packages/common/src/commonEditorFilter/commonEditorFilterUtils.ts +++ b/packages/common/src/commonEditorFilter/commonEditorFilterUtils.ts @@ -1,6 +1,11 @@ import type { AutocompleteItem } from 'autocompleter'; +import type { IOptions } from 'vanilla-calendar-picker'; +import * as moment_ from 'moment-mini'; +const moment = (moment_ as any)['default'] || moment_; -import type { AutocompleterOption } from '../interfaces/index'; +import type { AutocompleterOption, Column, ColumnEditor, ColumnFilter } from '../interfaces/index'; +import { formatDateByFieldType, mapMomentDateFormatWithFieldType } from '../services'; +import { FieldType } from '../enums'; /** * add loading class ".slick-autocomplete-loading" to the Kraaden Autocomplete input element @@ -27,4 +32,28 @@ export function addAutocompleteLoadingByOverridingFetch p.format(isoFormat)).join(':')], + month: pickerDates[0].month(), + year: pickerDates[0].year(), + time: inputFormat.toLowerCase().includes('h') ? pickerDates[0].format('HH:mm') : null, + }; + dateInputElm.value = initialDates.length ? pickerDates.map(p => formatDateByFieldType(p, undefined, outputFieldType)).join(' — ') : ''; + } } \ No newline at end of file diff --git a/packages/common/src/core/__tests__/slickDataView.spec.ts b/packages/common/src/core/__tests__/slickDataView.spec.ts index daf3f0b76..08ed55718 100644 --- a/packages/common/src/core/__tests__/slickDataView.spec.ts +++ b/packages/common/src/core/__tests__/slickDataView.spec.ts @@ -3,7 +3,6 @@ import { SortDirectionNumber } from '../../enums'; import { GridOption, Grouping } from '../../interfaces'; import { SortComparers } from '../../sortComparers'; import { SlickDataView } from '../slickDataview'; -import 'flatpickr'; import { SlickGrid } from '../slickGrid'; import { SlickRowSelectionModel } from '../../extensions/slickRowSelectionModel'; import { SlickEventData } from '../slickCore'; diff --git a/packages/common/src/editors/__tests__/dateEditor.spec.ts b/packages/common/src/editors/__tests__/dateEditor.spec.ts index 17b28a549..3738850ae 100644 --- a/packages/common/src/editors/__tests__/dateEditor.spec.ts +++ b/packages/common/src/editors/__tests__/dateEditor.spec.ts @@ -1,17 +1,13 @@ import moment from 'moment-mini'; +import { VanillaCalendar } from 'vanilla-calendar-picker'; import { Editors } from '../index'; import { DateEditor } from '../dateEditor'; import { FieldType } from '../../enums/index'; -import { Column, ColumnEditor, Editor, EditorArguments, GridOption } from '../../interfaces/index'; +import { Column, Editor, EditorArguments, GridOption } from '../../interfaces/index'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; import { SlickEvent, type SlickDataView, type SlickGrid } from '../../core/index'; -const containerId = 'demo-container'; - -// define a
container to simulate the grid container -const template = `
`; - const dataViewStub = { refresh: jest.fn(), } as unknown as SlickDataView; @@ -40,6 +36,27 @@ const gridStub = { onCompositeEditorChange: new SlickEvent(), } as unknown as SlickGrid; +const gridId = 'grid1'; +const gridUid = 'slickgrid_124343'; +const template = + `
+
+
+
+
+
+
+ +
+
+
+
+
+
+
`; + +jest.useFakeTimers(); + describe('DateEditor', () => { let translateService: TranslateServiceStub; let divContainer: HTMLDivElement; @@ -108,18 +125,17 @@ describe('DateEditor', () => { expect(editorCount).toBe(1); }); - it('should initialize the editor and expect to focus on the element after a small delay', (done) => { + it('should initialize the editor and expect to focus on the element after a small delay', () => { const focusSpy = jest.spyOn(editor, 'focus'); - const showSpy = jest.spyOn(editor, 'focus'); + const showSpy = jest.spyOn(editor, 'show'); editor = new DateEditor(editorArguments); const editorCount = divContainer.querySelectorAll('input.editor-text.editor-startDate').length; - setTimeout(() => { - expect(editorCount).toBe(1); - expect(focusSpy).toHaveBeenCalled(); - expect(showSpy).toHaveBeenCalled(); - done(); - }, 51); + jest.runAllTimers(); + + expect(editorCount).toBe(1); + expect(focusSpy).toHaveBeenCalled(); + expect(showSpy).toHaveBeenCalled(); }); it('should have a placeholder when defined in its column definition', () => { @@ -153,16 +169,22 @@ describe('DateEditor', () => { expect(editor.columnEditor).toEqual(mockColumn.editor); }); - it('should call "setValue" and expect the DOM element value to be the same string when calling "getValue"', () => { + it('should call "setValue" and expect the DOM element value to be the same string when calling "getValue"', async () => { editor = new DateEditor(editorArguments); - editor.setValue('2001-01-02T11:02:02.000Z'); - expect(editor.getValue()).toBe('2001-01-02T11:02:02.000Z'); + jest.runAllTimers(); + + editor.setValue('2001-01-02T11:02:00.000Z'); + + expect(editor.getValue()).toBe('2001-01-02T11:02:00.000Z'); }); 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.dateIso; editor = new DateEditor(editorArguments); + + jest.runAllTimers(); + editor.setValue('2001-01-02', true); expect(editor.getValue()).toBe('2001-01-02'); @@ -172,17 +194,19 @@ describe('DateEditor', () => { 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"', () => { mockItemData = { id: 1, startDate: '2001-01-02T11:02:02.000Z', isActive: true }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); - const editorElm = editor.editorDomElement; - expect(editor.getValue()).toBe('2001-01-02T11:02:02.000Z'); - expect(editorElm.defaultValue).toBe('2001-01-02T11:02:02.000Z'); + expect(editor.getValue()).toBe('2001-01-02'); }); it('should hide the DOM element when the "hide" method is called', () => { editor = new DateEditor(editorArguments); - const spy = jest.spyOn(editor.flatInstance, 'close'); - const calendarElm = document.body.querySelector('.flatpickr-calendar'); + + jest.runAllTimers(); + + const spy = jest.spyOn(editor.calendarInstance!, 'hide'); + const calendarElm = document.body.querySelector('.vanilla-calendar'); editor.hide(); expect(calendarElm).toBeTruthy(); @@ -191,68 +215,68 @@ describe('DateEditor', () => { it('should show the DOM element when the "show" method is called', () => { editor = new DateEditor(editorArguments); - const spy = jest.spyOn(editor.flatInstance, 'open'); - const calendarElm = document.body.querySelector('.flatpickr-calendar'); - editor.show(); - editor.focus(); - expect(gridStub.focus).toHaveBeenCalled(); - expect(calendarElm).toBeTruthy(); - expect(spy).toHaveBeenCalled(); - }); + jest.runAllTimers(); - it('should enable Dark Mode and expect ".slick-dark-mode" CSS class to be found on parent element', () => { - gridOptionMock.darkMode = true; - editor = new DateEditor(editorArguments); - const spy = jest.spyOn(editor.flatInstance, 'open'); - const calendarElm = document.body.querySelector('.flatpickr-calendar'); + const spy = jest.spyOn(editor.calendarInstance!, 'show'); + const calendarElm = document.body.querySelector('.vanilla-calendar'); editor.show(); editor.focus(); expect(gridStub.focus).toHaveBeenCalled(); - expect(calendarElm?.classList.contains('slick-dark-mode')).toBeTruthy(); + expect(calendarElm).toBeTruthy(); 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 Flatpickr "set" method', () => { + it('should call the "changeEditorOption" method and expect new option to be merged with the previous Editor options', () => { editor = new DateEditor(editorArguments); - const spy = jest.spyOn(editor.flatInstance, 'set'); - const calendarElm = document.body.querySelector('.flatpickr-calendar'); - editor.changeEditorOption('minDate', 'today'); + const calendarElm = document.body.querySelector('.vanilla-calendar'); + editor.changeEditorOption('range', { disablePast: true }); + editor.changeEditorOption('selected', { dates: ['2001-02-04'], month: 2 }); expect(calendarElm).toBeTruthy(); - expect(spy).toHaveBeenCalledWith('minDate', 'today'); + expect(editor.pickerOptions.settings?.range?.disablePast).toBeTruthy(); + expect(editor.pickerOptions.settings?.selected).toEqual({ dates: ['2001-02-04'], month: 2 }); + + editor.changeEditorOption('range', { edgesOnly: true }); + editor.changeEditorOption('selected', { dates: ['2020-03-10', 'today'] }); + + expect(editor.pickerOptions.settings?.range).toEqual({ disablePast: true, edgesOnly: true }); + expect(editor.pickerOptions.settings?.selected).toEqual({ dates: ['2020-03-10', 'today'], month: 2 }); }); describe('isValueChanged method', () => { it('should return True when date is changed in the picker', () => { - // change to allow input value only for testing purposes & use the regular flatpickr input to test that one too - mockColumn.editor!.editorOptions = { allowInput: true, altInput: false }; + const dateMock = '2024-04-02'; mockItemData = { id: 1, startDate: '2001-01-02T11:02:02.000Z', isActive: true }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); + editor.loadValue(mockItemData); editor.focus(); - const editorInputElm = divContainer.querySelector('.flatpickr input') as HTMLInputElement; + const editorInputElm = editor.editorDomElement; editorInputElm.value = '2024-04-02T16:02:02.239Z'; - editorInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); + editor.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [dateMock] } as unknown as VanillaCalendar); + editor.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [dateMock], hide: jest.fn() } as unknown as VanillaCalendar); expect(editor.isValueChanged()).toBe(true); expect(editor.isValueTouched()).toBe(true); }); it('should return True when date is reset by the clear date button', () => { - // change to allow input value only for testing purposes & use the regular flatpickr input to test that one too - mockColumn.editor!.editorOptions = { allowInput: true, altInput: false }; mockItemData = { id: 1, startDate: '2001-01-02T11:02:02.000Z', isActive: true }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); + editor.loadValue(mockItemData); editor.focus(); - const clearBtnElm = divContainer.querySelector('.btn.btn-clear') as HTMLInputElement; - const editorInputElm = divContainer.querySelector('.flatpickr input') as HTMLInputElement; + const clearBtnElm = divContainer.querySelector('.btn-clear') as HTMLInputElement; + const editorInputElm = divContainer.querySelector('input.date-picker') as HTMLInputElement; clearBtnElm.click(); - editorInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); + editor.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [] } as unknown as VanillaCalendar); + editor.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [], hide: jest.fn() } as unknown as VanillaCalendar); expect(editorInputElm.value).toBe(''); expect(editor.isValueChanged()).toBe(true); @@ -260,15 +284,17 @@ describe('DateEditor', () => { }); it('should also return True when date is reset by the clear date button even if the previous date was empty', () => { - // change to allow input value only for testing purposes & use the regular flatpickr input to test that one too - mockColumn.editor!.editorOptions = { allowInput: true, altInput: false }; mockItemData = { id: 1, startDate: '', isActive: true }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); + editor.loadValue(mockItemData); editor.focus(); - const clearBtnElm = divContainer.querySelector('.btn.btn-clear') as HTMLInputElement; - const editorInputElm = divContainer.querySelector('.flatpickr input') as HTMLInputElement; + const clearBtnElm = divContainer.querySelector('.btn-clear') as HTMLInputElement; + const editorInputElm = divContainer.querySelector('input.date-picker') as HTMLInputElement; + editor.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [] } as unknown as VanillaCalendar); + editor.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [], hide: jest.fn() } as unknown as VanillaCalendar); clearBtnElm.click(); expect(editorInputElm.value).toBe(''); @@ -277,14 +303,18 @@ describe('DateEditor', () => { }); it('should return False when date in the picker is the same as the current date', () => { - mockItemData = { id: 1, startDate: '2001-01-02T11:02:02.000Z', isActive: true }; - mockColumn.editor!.editorOptions = { allowInput: true }; // change to allow input value only for testing purposes + mockItemData = { id: 1, startDate: '2001-01-02', isActive: true }; + mockColumn.type = FieldType.dateIso; editor = new DateEditor(editorArguments); + jest.runAllTimers(); + editor.loadValue(mockItemData); - const editorInputElm = divContainer.querySelector('input.flatpickr-alt-input') as HTMLInputElement; - editorInputElm.value = '2001-01-02T11:02:02.000Z'; - editorInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); + + const editorInputElm = divContainer.querySelector('input.date-picker') as HTMLInputElement; + editorInputElm.value = '2001-01-02'; + editor.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: ['2001-01-02'] } as unknown as VanillaCalendar); + editor.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: ['2001-01-02'], hide: jest.fn() } as unknown as VanillaCalendar); expect(editor.isValueChanged()).toBe(false); expect(editor.isValueTouched()).toBe(true); @@ -293,13 +323,16 @@ describe('DateEditor', () => { it('should return False when input date is invalid', () => { mockItemData = { id: 1, startDate: '1900-02-32', isActive: true }; mockColumn.type = FieldType.dateUs; - mockColumn.editor!.editorOptions = { allowInput: true }; // change to allow input value only for testing purposes + const dateMock = '1900-02-32'; editor = new DateEditor(editorArguments); + jest.runAllTimers(); + editor.loadValue(mockItemData); - const editorInputElm = divContainer.querySelector('input.flatpickr-alt-input') as HTMLInputElement; - editorInputElm.value = '1900-02-32'; - editorInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); + const editorInputElm = divContainer.querySelector('input.date-picker') as HTMLInputElement; + editorInputElm.value = dateMock; + editor.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [dateMock] } as unknown as VanillaCalendar); + editor.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [dateMock], hide: jest.fn() } as unknown as VanillaCalendar); expect(editor.isValueChanged()).toBe(false); expect(editor.isValueTouched()).toBe(true); @@ -314,6 +347,7 @@ describe('DateEditor', () => { const newDate = new Date(Date.UTC(2001, 0, 2, 16, 2, 2, 0)); editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.applyValue(mockItemData, newDate); // @ts-ignore:2349 @@ -329,6 +363,7 @@ describe('DateEditor', () => { const newDate = new Date(Date.UTC(2001, 0, 2, 16, 2, 2, 0)); editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.applyValue(mockItemData, newDate); // @ts-ignore:2349 @@ -343,6 +378,7 @@ describe('DateEditor', () => { const newDate = new Date(Date.UTC(2001, 0, 2, 16, 2, 2, 0)); editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.applyValue(mockItemData, newDate); // @ts-ignore:2349 @@ -359,6 +395,7 @@ describe('DateEditor', () => { mockItemData = { id: 1, startDate: '2001-04-05T11:33:42.000Z', isActive: true }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.applyValue(mockItemData, '2001-01-02T16:02:02.000+05:00'); expect(mockItemData).toEqual({ id: 1, startDate: '', isActive: true }); @@ -371,6 +408,7 @@ describe('DateEditor', () => { mockItemData = { id: 1, startDate: '2001-01-02T16:02:02.000+05:00', isActive: true }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); const output = editor.serializeValue(); @@ -381,6 +419,7 @@ describe('DateEditor', () => { mockItemData = { id: 1, startDate: '', isActive: true }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); const output = editor.serializeValue(); @@ -391,6 +430,7 @@ describe('DateEditor', () => { mockItemData = { id: 1, startDate: null, isActive: true }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); const output = editor.serializeValue(); @@ -403,6 +443,7 @@ describe('DateEditor', () => { mockItemData = { id: 1, employee: { startDate: '2001-01-02T16:02:02.000+05:00' }, isActive: true }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); const output = editor.serializeValue(); @@ -421,6 +462,7 @@ describe('DateEditor', () => { const spy = jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit'); editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); editor.setValue('2022-03-02T16:02:02.000+05:00'); editor.save(); @@ -434,6 +476,7 @@ describe('DateEditor', () => { const spy = jest.spyOn(editorArguments, 'commitChanges'); editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); editor.setValue('2022-03-02T16:02:02.000+05:00'); editor.save(); @@ -448,24 +491,27 @@ describe('DateEditor', () => { const spy = jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit'); editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); editor.save(); expect(spy).not.toHaveBeenCalled(); }); - it('should not throw any error when date is invalid when lower than required "minDate" defined in the "editorOptions" and "autoCommitEdit" is enabled', () => { - // change to allow input value only for testing purposes & use the regular flatpickr input to test that one too - mockColumn.editor!.editorOptions = { minDate: 'today', altInput: true }; + it('should not throw any error when date is lower than required "minDate" defined in the "editorOptions" and "autoCommitEdit" is enabled', () => { + mockColumn.editor!.editorOptions = { range: { min: 'today' } }; mockItemData = { id: 1, startDate: '500-01-02T11:02:02.000Z', isActive: true }; gridOptionMock.autoCommitEdit = true; gridOptionMock.autoEdit = true; gridOptionMock.editable = true; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); - editor.flatInstance.toggle(); - const editorInputElm = divContainer.querySelector('.flatpickr input') as HTMLInputElement; + editor.calendarInstance?.show(); + const editorInputElm = divContainer.querySelector('input.date-picker') as HTMLInputElement; + editor.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [] } as unknown as VanillaCalendar); + editor.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [], hide: jest.fn() } as unknown as VanillaCalendar); expect(editor.pickerOptions).toBeTruthy(); expect(editorInputElm.value).toBe(''); @@ -473,9 +519,9 @@ describe('DateEditor', () => { }); it('should not throw any error when date is invalid when lower than required "minDate" defined in the global default editorOptions and "autoCommitEdit" is enabled', () => { - // change to allow input value only for testing purposes & use the regular flatpickr input to test that one too + // change to allow input value only for testing purposes & use the regular date picker input to test that one too gridOptionMock.defaultEditorOptions = { - date: { minDate: 'today', altInput: true } + date: { range: { min: 'today' } } }; mockItemData = { id: 1, startDate: '500-01-02T11:02:02.000Z', isActive: true }; gridOptionMock.autoCommitEdit = true; @@ -483,9 +529,12 @@ describe('DateEditor', () => { gridOptionMock.editable = true; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); - editor.flatInstance.toggle(); - const editorInputElm = divContainer.querySelector('.flatpickr input') as HTMLInputElement; + editor.calendarInstance?.show(); + const editorInputElm = divContainer.querySelector('input.date-picker') as HTMLInputElement; + editor.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [] } as unknown as VanillaCalendar); + editor.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [], hide: jest.fn() } as unknown as VanillaCalendar); expect(editor.pickerOptions).toBeTruthy(); expect(editorInputElm.value).toBe(''); @@ -497,6 +546,7 @@ describe('DateEditor', () => { it('should return False when field is required and field is empty', () => { mockColumn.editor!.required = true; editor = new DateEditor(editorArguments); + jest.runAllTimers(); const validation = editor.validate(null, ''); expect(validation).toEqual({ valid: false, msg: 'Field is required' }); @@ -505,6 +555,7 @@ describe('DateEditor', () => { it('should return True when field is required and input is a valid input value', () => { mockColumn.editor!.required = true; editor = new DateEditor(editorArguments); + jest.runAllTimers(); const validation = editor.validate(null, 'text'); expect(validation).toEqual({ valid: true, msg: null }); @@ -512,36 +563,20 @@ describe('DateEditor', () => { }); describe('with different locale', () => { - it('should display a console warning when locale is not previously imported', (done) => { - const consoleSpy = jest.spyOn(global.console, 'warn').mockReturnValue(); - - gridOptionMock.translater = translateService; - - translateService.use('zz-yy'); // will be trimmed to 2 chars "zz" - editor = new DateEditor(editorArguments); - setTimeout(() => { - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`[Slickgrid-Universal] Flatpickr missing locale imports (zz), will revert to English as the default locale.`)); - done(); - }); - }); - it('should display text in new locale', async () => { - await (await import('flatpickr/dist/l10n/fr')).French; gridOptionMock.translater = translateService; translateService.use('fr'); editor = new DateEditor(editorArguments); + jest.runAllTimers(); - const spy = jest.spyOn(editor.flatInstance, 'open'); - const calendarElm = document.body.querySelector('.flatpickr-calendar') as HTMLDivElement; - const selectonOptionElms = calendarElm.querySelectorAll(' .flatpickr-monthDropdown-months option'); - - editor.show(); + const calendarElm = document.body.querySelector('.vanilla-calendar') as HTMLDivElement; + const monthElm = calendarElm.querySelector('.vanilla-calendar-month') as HTMLButtonElement; expect(calendarElm).toBeTruthy(); - expect(selectonOptionElms.length).toBe(12); - expect(selectonOptionElms[0].textContent).toBe('janvier'); - expect(spy).toHaveBeenCalled(); + expect(monthElm).toBeTruthy(); + expect(editor.calendarInstance?.settings.lang).toBe('fr'); + // expect(monthElm.textContent).toBe('janvier'); }); }); }); @@ -566,6 +601,7 @@ describe('DateEditor', () => { } as any); mockColumn.type = FieldType.dateIso; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.setValue('2001-01-02', true); expect(editor.getValue()).toContain('2001-01-02'); @@ -583,6 +619,7 @@ describe('DateEditor', () => { } as any); editor = new DateEditor(editorArguments); + jest.runAllTimers(); const disableSpy = jest.spyOn(editor, 'disable'); editor.show(); @@ -602,6 +639,7 @@ describe('DateEditor', () => { } as any); editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); const disableSpy = jest.spyOn(editor, 'disable'); editor.show(); @@ -613,8 +651,8 @@ describe('DateEditor', () => { formValues: { startDate: '' }, editors: {}, triggeredBy: 'user', }, expect.anything()); expect(disableSpy).toHaveBeenCalledWith(true); - expect(editor.flatInstance._input.disabled).toEqual(true); - expect(editor.flatInstance._input.value).toEqual(''); + expect(editor.calendarInstance?.HTMLInputElement?.disabled).toEqual(true); + expect(editor.calendarInstance?.HTMLInputElement?.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', () => { @@ -631,6 +669,7 @@ describe('DateEditor', () => { }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); const disableSpy = jest.spyOn(editor, 'disable'); editor.show(); @@ -639,8 +678,8 @@ describe('DateEditor', () => { 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.flatInstance._input.disabled).toEqual(true); - expect(editor.flatInstance._input.value).toEqual(''); + expect(editor.calendarInstance?.HTMLInputElement?.disabled).toEqual(true); + expect(editor.calendarInstance?.HTMLInputElement?.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', () => { @@ -654,6 +693,7 @@ describe('DateEditor', () => { }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue({ ...mockItemData, startDate: '2020-01-01' }); editor.show(); editor.disable(); @@ -663,13 +703,13 @@ describe('DateEditor', () => { ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub, formValues: {}, editors: {}, triggeredBy: 'user', }, expect.anything()); - expect(editor.flatInstance._input.disabled).toEqual(true); - expect(editor.flatInstance._input.value).toEqual(''); + expect(editor.calendarInstance?.HTMLInputElement?.disabled).toEqual(true); + expect(editor.calendarInstance?.HTMLInputElement?.value).toEqual(''); }); it('should expect "onCompositeEditorChange" to have been triggered with the new value showing up in its "formValues" object', () => { const activeCellMock = { row: 0, cell: 0 }; - mockColumn.editor!.editorOptions = { allowInput: true, altInput: false }; + const dateMock = '2001-01-02'; mockColumn.type = FieldType.dateIso; const getCellSpy = jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(activeCellMock); const onBeforeEditSpy = jest.spyOn(gridStub.onBeforeEditCell, 'notify').mockReturnValue({ @@ -679,20 +719,22 @@ describe('DateEditor', () => { getReturnValue: () => false } as any); gridOptionMock.autoCommitEdit = true; - mockItemData = { id: 1, startDate: '2001-01-02', isActive: true }; + mockItemData = { id: 1, startDate: dateMock, isActive: true }; editor = new DateEditor(editorArguments); + jest.runAllTimers(); editor.loadValue(mockItemData); editor.focus(); - const editorInputElm = divContainer.querySelector('.flatpickr input') as HTMLInputElement; - editorInputElm.value = '2001-01-02'; - editorInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); + const editorInputElm = divContainer.querySelector('input.date-picker') as HTMLInputElement; + editorInputElm.value = dateMock; + editor.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [dateMock] } as unknown as VanillaCalendar); + editor.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: editorInputElm, selectedDates: [dateMock], hide: jest.fn() } as unknown as VanillaCalendar); expect(getCellSpy).toHaveBeenCalled(); expect(onBeforeEditSpy).toHaveBeenCalledWith({ ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub, target: 'composite', compositeEditorOptions: editorArguments.compositeEditorOptions }); expect(onCompositeEditorSpy).toHaveBeenCalledWith({ ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub, - formValues: { startDate: '2001-01-02' }, editors: {}, triggeredBy: 'user', + formValues: { startDate: dateMock }, editors: {}, triggeredBy: 'user', }, expect.anything()); }); }); diff --git a/packages/common/src/editors/dateEditor.ts b/packages/common/src/editors/dateEditor.ts index 6e9b9bbc6..27c278e0b 100644 --- a/packages/common/src/editors/dateEditor.ts +++ b/packages/common/src/editors/dateEditor.ts @@ -1,8 +1,6 @@ import { BindingEventService } from '@slickgrid-universal/binding'; -import { createDomElement, destroyAllElementProps, emptyElement, setDeepValue } from '@slickgrid-universal/utils'; -import flatpickr from 'flatpickr'; -import type { BaseOptions as FlatpickrBaseOptions } from 'flatpickr/dist/types/options'; -import type { Instance as FlatpickrInstance } from 'flatpickr/dist/types/instance'; +import { createDomElement, emptyElement, extend, setDeepValue } from '@slickgrid-universal/utils'; +import { VanillaCalendar, type IOptions } from 'vanilla-calendar-picker'; import * as moment_ from 'moment-mini'; const moment = (moment_ as any)['default'] || moment_; @@ -16,30 +14,31 @@ import type { EditorArguments, EditorValidator, EditorValidationResult, - FlatpickrOption, GridOption, + VanillaCalendarOption, } from './../interfaces/index'; -import { getDescendantProperty, mapFlatpickrDateFormatWithFieldType, mapMomentDateFormatWithFieldType, } from './../services/utilities'; +import { formatDateByFieldType, getDescendantProperty, mapMomentDateFormatWithFieldType, } from './../services/utilities'; import type { TranslaterService } from '../services/translater.service'; import { SlickEventData, type SlickGrid } from '../core/index'; +import { setPickerDates } from '../commonEditorFilter'; +import { sanitizeTextByAvailableSanitizer } from '../services'; /* - * An example of a date picker editor using Flatpickr - * https://chmln.github.io/flatpickr + * An example of a date picker editor using Vanilla-Calendar-Picker */ export class DateEditor implements Editor { protected _bindEventService: BindingEventService; protected _clearButtonElm!: HTMLButtonElement; protected _editorInputGroupElm!: HTMLDivElement; protected _inputElm!: HTMLInputElement; - protected _inputWithDataElm!: HTMLInputElement | null; protected _isValueTouched = false; + protected _lastClickIsDate = false; protected _lastTriggeredByClearDate = false; protected _originalDate?: string; - protected _pickerMergedOptions!: FlatpickrOption; - - flatInstance!: FlatpickrInstance; + protected _pickerMergedOptions!: IOptions; + calendarInstance?: VanillaCalendar; defaultDate?: string; + hasTimePicker = false; /** is the Editor disabled? */ disabled = false; @@ -86,8 +85,8 @@ export class DateEditor implements Editor { return this._inputElm; } - /** Get Flatpickr options passed to the editor by the user */ - get editorOptions(): FlatpickrOption { + /** Get options passed to the editor by the user */ + get editorOptions(): IOptions { return { ...this.gridOptions.defaultEditorOptions?.date, ...this.columnEditor?.editorOptions }; } @@ -95,7 +94,7 @@ export class DateEditor implements Editor { return this.gridOptions.autoCommitEdit ?? false; } - get pickerOptions(): FlatpickrOption { + get pickerOptions(): IOptions { return this._pickerMergedOptions; } @@ -104,46 +103,78 @@ export class DateEditor implements Editor { return this.columnEditor?.validator ?? this.columnDef?.validator; } - init(): void { + async init() { if (this.args && this.columnDef) { const compositeEditorOptions = this.args.compositeEditorOptions; const columnId = this.columnDef?.id ?? ''; const gridOptions = (this.args.grid.getOptions() || {}) as GridOption; - this.defaultDate = (this.args.item) ? this.args.item[this.columnDef.field] : null; - const inputFormat = mapFlatpickrDateFormatWithFieldType(this.columnEditor.type || this.columnDef.type || FieldType.dateUtc); - const outputFormat = mapFlatpickrDateFormatWithFieldType(this.columnDef.outputType || this.columnEditor.type || this.columnDef.type || FieldType.dateUtc); - let currentLocale = this._translaterService?.getCurrentLanguage?.() || gridOptions.locale || 'en'; - if (currentLocale.length > 2) { - currentLocale = currentLocale.substring(0, 2); + this.defaultDate = this.args.item?.[this.columnDef.field]; + const outputFieldType = this.columnDef.outputType || this.columnEditor.type || this.columnDef.type || FieldType.dateUtc; + const outputFormat = mapMomentDateFormatWithFieldType(outputFieldType); + const currentLocale = this._translaterService?.getCurrentLanguage?.() || gridOptions.locale || 'en'; + + // add the time picker when format is UTC (Z) or has the 'h' (meaning hours) + if (outputFormat && (outputFormat === 'Z' || outputFormat.toLowerCase().includes('h'))) { + this.hasTimePicker = true; } - - const pickerOptions: FlatpickrOption = { - defaultDate: this.defaultDate as string, - altInput: true, - altFormat: outputFormat, - dateFormat: inputFormat, - closeOnSelect: true, - wrap: true, - locale: currentLocale, - onChange: () => this.handleOnDateChange(), - errorHandler: (error: Error) => { - if (error.toString().includes('invalid locale')) { - console.warn(`[Slickgrid-Universal] Flatpickr missing locale imports (${currentLocale}), will revert to English as the default locale. - See Flatpickr Localization for more info, for example if we want to use French, then we can import it with: import 'flatpickr/dist/l10n/fr';`); + const pickerFormat = mapMomentDateFormatWithFieldType(this.hasTimePicker ? FieldType.dateTimeIsoAM_PM : FieldType.dateIso); + + const pickerOptions: IOptions = { + input: true, + jumpToSelectedDate: true, + sanitizer: (dirtyHtml) => sanitizeTextByAvailableSanitizer(this.gridOptions, dirtyHtml), + toggleSelected: false, + actions: { + clickDay: () => { + this._lastClickIsDate = true; + }, + changeToInput: (_e, self) => { + if (self.HTMLInputElement) { + let chosenDate = ''; + if (self.selectedDates[0]) { + chosenDate = self.selectedDates[0]; + self.HTMLInputElement.value = formatDateByFieldType(self.selectedDates[0], undefined, outputFieldType); + } else { + self.HTMLInputElement.value = ''; + } + + if (this.hasTimePicker) { + const momentDate = moment(chosenDate, pickerFormat); + momentDate.hours(self.selectedHours); + momentDate.minute(self.selectedMinutes); + self.HTMLInputElement.value = formatDateByFieldType(momentDate, undefined, outputFieldType); + } + + if (this._lastClickIsDate) { + this.handleOnDateChange(); + self.hide(); + } + } } - // for any other error do nothing - // Flatpickr is a little too sensitive and will throw an error when provided date is lower than minDate so just disregard the error completely - } + }, + settings: { + lang: currentLocale, + iso8601: false, + visibility: { + theme: this.gridOptions?.darkMode ? 'dark' : 'light', + positionToInput: 'auto', + weekend: false, + }, + }, }; - // merge options with optional user's custom options - this._pickerMergedOptions = { ...pickerOptions, ...(this.editorOptions as FlatpickrOption) }; - const inputCssClasses = `.editor-text.editor-${columnId}.form-control`; - if (this._pickerMergedOptions.altInput) { - this._pickerMergedOptions.altInputClass = 'flatpickr-alt-input form-control'; + // add the time picker when format is UTC (Z) or has the 'h' (meaning hours) + if (this.hasTimePicker) { + pickerOptions.settings!.selection = { + time: 24 + }; } - this._editorInputGroupElm = createDomElement('div', { className: 'flatpickr input-group' }); + // merge options with optional user's custom options + this._pickerMergedOptions = extend(true, {}, pickerOptions, { settings: this.editorOptions, type: 'default' }); + + const inputCssClasses = `.editor-text.date-picker.editor-${columnId}.form-control.input-group-editor`; + this._editorInputGroupElm = createDomElement('div', { className: 'vanilla-picker 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 btn-clear' }); this._clearButtonElm.appendChild(createDomElement('i', { className: 'icon-clear' })); @@ -153,64 +184,59 @@ export class DateEditor implements Editor { placeholder: this.columnEditor?.placeholder ?? '', title: this.columnEditor && this.columnEditor.title || '', className: inputCssClasses.replace(/\./g, ' '), - dataset: { input: '', defaultdate: this.defaultDate } + dataset: { input: '', defaultdate: this.defaultDate }, + readOnly: true, }, this._editorInputGroupElm ); + this.args.container.appendChild(this._editorInputGroupElm); + // show clear date button (unless user specifically doesn't want it) - if (!(this.editorOptions as FlatpickrOption)?.hideClearButton) { + if (!(this.columnEditor.editorOptions as any)?.hideClearButton) { closeButtonGroupElm.appendChild(this._clearButtonElm); this._editorInputGroupElm.appendChild(closeButtonGroupElm); this._bindEventService.bind(this._clearButtonElm, 'click', () => this._lastTriggeredByClearDate = true); } - this.args.container.appendChild(this._editorInputGroupElm); - this.flatInstance = flatpickr(this._editorInputGroupElm, this._pickerMergedOptions as unknown as Partial); - - // add dark mode CSS class when enabled - if (this.gridOptions?.darkMode) { - this.flatInstance.calendarContainer.classList.add('slick-dark-mode'); - } - - // when we're using an alternate input to display data, we'll consider this input as the one to do the focus later on - // else just use the top one - this._inputWithDataElm = (this._pickerMergedOptions?.altInput) ? document.querySelector(`${inputCssClasses}.flatpickr-alt-input`) : this._inputElm; - - if (!compositeEditorOptions) { - setTimeout(() => { + setTimeout(() => { + this.calendarInstance = new VanillaCalendar(this._inputElm, this._pickerMergedOptions); + this.calendarInstance.init(); + if (!compositeEditorOptions) { this.show(); this.focus(); - }, 50); - } + } + if (this.calendarInstance) { + setPickerDates(this._inputElm, this.calendarInstance, this.defaultDate, this.columnDef, this.columnEditor); + this.calendarInstance.update({ + dates: true, + month: true, + year: true, + time: true, + }); + } + }); } } destroy() { this.hide(); this._bindEventService.unbindAll(); + this.calendarInstance?.destroy(); - if (typeof this.flatInstance?.destroy === 'function') { - this.flatInstance.destroy(); - if (this.flatInstance?.element) { - setTimeout(() => destroyAllElementProps(this.flatInstance)); - } - } emptyElement(this._editorInputGroupElm); - emptyElement(this._inputWithDataElm); emptyElement(this._inputElm); - this._editorInputGroupElm?.remove?.(); - this._inputWithDataElm?.remove?.(); - this._inputElm?.remove?.(); + this._editorInputGroupElm?.remove(); + this._inputElm?.remove(); } disable(isDisabled = true) { const prevIsDisabled = this.disabled; this.disabled = isDisabled; - if (this.flatInstance?._input) { + if (this._inputElm) { if (isDisabled) { - this.flatInstance._input.setAttribute('disabled', 'disabled'); + this._inputElm.setAttribute('disabled', 'disabled'); this._clearButtonElm.disabled = true; // clear picker when it's newly disabled and not empty @@ -219,7 +245,7 @@ export class DateEditor implements Editor { this.reset('', true, true); } } else { - this.flatInstance._input.removeAttribute('disabled'); + this._inputElm.removeAttribute('disabled'); this._clearButtonElm.disabled = false; } } @@ -228,16 +254,15 @@ export class DateEditor implements Editor { /** * 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 - Flatpickr option name - * @param {newValue} newValue - Flatpickr new option value + * @param {string} optionName + * @param {newValue} newValue */ - changeEditorOption(optionName: keyof FlatpickrBaseOptions, newValue: any) { + changeEditorOption>(optionName: T, newValue: K) { if (!this.columnEditor.editorOptions) { this.columnEditor.editorOptions = {}; } this.columnEditor.editorOptions[optionName] = newValue; - this._pickerMergedOptions = { ...this._pickerMergedOptions, [optionName]: newValue }; - this.flatInstance.set(optionName, newValue); + this._pickerMergedOptions = extend(true, {}, this._pickerMergedOptions, { settings: { [optionName]: newValue } }); } focus() { @@ -246,22 +271,16 @@ export class DateEditor implements Editor { this.show(); this._inputElm?.focus(); - if (this._inputWithDataElm?.focus) { - this._inputWithDataElm.focus(); - this._inputWithDataElm.select(); - } } hide() { - if (this.flatInstance && typeof this.flatInstance.close === 'function') { - this.flatInstance.close(); - } + this.calendarInstance?.hide(); } show() { const isCompositeEditor = !!this.args?.compositeEditorOptions; - if (!isCompositeEditor && this.flatInstance && typeof this.flatInstance.open === 'function' && this.flatInstance._input) { - this.flatInstance.open(); + if (!isCompositeEditor && this.calendarInstance) { + this.calendarInstance.show(); } else if (isCompositeEditor) { // when it's a Composite Editor, we'll check if the Editor is editable (by checking onBeforeEditCell) and if not Editable we'll disable the Editor this.applyInputUsabilityState(); @@ -273,7 +292,9 @@ export class DateEditor implements Editor { } setValue(val: string, isApplyingValue = false, triggerOnCompositeEditorChange = true) { - this.flatInstance.setDate(val); + if (this.calendarInstance) { + setPickerDates(this._inputElm, this.calendarInstance, val, this.columnDef, this.columnEditor); + } if (isApplyingValue) { this.applyValue(this.args.item, this.serializeValue()); @@ -287,21 +308,21 @@ export class DateEditor implements Editor { } applyValue(item: any, state: any) { - const fieldName = this.columnDef && this.columnDef.field; - if (fieldName !== undefined) { - const outputTypeFormat = mapMomentDateFormatWithFieldType((this.columnDef && (this.columnDef.outputType || this.columnEditor.type || this.columnDef.type)) || FieldType.dateUtc); - const saveTypeFormat = mapMomentDateFormatWithFieldType((this.columnDef && (this.columnDef.saveOutputType || this.columnDef.outputType || this.columnEditor.type || this.columnDef.type)) || FieldType.dateUtc); - const isComplexObject = fieldName?.indexOf('.') > 0; // is the field a complex object, "address.streetNumber" + const fieldName = this.columnDef?.field; + if (this.columnDef && fieldName !== undefined) { + const saveFieldType = this.columnDef.saveOutputType || this.columnDef.outputType || this.columnEditor.type || this.columnDef.type || FieldType.dateUtc; + const outputFieldType = this.columnDef.outputType || this.columnEditor.type || this.columnDef.type || FieldType.dateUtc; + const isComplexObject = fieldName.indexOf('.') > 0; // is the field a complex object, "address.streetNumber" // validate the value before applying it (if not valid we'll set an empty string) const validation = this.validate(null, state); - const newValue = (state && validation && validation.valid) ? moment(state, outputTypeFormat).format(saveTypeFormat) : ''; + const newValue = (state && validation?.valid) ? formatDateByFieldType(state, outputFieldType, saveFieldType) : ''; // set the new value to the item datacontext if (isComplexObject) { // when it's a complex object, user could override the object path (where the editable object is located) // else we use the path provided in the Field Column Definition - const objectPath = this.columnEditor?.complexObjectPath ?? fieldName ?? ''; + const objectPath = this.columnEditor?.complexObjectPath ?? fieldName; setDeepValue(item, objectPath, newValue); } else { item[fieldName] = newValue; @@ -310,15 +331,12 @@ export class DateEditor implements Editor { } isValueChanged(): boolean { - const elmValue = this._inputElm.value; - const inputFormat = mapMomentDateFormatWithFieldType(this.columnEditor.type || this.columnDef?.type || FieldType.dateIso); - const outputTypeFormat = mapMomentDateFormatWithFieldType((this.columnDef && (this.columnDef.outputType || this.columnEditor.type || this.columnDef.type)) || FieldType.dateUtc); - const elmDateStr = elmValue ? moment(elmValue, inputFormat, false).format(outputTypeFormat) : ''; - const orgDateStr = this._originalDate ? moment(this._originalDate, inputFormat, false).format(outputTypeFormat) : ''; - if (elmDateStr === 'Invalid date' || orgDateStr === 'Invalid date') { - return false; + let isChanged = false; + const elmDateStr = this.getValue(); + + if (this.columnDef) { + isChanged = this._lastTriggeredByClearDate || (!(elmDateStr === '' && this._originalDate === '')) && (elmDateStr !== this._originalDate); } - const isChanged = this._lastTriggeredByClearDate || (!(elmDateStr === '' && orgDateStr === '')) && (elmDateStr !== orgDateStr); return isChanged; } @@ -328,15 +346,17 @@ export class DateEditor implements Editor { } loadValue(item: any) { - const fieldName = this.columnDef && this.columnDef.field; + const fieldName = this.columnDef?.field; - if (item && fieldName !== undefined) { + if (item && this.columnDef && fieldName !== undefined) { // is the field a complex object, "address.streetNumber" const isComplexObject = fieldName?.indexOf('.') > 0; - const value = (isComplexObject) ? getDescendantProperty(item, fieldName) : item[fieldName]; + const value = isComplexObject ? getDescendantProperty(item, fieldName) : item[fieldName]; + const inputFieldType = this.columnEditor.type || this.columnDef?.type || FieldType.dateIso; + const outputFieldType = this.columnDef.outputType || this.columnEditor.type || this.columnDef.type || FieldType.dateIso; - this._originalDate = value; - this.flatInstance.setDate(value); + this._originalDate = formatDateByFieldType(value, inputFieldType, outputFieldType); + this._inputElm.value = this._originalDate; } } @@ -346,11 +366,12 @@ export class DateEditor implements Editor { */ reset(value?: string, triggerCompositeEventWhenExist = true, clearByDisableCommand = false) { const inputValue = value ?? this._originalDate ?? ''; - if (this.flatInstance) { + if (this.calendarInstance) { this._originalDate = inputValue; - this.flatInstance.setDate(inputValue); + this.calendarInstance.settings.selected.dates = [inputValue]; if (!inputValue) { - this.flatInstance.clear(); + this.calendarInstance.settings.selected.dates = []; + this._inputElm.value = ''; } } this._isValueTouched = false; @@ -364,7 +385,7 @@ export class DateEditor 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. @@ -376,22 +397,17 @@ export class DateEditor implements Editor { } serializeValue() { - const domValue: string = this._inputElm.value; - + const domValue = this.getValue(); if (!domValue) { return ''; } - const inputFormat = mapMomentDateFormatWithFieldType(this.columnEditor.type || this.columnDef?.type || FieldType.dateIso); - const outputTypeFormat = mapMomentDateFormatWithFieldType((this.columnDef && (this.columnDef.outputType || this.columnEditor.type || this.columnDef.type)) || FieldType.dateIso); - const value = moment(domValue, inputFormat, false).format(outputTypeFormat); - - return value; + return domValue; } validate(_targetElm?: any, inputValue?: any): EditorValidationResult { const isRequired = this.args?.compositeEditorOptions ? false : this.columnEditor.required; - const elmValue = (inputValue !== undefined) ? inputValue : this._inputElm?.value; + const elmValue = inputValue ?? this._inputElm?.value; const errorMsg = this.columnEditor.errorMessage; // when using Composite Editor, we also want to recheck if the field if disabled/enabled since it might change depending on other inputs on the composite form @@ -431,9 +447,8 @@ export class DateEditor implements Editor { protected handleOnDateChange() { this._isValueTouched = true; - const currentFlatpickrOptions = this.flatInstance?.config ?? this._pickerMergedOptions; - if (this.args && currentFlatpickrOptions?.closeOnSelect) { + if (this.args) { const compositeEditorOptions = this.args.compositeEditorOptions; if (compositeEditorOptions) { this.handleChangeOnCompositeEditor(compositeEditorOptions); diff --git a/packages/common/src/editors/editors.index.ts b/packages/common/src/editors/editors.index.ts index 1e4cee0be..3ea7648f3 100644 --- a/packages/common/src/editors/editors.index.ts +++ b/packages/common/src/editors/editors.index.ts @@ -18,7 +18,7 @@ export const Editors = { /** Checkbox Editor (uses native checkbox DOM element) */ checkbox: CheckboxEditor, - /** Date Picker Editor (which uses 3rd party lib "flatpickr") */ + /** Date Picker Editor (which uses 3rd party lib "vanilla-calendar-picker") */ date: DateEditor, /** Dual Input Editor, default input type is text but it could be (integer/float/number/password/text) */ diff --git a/packages/common/src/extensions/__tests__/slickCellExcelCopyManager.spec.ts b/packages/common/src/extensions/__tests__/slickCellExcelCopyManager.spec.ts index 283a25993..c05ab7c26 100644 --- a/packages/common/src/extensions/__tests__/slickCellExcelCopyManager.spec.ts +++ b/packages/common/src/extensions/__tests__/slickCellExcelCopyManager.spec.ts @@ -6,8 +6,6 @@ import { SlickCellExternalCopyManager } from '../slickCellExternalCopyManager'; import { SlickEvent, SlickEventData, SlickGrid, SlickRange } from '../../core/index'; import { Editors } from '../../editors'; -jest.mock('flatpickr', () => { }); - const getEditorLockMock = { isActive: jest.fn(), commitCurrentEdit: jest.fn(), @@ -364,7 +362,7 @@ describe('CellExcelCopyManager', () => { (gridStub.getCellEditor as jest.Mock).mockReturnValue({}); (gridStub.getActiveCell as jest.Mock).mockReturnValue({ row: 6, cell: 6 }); - const output = plugin.addonOptions!.dataItemColumnValueExtractor!({ firstName: 'John', lastName: 'Doe' }, { id: 'firstName', field: 'firstName', exportWithFormatter: true, editor: { model: Editors.text }, formatter: myBoldFormatter}, 6, 6); + const output = plugin.addonOptions!.dataItemColumnValueExtractor!({ firstName: 'John', lastName: 'Doe' }, { id: 'firstName', field: 'firstName', exportWithFormatter: true, editor: { model: Editors.text }, formatter: myBoldFormatter }, 6, 6); expect(output).toBeNull(); }); diff --git a/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts b/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts index 5a990b7d3..26ef2e661 100644 --- a/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts +++ b/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts @@ -8,8 +8,6 @@ import { InputEditor } from '../../editors/inputEditor'; import { SlickEvent, SlickEventData, SlickGrid, SlickRange } from '../../core/index'; import { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; -jest.mock('flatpickr', () => { }); - const pubSubServiceStub = { publish: jest.fn(), subscribe: jest.fn(), diff --git a/packages/common/src/extensions/__tests__/slickCellRangeDecorator.spec.ts b/packages/common/src/extensions/__tests__/slickCellRangeDecorator.spec.ts index f9b47724a..834184574 100644 --- a/packages/common/src/extensions/__tests__/slickCellRangeDecorator.spec.ts +++ b/packages/common/src/extensions/__tests__/slickCellRangeDecorator.spec.ts @@ -4,8 +4,6 @@ import type { GridOption } from '../../interfaces/index'; import { SlickCellRangeDecorator } from '../slickCellRangeDecorator'; import { SlickGrid } from '../../core'; -jest.mock('flatpickr', () => { }); - const gridStub = { getActiveCell: jest.fn(), getActiveCanvasNode: jest.fn(), diff --git a/packages/common/src/extensions/__tests__/slickCellRangeSelector.spec.ts b/packages/common/src/extensions/__tests__/slickCellRangeSelector.spec.ts index 33e2ed70b..01d36033d 100644 --- a/packages/common/src/extensions/__tests__/slickCellRangeSelector.spec.ts +++ b/packages/common/src/extensions/__tests__/slickCellRangeSelector.spec.ts @@ -6,7 +6,6 @@ import { SlickEvent, SlickGrid } from '../../core/index'; import { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; const GRID_UID = 'slickgrid_12345'; -jest.mock('flatpickr', () => { }); const addVanillaEventPropagation = function (event) { Object.defineProperty(event, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); diff --git a/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts b/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts index 5a1a2c733..5785a92de 100644 --- a/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts +++ b/packages/common/src/extensions/__tests__/slickCellSelectionModel.spec.ts @@ -9,7 +9,6 @@ import { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; const GRID_UID = 'slickgrid_12345'; const NB_ITEMS = 200; const CALCULATED_PAGE_ROW_COUNT = 23; // pageRowCount with our mocked sizes is 23 => ((600 - 17) / 25) -jest.mock('flatpickr', () => { }); const addVanillaEventPropagation = function (event, commandKeys: string[] = [], keyName = '') { Object.defineProperty(event, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); diff --git a/packages/common/src/extensions/__tests__/slickGridMenu.spec.ts b/packages/common/src/extensions/__tests__/slickGridMenu.spec.ts index fb7b6e843..a80d4502e 100644 --- a/packages/common/src/extensions/__tests__/slickGridMenu.spec.ts +++ b/packages/common/src/extensions/__tests__/slickGridMenu.spec.ts @@ -9,8 +9,6 @@ import { type SlickDataView, SlickEvent, SlickEventData, SlickGrid } from '../.. import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; import { ExtensionUtility } from '../../extensions/extensionUtility'; -jest.mock('flatpickr', () => { }); - const gridId = 'grid1'; const gridUid = 'slickgrid_124343'; const containerId = 'demo-container'; diff --git a/packages/common/src/extensions/__tests__/slickRowMoveManager.spec.ts b/packages/common/src/extensions/__tests__/slickRowMoveManager.spec.ts index a3ac4abc2..0e008f5b4 100644 --- a/packages/common/src/extensions/__tests__/slickRowMoveManager.spec.ts +++ b/packages/common/src/extensions/__tests__/slickRowMoveManager.spec.ts @@ -6,7 +6,6 @@ import { SlickRowMoveManager } from '../slickRowMoveManager'; import { SlickEvent, SlickGrid } from '../../core/index'; const GRID_UID = 'slickgrid_12345'; -jest.mock('flatpickr', () => { }); const addVanillaEventPropagation = function (event, target?: HTMLElement) { Object.defineProperty(event, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); diff --git a/packages/common/src/extensions/__tests__/slickRowSelectionModel.spec.ts b/packages/common/src/extensions/__tests__/slickRowSelectionModel.spec.ts index 602c4cb2f..4f83bfc0c 100644 --- a/packages/common/src/extensions/__tests__/slickRowSelectionModel.spec.ts +++ b/packages/common/src/extensions/__tests__/slickRowSelectionModel.spec.ts @@ -7,7 +7,6 @@ import { SlickRowSelectionModel } from '../slickRowSelectionModel'; import { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; const GRID_UID = 'slickgrid_12345'; -jest.mock('flatpickr', () => { }); const addVanillaEventPropagation = function (event, commandKey = '', keyName = '') { Object.defineProperty(event, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() }); diff --git a/packages/common/src/filters/__tests__/compoundDateFilter.spec.ts b/packages/common/src/filters/__tests__/compoundDateFilter.spec.ts index 4e473abfd..54095fb65 100644 --- a/packages/common/src/filters/__tests__/compoundDateFilter.spec.ts +++ b/packages/common/src/filters/__tests__/compoundDateFilter.spec.ts @@ -1,4 +1,6 @@ import 'jest-extended'; +import { VanillaCalendar } from 'vanilla-calendar-picker'; + import { Filters } from '../filters.index'; import { FieldType, OperatorType } from '../../enums/index'; import { Column, FilterArguments, GridOption } from '../../interfaces/index'; @@ -28,6 +30,8 @@ const gridStub = { render: jest.fn(), } as unknown as SlickGrid; +jest.useFakeTimers(); + describe('CompoundDateFilter', () => { let divContainer: HTMLDivElement; let filter: CompoundDateFilter; @@ -83,15 +87,17 @@ describe('CompoundDateFilter', () => { mockColumn.filter!.placeholder = testValue; filter.init(filterArguments); - const filterElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; + const filterElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; expect(filterElm.placeholder).toBe(testValue); }); it('should hide the DOM element when the "hide" method is called', () => { filter.init(filterArguments); - const spy = jest.spyOn(filter.flatInstance, 'close'); - const calendarElm = document.body.querySelector('.flatpickr-calendar') as HTMLDivElement; + const spy = jest.spyOn(filter.calendarInstance!, 'hide'); + const inputElm = document.body.querySelector('input.date-picker') as HTMLInputElement; + inputElm.dispatchEvent(new MouseEvent('click')); + const calendarElm = document.body.querySelector('.vanilla-calendar') as HTMLDivElement; filter.hide(); expect(calendarElm).toBeTruthy(); @@ -100,44 +106,37 @@ describe('CompoundDateFilter', () => { it('should show the DOM element when the "show" method is called', () => { filter.init(filterArguments); - const spy = jest.spyOn(filter.flatInstance, 'open'); - const calendarElm = document.body.querySelector('.flatpickr-calendar') as HTMLDivElement; + const spy = jest.spyOn(filter.calendarInstance!, 'show'); filter.show(); + const calendarElm = document.body.querySelector('.vanilla-calendar') as HTMLDivElement; expect(calendarElm).toBeTruthy(); expect(spy).toHaveBeenCalled(); }); - it('should enable Dark Mode and expect ".slick-dark-mode" CSS class to be found on parent element', () => { - jest.spyOn(gridStub, 'getOptions').mockReturnValue({ - ...gridOptionMock, darkMode: true - }); - + it('should be able to retrieve default picker options through the Getter', () => { filter.init(filterArguments); - const spy = jest.spyOn(filter.flatInstance, 'open'); - const calendarElm = document.body.querySelector('.flatpickr-calendar') as HTMLDivElement; - filter.show(); - - expect(calendarElm.classList.contains('slick-dark-mode')).toBeTruthy(); - expect(spy).toHaveBeenCalled(); - }); - it('should be able to retrieve default flatpickr options through the Getter', () => { - filter.init(filterArguments); - - expect(filter.flatInstance).toBeTruthy(); - expect(filter.flatpickrOptions).toEqual({ - altFormat: 'Y-m-d', - altInput: true, - closeOnSelect: true, - dateFormat: 'Y-m-d', - defaultDate: '', - errorHandler: expect.toBeFunction(), - locale: 'en', - mode: 'single', - onChange: expect.anything(), - theme: 'light', - wrap: true, + expect(filter.calendarInstance).toBeTruthy(); + expect(filter.pickerOptions).toEqual({ + actions: { + changeToInput: expect.any(Function), + clickDay: expect.any(Function), + }, + input: true, + jumpToSelectedDate: true, + sanitizer: expect.any(Function), + toggleSelected: false, + settings: { + iso8601: false, + lang: 'en', + visibility: { + positionToInput: 'auto', + theme: 'light', + weekend: false, + }, + }, + type: 'default' }); }); @@ -167,14 +166,14 @@ describe('CompoundDateFilter', () => { }); it('should trigger input change event and expect the callback to be called with the date provided in the input', () => { - mockColumn.filter!.filterOptions = { allowInput: true }; // change to allow input value only for testing purposes mockColumn.filter!.operator = '>'; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; filterInputElm.value = '2001-01-02T16:02:02.239Z'; - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2001-01-02'], hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2001-01-02'], hide: jest.fn() } as unknown as VanillaCalendar); const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); @@ -183,15 +182,29 @@ describe('CompoundDateFilter', () => { }); }); + it('should trigger input change event with empty value and still expect the callback to be called with the date provided in the input', () => { + mockColumn.filter!.operator = '>'; + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; + filterInputElm.value = '2001-01-02T16:02:02.239Z'; + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: [], hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: [], hide: jest.fn() } as unknown as VanillaCalendar); + const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); + + expect(filterFilledElms.length).toBe(0); + expect(filterInputElm.value).toBe(''); + }); + it('should pass a different operator then trigger an input change event and expect the callback to be called with the date provided in the input', () => { - mockColumn.filter!.filterOptions = { allowInput: true }; // change to allow input value only for testing purposes mockColumn.filter!.operator = '>'; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; filterInputElm.value = '2001-01-02T16:02:02.239Z'; - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2001-01-02'], hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2001-01-02'], hide: jest.fn() } as unknown as VanillaCalendar); const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); @@ -199,12 +212,11 @@ describe('CompoundDateFilter', () => { }); it('should change operator dropdown without a date entered and not expect the callback to be called', () => { - mockColumn.filter!.filterOptions = { allowInput: true }; // change to allow input value only for testing purposes mockColumn.filter!.operator = '>'; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; const filterSelectElm = divContainer.querySelector('.search-filter.filter-finish select') as HTMLInputElement; filterInputElm.value = undefined as any; filterSelectElm.value = '<='; @@ -214,13 +226,12 @@ describe('CompoundDateFilter', () => { }); it('should change operator dropdown without a date entered and expect the callback to be called when "skipCompoundOperatorFilterWithNullInput" is defined as False', () => { - mockColumn.filter!.filterOptions = { allowInput: true }; // change to allow input value only for testing purposes mockColumn.filter!.operator = '>'; mockColumn.filter!.skipCompoundOperatorFilterWithNullInput = false; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; const filterSelectElm = divContainer.querySelector('.search-filter.filter-finish select') as HTMLInputElement; filterInputElm.value = undefined as any; filterSelectElm.value = '<='; @@ -229,22 +240,109 @@ describe('CompoundDateFilter', () => { expect(spyCallback).toHaveBeenCalled(); }); - it('should create the input filter with a default search term when passed as a filter argument', () => { - filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z']; + it('should hide picker when pressing Escape key', () => { + const hideSpy = jest.spyOn(filter, 'hide'); + + filter.init(filterArguments); + filter.show(); + const calendarElm = document.body.querySelector('.vanilla-calendar') as HTMLDivElement; + + expect(calendarElm).toBeTruthy(); + + calendarElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })); + expect(hideSpy).toHaveBeenCalled(); + }); + + it('should hide picker when pressing Tab key', () => { + const hideSpy = jest.spyOn(filter, 'hide'); + + filter.init(filterArguments); + filter.show(); + const calendarElm = document.body.querySelector('.vanilla-calendar') as HTMLDivElement; + + expect(calendarElm).toBeTruthy(); + + calendarElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true })); + expect(hideSpy).toHaveBeenCalled(); + }); + + it('should clear picker when pressing Backspace key', () => { + filterArguments.searchTerms = ['2000-01-01']; mockColumn.filter!.operator = '<='; + const clearSpy = jest.spyOn(filter, 'clear'); + + filter.init(filterArguments); + filter.show(); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; + const calendarElm = document.body.querySelector('.vanilla-calendar') as HTMLDivElement; + + expect(calendarElm).toBeTruthy(); + expect(filterInputElm.value).toBe('2000-01-01'); + + filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { key: 'Backspace', bubbles: true, cancelable: true })); + expect(clearSpy).toHaveBeenCalled(); + expect(filterInputElm.value).toBe(''); + }); + + it('should create the input filter with a default search terms when passed as a filter argument', () => { + filterArguments.searchTerms = ['2000-01-01T05:00:00.000+05:00']; + mockColumn.filter!.operator = '<='; + mockColumn.type = FieldType.dateUtc; + mockColumn.outputType = FieldType.dateUtc; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2000-01-01T05:00:00.000+05:00'], hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2000-01-01T05:00:00.000+05:00'], hide: jest.fn() } as unknown as VanillaCalendar); const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); - expect(filter.currentDateOrDates).toBe('2000-01-01T05:00:00.000Z'); - expect(filterInputElm.value).toBe('2000-01-01'); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['2000-01-01T05:00:00.000Z'], shouldTriggerQuery: true }); + expect(filter.currentDateOrDates![0].toISOString()).toBe('2000-01-01T10:00:00.000Z'); + expect(filterInputElm.value).toBe('2000-01-01T10:00:00.000Z'); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['2000-01-01T10:00:00.000Z'], shouldTriggerQuery: true }); + }); + + it('should create the input filter with a default input dates when passed as a filter options', () => { + mockColumn.filter!.operator = '<='; + mockColumn.filter!.filterOptions = { + selected: { dates: ['2001-01-02'] } + }; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; + + filterInputElm.focus(); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2000-01-02'], hide: jest.fn() } as unknown as VanillaCalendar); + const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); + + expect(filterFilledElms.length).toBe(1); + expect(filter.currentDateOrDates![0].toISOString()).toBe('2000-01-02T00:00:00.000Z'); + expect(filterInputElm.value).toBe('2000-01-02'); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: '<=', searchTerms: ['2000-01-02'], shouldTriggerQuery: true }); + }); + + it('should have a value with date & time in the picker when "enableTime" option is set as a global default filter option and we trigger a change', () => { + gridOptionMock.defaultFilterOptions = { + date: { selected: { dates: ['2001-01-02'] } } + }; + mockColumn.filter!.operator = '<='; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; + + filterInputElm.focus(); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2000-01-02'], hide: jest.fn() } as unknown as VanillaCalendar); + const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); + + expect(filterFilledElms.length).toBe(1); + expect(filter.currentDateOrDates![0].toISOString()).toBe('2000-01-02T00:00:00.000Z'); + expect(filterInputElm.value).toBe('2000-01-02'); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: '<=', searchTerms: ['2000-01-02'], shouldTriggerQuery: true }); }); it('should trigger an operator change event and expect the callback to be called with the searchTerms and operator defined', () => { @@ -267,67 +365,45 @@ describe('CompoundDateFilter', () => { const mockDate = '2001-01-02T16:02:02.239Z'; filter.init(filterArguments); filter.setValues(mockDate); - let filledInputElm = divContainer.querySelector('.search-filter.filter-finish .filled') as HTMLInputElement; + let filledInputElm = divContainer.querySelector('.search-filter.filter-finish.filled') as HTMLInputElement; expect(filter.currentDateOrDates).toEqual(mockDate); expect(filledInputElm).toBeTruthy(); filter.setValues(''); - filledInputElm = divContainer.querySelector('.search-filter.filter-finish .filled') as HTMLInputElement; + filledInputElm = divContainer.querySelector('.search-filter.filter-finish.filled') as HTMLInputElement; expect(filledInputElm).toBeFalsy(); }); it('should work with different locale when locale is changed', async () => { - await (await import('flatpickr/dist/l10n/fr')).French; - translateService.use('fr'); - filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z']; + filterArguments.searchTerms = ['2000-01-01T05:00:00.000+05:00']; mockColumn.filter!.operator = '<='; + mockColumn.type = FieldType.dateUtc; + mockColumn.outputType = FieldType.dateUtc; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; - const calendarElm = document.body.querySelector('.flatpickr-calendar') as HTMLDivElement; - const selectonOptionElms = calendarElm.querySelectorAll(' .flatpickr-monthDropdown-months option'); + filter.show(); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; + const calendarElm = document.body.querySelector('.vanilla-calendar') as HTMLDivElement; + const monthElm = calendarElm.querySelector('.vanilla-calendar-month') as HTMLButtonElement; filter.show(); filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2000-01-01T05:00:00.000+05:00'], hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2000-01-01T05:00:00.000+05:00'], hide: jest.fn() } as unknown as VanillaCalendar); const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); - expect(filter.currentDateOrDates).toBe('2000-01-01T05:00:00.000Z'); - expect(filterInputElm.value).toBe('2000-01-01'); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['2000-01-01T05:00:00.000Z'], shouldTriggerQuery: true }); + expect(filter.currentDateOrDates![0].toISOString()).toBe('2000-01-01T10:00:00.000Z'); + expect(filterInputElm.value).toBe('2000-01-01T10:00:00.000Z'); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['2000-01-01T10:00:00.000Z'], shouldTriggerQuery: true }); expect(calendarElm).toBeTruthy(); - expect(selectonOptionElms.length).toBe(12); - expect(selectonOptionElms[0].textContent).toBe('janvier'); - }); - - it('should display a console warning when locale is not previously imported', (done) => { - const consoleSpy = jest.spyOn(global.console, 'warn').mockReturnValue(); - - translateService.use('zz-yy'); // will be trimmed to 2 chars "zz" - filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z']; - mockColumn.filter!.operator = '<='; - - filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; - const calendarElm = document.body.querySelector('.flatpickr-calendar') as HTMLDivElement; - const selectonOptionElms = calendarElm.querySelectorAll(' .flatpickr-monthDropdown-months option'); - - filter.show(); - - filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); - - setTimeout(() => { - expect(selectonOptionElms.length).toBe(12); - expect(selectonOptionElms[0].textContent).toBe('January'); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`[Slickgrid-Universal] Flatpickr missing locale imports (zz), will revert to English as the default locale.`)); - done(); - }); + expect(monthElm).toBeTruthy(); + // expect(monthElm.textContent).toBe('janvier'); }); it('should trigger a callback with the clear filter set when calling the "clear" method', () => { @@ -335,13 +411,14 @@ describe('CompoundDateFilter', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); + filter.show(); filter.clear(); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); expect(filterInputElm.value).toBe(''); expect(filterFilledElms.length).toBe(0); - expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); + 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', () => { @@ -350,73 +427,53 @@ describe('CompoundDateFilter', () => { filter.init(filterArguments); filter.clear(false); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); expect(filterInputElm.value).toBe(''); expect(filterFilledElms.length).toBe(0); - expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); }); it('should have a value with date & time in the picker when "enableTime" option is set and we trigger a change', () => { - mockColumn.filter!.filterOptions = { enableTime: true, allowInput: true }; // change to allow input value only for testing purposes - mockColumn.outputType = FieldType.dateTimeIsoAmPm; - mockColumn.filter!.operator = '>'; - const spyCallback = jest.spyOn(filterArguments, 'callback'); - - filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; - filterInputElm.value = '2001-01-02T16:02:02.000+05:00'; - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); - - expect(filterFilledElms.length).toBe(1); - // expect(filter.currentDateOrDates.toISOString()).toBe('2001-01-02T21:02:02.000Z'); - expect(filterInputElm.value).toBe('2001-01-02 4:02:02 PM'); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { - columnDef: mockColumn, operator: '>', searchTerms: ['2001-01-02'], shouldTriggerQuery: true - }); - }); - - it('should have a value with date & time in the picker when "enableTime" option is set as a global default filter option and we trigger a change', () => { - gridOptionMock.defaultFilterOptions = { - date: { enableTime: true, allowInput: true } - }; mockColumn.outputType = FieldType.dateTimeIsoAmPm; mockColumn.filter!.operator = '>'; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; - filterInputElm.value = '2001-01-02T16:02:02.000+05:00'; - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; + filterInputElm.value = '2001-01-02T16:02:02.000'; + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2001-01-02 16:02:02.000'], hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2001-01-02 16:02:02.000'], hide: jest.fn() } as unknown as VanillaCalendar); const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); // expect(filter.currentDateOrDates.toISOString()).toBe('2001-01-02T21:02:02.000Z'); - expect(filterInputElm.value).toBe('2001-01-02 4:02:02 PM'); + expect(filterInputElm.value).toBe('2001-01-02 04:02:02 pm'); expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>', searchTerms: ['2001-01-02'], shouldTriggerQuery: true }); }); it('should have a value with date & time in the picker when using no "outputType" which will default to UTC date', () => { + mockColumn.type = FieldType.dateUtc; mockColumn.outputType = null as any; - filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z']; + filterArguments.searchTerms = ['2000-01-01T05:00:00.000+05:00']; mockColumn.filter!.operator = '<='; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2000-01-01T05:00:00.000+05:00'], hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: ['2000-01-01T05:00:00.000+05:00'], hide: jest.fn() } as unknown as VanillaCalendar); const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); - expect(filter.currentDateOrDates).toBe('2000-01-01T05:00:00.000Z'); - expect(filterInputElm.value).toBe('2000-01-01T05:00:00.000Z'); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['2000-01-01T05:00:00.000Z'], shouldTriggerQuery: true }); + expect(filter.currentDateOrDates![0].toISOString()).toBe('2000-01-01T10:00:00.000Z'); + expect(filterInputElm.value).toBe('2000-01-01T10:00:00.000Z'); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['2000-01-01T10:00:00.000Z'], shouldTriggerQuery: true }); }); it('should have default English text with operator dropdown options related to dates', () => { diff --git a/packages/common/src/filters/__tests__/dateRangeFilter.spec.ts b/packages/common/src/filters/__tests__/dateRangeFilter.spec.ts index ddc728a8b..8d55c9f33 100644 --- a/packages/common/src/filters/__tests__/dateRangeFilter.spec.ts +++ b/packages/common/src/filters/__tests__/dateRangeFilter.spec.ts @@ -1,4 +1,6 @@ import 'jest-extended'; +import { VanillaCalendar } from 'vanilla-calendar-picker'; + import { FieldType } from '../../enums/index'; import { Column, FilterArguments, GridOption } from '../../interfaces/index'; import { Filters } from '../filters.index'; @@ -39,7 +41,7 @@ describe('DateRangeFilter', () => { document.body.appendChild(divContainer); spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer); - mockColumn = { id: 'finish', field: 'finish', filterable: true, filter: { model: Filters.dateRange, operator: 'RangeInclusive' } }; + mockColumn = { id: 'finish', field: 'finish', type: FieldType.dateIso, filterable: true, filter: { model: Filters.dateRange, operator: 'RangeInclusive' } }; filterArguments = { grid: gridStub, columnDef: mockColumn, @@ -64,7 +66,7 @@ describe('DateRangeFilter', () => { it('should initialize the filter', () => { filter.init(filterArguments); - const filterCount = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish').length; + const filterCount = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish').length; expect(spyGetHeaderRow).toHaveBeenCalled(); expect(filterCount).toBe(1); @@ -75,15 +77,17 @@ describe('DateRangeFilter', () => { mockColumn.filter!.placeholder = testValue; filter.init(filterArguments); - const filterElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input') as HTMLInputElement; + const filterElm = divContainer.querySelector('.date-picker.search-filter.filter-finish input') as HTMLInputElement; expect(filterElm.placeholder).toBe(testValue); }); it('should hide the DOM element when the "hide" method is called', () => { filter.init(filterArguments); - const spy = jest.spyOn(filter.flatInstance, 'close'); - const calendarElm = document.body.querySelector('.flatpickr-calendar') as HTMLDivElement; + const spy = jest.spyOn(filter.calendarInstance!, 'hide'); + const inputElm = document.body.querySelector('input.date-picker') as HTMLInputElement; + inputElm.dispatchEvent(new MouseEvent('click')); + const calendarElm = document.body.querySelector('.vanilla-calendar') as HTMLDivElement; filter.hide(); expect(calendarElm).toBeTruthy(); @@ -92,31 +96,42 @@ describe('DateRangeFilter', () => { it('should show the DOM element when the "show" method is called', () => { filter.init(filterArguments); - const spy = jest.spyOn(filter.flatInstance, 'open'); - const calendarElm = document.body.querySelector('.flatpickr-calendar') as HTMLDivElement; + const spy = jest.spyOn(filter.calendarInstance!, 'show'); filter.show(); + const calendarElm = document.body.querySelector('.vanilla-calendar') as HTMLDivElement; expect(calendarElm).toBeTruthy(); expect(spy).toHaveBeenCalled(); }); - it('should be able to retrieve default flatpickr options through the Getter', () => { + it('should be able to retrieve default date picker options through the Getter', () => { filter.init(filterArguments); - expect(filter.flatInstance).toBeTruthy(); - expect(filter.flatpickrOptions).toEqual({ - altFormat: 'Z', - altInput: true, - closeOnSelect: true, - dateFormat: 'Y-m-d', - defaultDate: [], - enableTime: true, - errorHandler: expect.toBeFunction(), - locale: 'en', - mode: 'range', - onChange: expect.anything(), - theme: 'light', - wrap: true, + expect(filter.calendarInstance).toBeTruthy(); + expect(filter.pickerOptions).toEqual({ + actions: { + changeToInput: expect.any(Function), + clickDay: expect.any(Function), + }, + input: true, + jumpMonths: 2, + jumpToSelectedDate: true, + months: 2, + sanitizer: expect.any(Function), + settings: { + iso8601: false, + lang: 'en', + range: { edgesOnly: true }, + selection: { day: 'multiple-ranged', }, + visibility: { + daysOutside: false, + positionToInput: 'auto', + theme: 'light', + weekend: false, + }, + }, + toggleSelected: false, + type: 'multiple' }); }); @@ -135,73 +150,92 @@ describe('DateRangeFilter', () => { }); it('should trigger input change event and expect the callback to be called with the date provided in the input', () => { - mockColumn.filter!.filterOptions = { allowInput: true }; // change to allow input value only for testing purposes mockColumn.filter!.operator = 'RangeInclusive'; const spyCallback = jest.spyOn(filterArguments, 'callback'); + const selectedDates = ['2001-01-02', '2001-01-03', '2001-01-04', '2001-01-05', '2001-01-06', '2001-01-07', '2001-01-08', '2001-01-09', '2001-01-10', '2001-01-11', '2001-01-12', '2001-01-13']; filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.input') as HTMLInputElement; - filterInputElm.value = '2001-01-02T16:02:02.239Z to 2001-01-31T16:02:02.239Z'; - filterInputElm.dispatchEvent(new CustomEvent('change')); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + const filterInputElm = divContainer.querySelector('div.date-picker.search-filter.filter-finish input.date-picker') as HTMLInputElement; + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + const filterFilledElms = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: ['2001-01-02', '2001-01-31'], shouldTriggerQuery: true }); + expect(filterInputElm.value).toBe('2001-01-02 — 2001-01-13'); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: ['2001-01-02', '2001-01-13'], shouldTriggerQuery: true }); + }); + + it('should trigger input change event with empty value and still expect the callback to be called with the date provided in the input', () => { + mockColumn.filter!.operator = 'RangeInclusive'; + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish input.date-picker') as HTMLInputElement; + filterInputElm.value = '2001-01-02T16:02:02.239Z'; + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates: [], hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.changeToInput!(new Event('click'), { HTMLInputElement: filterInputElm, selectedDates: [], hide: jest.fn() } as unknown as VanillaCalendar); + const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); + + expect(filterFilledElms.length).toBe(0); + expect(filterInputElm.value).toBe(''); }); it('should pass a different operator then trigger an input change event and expect the callback to be called with the date provided in the input', () => { - mockColumn.filter!.filterOptions = { allowInput: true, enableTime: false }; // change to allow input value only for testing purposes mockColumn.filter!.operator = 'RangeExclusive'; const spyCallback = jest.spyOn(filterArguments, 'callback'); + const selectedDates = ['2001-01-02', '2001-01-03', '2001-01-04', '2001-01-05', '2001-01-06', '2001-01-07', '2001-01-08', '2001-01-09', '2001-01-10', '2001-01-11', '2001-01-12', '2001-01-13']; filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.input') as HTMLInputElement; - filterInputElm.value = '2001-01-02T16:02:02.239Z to 2001-01-31T16:02:02.239Z'; - filterInputElm.dispatchEvent(new CustomEvent('change')); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + const filterInputElm = divContainer.querySelector('.date-picker.search-filter.filter-finish input.date-picker') as HTMLInputElement; + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + const filterFilledElms = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeExclusive', searchTerms: ['2001-01-02', '2001-01-31'], shouldTriggerQuery: true }); + expect(filterInputElm.value).toBe('2001-01-02 — 2001-01-13'); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'RangeExclusive', searchTerms: ['2001-01-02', '2001-01-13'], shouldTriggerQuery: true }); }); - it('should create the input filter with a default search term when passed as a filter argument', () => { - filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']; + it('should create the input filter with a default search terms when passed as a filter argument', () => { + const selectedDates = ['2001-01-02', '2001-01-03', '2001-01-04', '2001-01-05', '2001-01-06', '2001-01-07', '2001-01-08', '2001-01-09', '2001-01-10', '2001-01-11', '2001-01-12', '2001-01-13']; + filterArguments.searchTerms = ['2001-01-02', '2001-01-13']; mockColumn.filter!.operator = 'RangeInclusive'; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('.date-picker.search-filter.filter-finish input.date-picker') as HTMLInputElement; filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + const filterFilledElms = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); - expect(filterInputElm.value).toBe('2000-01-01T05:00:00.000Z to 2000-01-31T05:00:00.000Z'); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: ['2000-01-01', '2000-01-31'], shouldTriggerQuery: true }); + expect(filterInputElm.value).toBe('2001-01-02 — 2001-01-13'); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: ['2001-01-02', '2001-01-13'], shouldTriggerQuery: true }); }); it('should create the input filter with a default search term when passed as a filter argument with 2 dots (..) notation', () => { - filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z..2000-01-31T05:00:00.000Z']; + const selectedDates = ['2001-01-01', '2001-01-02', '2001-01-03']; + filterArguments.searchTerms = ['2001-01-01..2001-01-03']; mockColumn.filter!.operator = 'RangeInclusive'; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('.date-picker.search-filter.filter-finish input.date-picker') as HTMLInputElement; filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + + const filterFilledElms = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); - expect(filterInputElm.value).toBe('2000-01-01T05:00:00.000Z to 2000-01-31T05:00:00.000Z'); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: ['2000-01-01', '2000-01-31'], shouldTriggerQuery: true }); + expect(filterInputElm.value).toBe('2001-01-01 — 2001-01-03'); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: ['2001-01-01', '2001-01-03'], shouldTriggerQuery: true }); }); it('should be able to call "setValues" and set empty values and the picker input to not have the "filled" css class', () => { - const mockDates = ['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']; + const mockDates = ['2001-01-02T05:00:00.000Z', '2001-01-13T05:00:00.000Z']; filter.init(filterArguments); filter.setValues(mockDates); let filledInputElm = divContainer.querySelector('.search-filter.filter-finish.filled') as HTMLInputElement; @@ -215,65 +249,41 @@ describe('DateRangeFilter', () => { }); it('should work with different locale when locale is changed', async () => { - await (await import('flatpickr/dist/l10n/fr')).French; - translateService.use('fr'); - filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']; + const selectedDates = ['2001-01-01', '2001-01-02', '2001-01-03']; + filterArguments.searchTerms = ['2001-01-01', '2001-01-03']; mockColumn.filter!.operator = 'RangeInclusive'; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.input') as HTMLInputElement; - const calendarElm = document.body.querySelector('.flatpickr-calendar') as HTMLDivElement; - const selectonOptionElms = calendarElm.querySelectorAll(' .flatpickr-monthDropdown-months option'); + filter.show(); + const filterInputElm = divContainer.querySelector('.date-picker.search-filter.filter-finish input.date-picker') as HTMLInputElement; + const calendarElm = document.body.querySelector('.vanilla-calendar') as HTMLDivElement; + const monthElm = calendarElm.querySelector('.vanilla-calendar-month') as HTMLButtonElement; filter.show(); filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + const filterFilledElms = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); - expect(filterInputElm.value).toBe('2000-01-01T05:00:00.000Z au 2000-01-31T05:00:00.000Z'); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: ['2000-01-01', '2000-01-31'], shouldTriggerQuery: true }); + expect(filterInputElm.value).toBe('2001-01-01 — 2001-01-03'); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: ['2001-01-01', '2001-01-03'], shouldTriggerQuery: true }); expect(calendarElm).toBeTruthy(); - expect(selectonOptionElms.length).toBe(12); - expect(selectonOptionElms[0].textContent).toBe('janvier'); - }); - - it('should display a console warning when locale is not previously imported', (done) => { - const consoleSpy = jest.spyOn(global.console, 'warn').mockReturnValue(); - - translateService.use('zz-yy'); // will be trimmed to 2 chars "zz" - filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']; - mockColumn.filter!.operator = 'RangeInclusive'; - - filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.input') as HTMLInputElement; - const calendarElm = document.body.querySelector('.flatpickr-calendar') as HTMLDivElement; - const selectonOptionElms = calendarElm.querySelectorAll(' .flatpickr-monthDropdown-months option'); - - filter.show(); - - filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); - - setTimeout(() => { - expect(selectonOptionElms.length).toBe(12); - expect(selectonOptionElms[0].textContent).toBe('January'); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`[Slickgrid-Universal] Flatpickr missing locale imports (zz), will revert to English as the default locale.`)); - done(); - }); + expect(monthElm).toBeTruthy(); + // expect(monthElm.textContent).toBe('janvier'); }); it('should trigger a callback with the clear filter set when calling the "clear" method', () => { - filterArguments.searchTerms = ['2000-01-01', '2000-01-31']; + filterArguments.searchTerms = ['2001-01-01', '2001-01-03']; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); filter.clear(); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.input') as HTMLInputElement; - const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + const filterInputElm = divContainer.querySelector('.date-picker.search-filter.filter-finish input.date-picker') as HTMLInputElement; + const filterFilledElms = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish.filled'); expect(filterInputElm.value).toBe(''); expect(filterFilledElms.length).toBe(0); @@ -281,13 +291,13 @@ describe('DateRangeFilter', () => { }); it('should trigger a callback with the clear filter but without querying when when calling the "clear" method with False as argument', () => { - filterArguments.searchTerms = ['2000-01-01', '2000-01-31']; + filterArguments.searchTerms = ['2001-01-01', '2001-01-31']; const spyCallback = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); filter.clear(false); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.input') as HTMLInputElement; - const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + const filterInputElm = divContainer.querySelector('.date-picker.search-filter.filter-finish input.date-picker') as HTMLInputElement; + const filterFilledElms = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish.filled'); expect(filterInputElm.value).toBe(''); expect(filterFilledElms.length).toBe(0); @@ -295,44 +305,47 @@ describe('DateRangeFilter', () => { }); it('should have a value with date & time in the picker when "enableTime" option is set and we trigger a change', () => { - mockColumn.filter!.filterOptions = { enableTime: true, allowInput: true }; // change to allow input value only for testing purposes mockColumn.outputType = FieldType.dateTimeIsoAmPm; mockColumn.filter!.operator = '>'; const spyCallback = jest.spyOn(filterArguments, 'callback'); + const selectedDates = ['2001-01-02', '2001-01-03', '2001-01-04', '2001-01-05', '2001-01-06', '2001-01-07', '2001-01-08', '2001-01-09', '2001-01-10', '2001-01-11', '2001-01-12', '2001-01-13']; filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.input') as HTMLInputElement; - filterInputElm.value = '2000-01-01T05:00:00.000+05:00 to 2000-01-31T05:00:00.000+05:00'; - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + const filterInputElm = divContainer.querySelector('.date-picker.search-filter.filter-finish input.date-picker') as HTMLInputElement; + filterInputElm.value = '2001-01-02 — 2001-01-13'; + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + + const filterFilledElms = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); - // expect(filter.currentDateOrDates.map((date) => date.toISOString())).toEqual(['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']); - expect(filterInputElm.value).toBe('2000-01-01 5:00:00 AM to 2000-01-31 5:00:00 AM'); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { - columnDef: mockColumn, operator: '>', searchTerms: ['2000-01-01 05:00:00 am', '2000-01-31 05:00:00 am'], shouldTriggerQuery: true + // expect(filter.currentDateOrDates.map((date) => date.toISOString())).toEqual(['2001-01-01T05:00:00.000Z', '2001-01-31T05:00:00.000Z']); + expect(filterInputElm.value).toBe('2001-01-02 12:00:00 am — 2001-01-13 12:00:00 am'); + expect(spyCallback).toHaveBeenCalledWith(undefined, { + columnDef: mockColumn, operator: '>', searchTerms: ['2001-01-02', '2001-01-13'], shouldTriggerQuery: true }); }); it('should have a value with date & time in the picker when "enableTime" option is set as a global default filter option and we trigger a change', () => { gridOptionMock.defaultFilterOptions = { - date: { enableTime: true, allowInput: true } }; mockColumn.outputType = FieldType.dateTimeIsoAmPm; mockColumn.filter!.operator = '>'; const spyCallback = jest.spyOn(filterArguments, 'callback'); + const selectedDates = ['2001-01-02', '2001-01-03', '2001-01-04', '2001-01-05', '2001-01-06', '2001-01-07', '2001-01-08', '2001-01-09', '2001-01-10', '2001-01-11', '2001-01-12', '2001-01-13']; filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.input') as HTMLInputElement; - filterInputElm.value = '2000-01-01T05:00:00.000+05:00 to 2000-01-31T05:00:00.000+05:00'; - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 13, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + const filterInputElm = divContainer.querySelector('div.date-picker.search-filter.filter-finish input.date-picker') as HTMLInputElement; + filterInputElm.value = '2001-01-02 — 2001-01-13'; + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + const filterFilledElms = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); // expect(filter.currentDateOrDates.map((date) => date.toISOString())).toEqual(['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']); - expect(filterInputElm.value).toBe('2000-01-01 5:00:00 AM to 2000-01-31 5:00:00 AM'); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { - columnDef: mockColumn, operator: '>', searchTerms: ['2000-01-01 05:00:00 am', '2000-01-31 05:00:00 am'], shouldTriggerQuery: true + expect(filterInputElm.value).toBe('2001-01-02 12:00:00 am — 2001-01-13 12:00:00 am'); + expect(spyCallback).toHaveBeenCalledWith(undefined, { + columnDef: mockColumn, operator: '>', searchTerms: ['2001-01-02', '2001-01-13'], shouldTriggerQuery: true }); }); @@ -341,17 +354,19 @@ describe('DateRangeFilter', () => { filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']; mockColumn.filter!.operator = '<='; const spyCallback = jest.spyOn(filterArguments, 'callback'); + const selectedDates = ['2001-01-02', '2001-01-03', '2001-01-04', '2001-01-05', '2001-01-06', '2001-01-07', '2001-01-08', '2001-01-09', '2001-01-10', '2001-01-11', '2001-01-12', '2001-01-13']; filter.init(filterArguments); - const filterInputElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input.flatpickr-input') as HTMLInputElement; + const filterInputElm = divContainer.querySelector('div.date-picker.search-filter.filter-finish input.date-picker') as HTMLInputElement; filterInputElm.focus(); - filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); - const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + filterInputElm.value = '2001-01-02 — 2001-01-13'; + filter.calendarInstance!.actions!.changeToInput!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + filter.calendarInstance!.actions!.clickDay!(new MouseEvent('click'), { HTMLInputElement: filterInputElm, selectedDates, hide: jest.fn() } as unknown as VanillaCalendar); + const filterFilledElms = divContainer.querySelectorAll('.date-picker.search-filter.filter-finish.filled'); expect(filterFilledElms.length).toBe(1); - expect(filter.currentDateOrDates).toEqual(['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']); - expect(filterInputElm.value).toBe('2000-01-01 to 2000-01-31'); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['2000-01-01', '2000-01-31'], shouldTriggerQuery: true }); + expect(filterInputElm.value).toBe('2001-01-02 — 2001-01-13'); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: '<=', searchTerms: ['2001-01-02', '2001-01-13'], shouldTriggerQuery: true }); }); }); diff --git a/packages/common/src/filters/dateFilter.ts b/packages/common/src/filters/dateFilter.ts index f6309b055..9e6d5c961 100644 --- a/packages/common/src/filters/dateFilter.ts +++ b/packages/common/src/filters/dateFilter.ts @@ -1,12 +1,9 @@ import { BindingEventService } from '@slickgrid-universal/binding'; -import { createDomElement, destroyAllElementProps, emptyElement, } from '@slickgrid-universal/utils'; -import flatpickr from 'flatpickr'; +import { createDomElement, emptyElement, extend, } from '@slickgrid-universal/utils'; +import { VanillaCalendar, type IOptions } from 'vanilla-calendar-picker'; import * as moment_ from 'moment-mini'; const moment = (moment_ as any)['default'] || moment_; -import type { BaseOptions as FlatpickrBaseOptions } from 'flatpickr/dist/types/options'; -import type { Instance as FlatpickrInstance } from 'flatpickr/dist/types/instance'; - import { FieldType, OperatorType, @@ -19,29 +16,32 @@ import type { Filter, FilterArguments, FilterCallback, - FlatpickrOption, GridOption, OperatorDetail, } from '../interfaces/index'; import { buildSelectOperator, compoundOperatorNumeric } from './filterUtilities'; -import { mapFlatpickrDateFormatWithFieldType, mapMomentDateFormatWithFieldType, mapOperatorToShorthandDesignation } from '../services/utilities'; +import { formatDateByFieldType, mapMomentDateFormatWithFieldType, mapOperatorToShorthandDesignation } from '../services/utilities'; import type { TranslaterService } from '../services/translater.service'; import type { SlickGrid } from '../core/index'; +import { setPickerDates } from '../commonEditorFilter'; +import { sanitizeTextByAvailableSanitizer } from '../services'; export class DateFilter implements Filter { protected _bindEventService: BindingEventService; protected _clearFilterTriggered = false; protected _currentValue?: string; - protected _currentDateOrDates?: Date | Date[] | string[]; + protected _currentDateOrDates?: Date | Date[] | string | string[]; protected _currentDateStrings?: string[]; - protected _flatpickrOptions!: FlatpickrOption; + protected _lastClickIsDate = false; + protected _pickerOptions!: IOptions; protected _filterElm!: HTMLDivElement; - protected _filterDivInputElm!: HTMLDivElement; + protected _dateInputElm!: HTMLInputElement; protected _operator!: OperatorType | OperatorString; protected _selectOperatorElm?: HTMLSelectElement; protected _shouldTriggerQuery = true; + hasTimePicker = false; inputFilterType: 'compound' | 'range' = 'range'; - flatInstance!: FlatpickrInstance; + calendarInstance?: VanillaCalendar; grid!: SlickGrid; searchTerms: SearchTerm[] = []; columnDef!: Column; @@ -74,13 +74,13 @@ export class DateFilter implements Filter { : (this.gridOptions.defaultFilterRangeOperator || OperatorType.rangeInclusive); } - get filterOptions(): FlatpickrOption { - return { ...this.gridOptions.defaultFilterOptions?.date, ...this.columnFilter?.filterOptions }; + /** Getter for the date picker options */ + get pickerOptions(): IOptions { + return this._pickerOptions || {}; } - /** Getter for the Flatpickr Options */ - get flatpickrOptions(): FlatpickrOption { - return this._flatpickrOptions || {}; + get filterOptions(): IOptions { + return { ...this.gridOptions.defaultFilterOptions?.date, ...this.columnFilter?.filterOptions }; } /** Getter for the Filter Operator */ @@ -100,9 +100,7 @@ export class DateFilter implements Filter { } } - /** - * Initialize the Filter - */ + /** Initialize the Filter */ init(args: FilterArguments) { if (!args) { throw new Error('[Slickgrid-Universal] A filter must always have an "init()" with valid arguments.'); @@ -125,62 +123,70 @@ export class DateFilter implements Filter { // step 1, create the DOM Element of the filter which contain the compound Operator+Input this._filterElm = this.createDomFilterElement(searchValues); + // if there's a search term, we will add the "filled" class for styling purposes + if (this.searchTerms.length) { + this._filterElm.classList.add('filled'); + } + // step 3, subscribe to the keyup event and run the callback when that happens // also add/remove "filled" class for styling purposes - this._bindEventService.bind(this._filterDivInputElm, 'keyup', this.onTriggerEvent.bind(this)); if (this._selectOperatorElm) { this._bindEventService.bind(this._selectOperatorElm, 'change', this.onTriggerEvent.bind(this)); } + + // close picker on Esc/Tab keys + this._bindEventService.bind(document.body, 'keydown', ((e: KeyboardEvent) => { + if (e.key === 'Escape' || e.key === 'Tab') { + this.hide(); + } + }) as EventListener); + + // clear date picker + compound operator when Backspace is pressed + this._bindEventService.bind(this._dateInputElm, 'keydown', ((e: KeyboardEvent) => { + if (e.key === 'Backspace') { + this.clear(true); + } + }) as EventListener); } - /** - * Clear the filter value - */ + /** Clear the filter value */ clear(shouldTriggerQuery = true) { - if (this.flatInstance) { + if (this.calendarInstance) { this._clearFilterTriggered = true; this._shouldTriggerQuery = shouldTriggerQuery; this.searchTerms = []; if (this._selectOperatorElm) { this._selectOperatorElm.selectedIndex = 0; } - if (this.flatInstance.input) { - this.flatInstance.clear(); + if (this.calendarInstance.input) { + this.calendarInstance.settings.selected.dates = []; + this._dateInputElm.value = ''; } } + this.onTriggerEvent(new Event('keyup')); this._filterElm.classList.remove('filled'); - this._filterDivInputElm.classList.remove('filled'); } - /** - * destroy the filter - */ + /** Destroy the filter */ destroy() { this._bindEventService.unbindAll(); + this.calendarInstance?.destroy(); - if (typeof this.flatInstance?.destroy === 'function') { - this.flatInstance.destroy(); - if (this.flatInstance.element) { - destroyAllElementProps(this.flatInstance); - } - } emptyElement(this.filterContainerElm); - emptyElement(this._filterDivInputElm); - this._filterDivInputElm?.remove(); this.filterContainerElm?.remove(); this._selectOperatorElm?.remove(); this._filterElm?.remove(); } hide() { - if (typeof this.flatInstance?.close === 'function') { - this.flatInstance.close(); + if (typeof this.calendarInstance?.hide === 'function') { + this.calendarInstance.hide(); } } show() { - if (typeof this.flatInstance?.open === 'function') { - this.flatInstance.open(); + if (typeof this.calendarInstance?.show === 'function') { + this.calendarInstance.show(); } } @@ -197,7 +203,6 @@ export class DateFilter implements Filter { if (this.inputFilterType === 'compound') { pickerValues = Array.isArray(values) ? values[0] : values; - } else { // get the picker values, if it's a string with the "..", we'll do the split else we'll use the array of search terms if (typeof values === 'string' || (Array.isArray(values) && typeof values[0] === 'string') && (values[0] as string).indexOf('..') > 0) { @@ -207,18 +212,16 @@ export class DateFilter implements Filter { } } - if (this.flatInstance) { + if (this.calendarInstance && pickerValues !== undefined) { + setPickerDates(this._dateInputElm, this.calendarInstance, pickerValues, this.columnDef, this.columnFilter); this._currentDateOrDates = (values && pickerValues) ? pickerValues : undefined; - this.flatInstance.setDate(this._currentDateOrDates || ''); } const currentValueOrValues = this.getValues() || []; if (this.getValues() || (Array.isArray(currentValueOrValues) && currentValueOrValues.length > 0 && values)) { this._filterElm.classList.add('filled'); - this._filterDivInputElm.classList.add('filled'); } else { this._filterElm.classList.remove('filled'); - this._filterDivInputElm.classList.remove('filled'); } // set the operator when defined @@ -232,17 +235,21 @@ export class DateFilter implements Filter { // // protected functions // ------------------ - protected buildDatePickerInput(searchTerms?: SearchTerm | SearchTerm[]): HTMLDivElement { + protected buildDatePickerInput(searchTerms?: SearchTerm | SearchTerm[]) { const columnId = this.columnDef?.id ?? ''; - const inputFormat = mapFlatpickrDateFormatWithFieldType(this.columnFilter.type || this.columnDef.type || FieldType.dateIso); - const outputFormat = mapFlatpickrDateFormatWithFieldType(this.columnDef.outputType || this.columnFilter.type || this.columnDef.type || FieldType.dateUtc); - const userFilterOptions = this.filterOptions as FlatpickrOption; + const columnFieldType = this.columnFilter.type || this.columnDef.type || FieldType.dateIso; + const outputFieldType = this.columnDef.outputType || this.columnFilter.type || this.columnDef.type || FieldType.dateUtc; + const outputFormat = mapMomentDateFormatWithFieldType(outputFieldType); + const inputFieldType = this.columnFilter.type || this.columnDef.type || FieldType.dateIso; - // get current locale, if user defined a custom locale just use or get it the Translate Service if it exist else just use English - let currentLocale = (userFilterOptions?.locale ?? this.translaterService?.getCurrentLanguage?.()) || this.gridOptions.locale || 'en'; - if (currentLocale?.length > 2) { - currentLocale = currentLocale.substring(0, 2); + // add the time picker when format is UTC (Z) or has the 'h' (meaning hours) + if (outputFormat && this.inputFilterType !== 'range' && outputFormat.toLowerCase().includes('h')) { + this.hasTimePicker = true; } + const pickerFormat = mapMomentDateFormatWithFieldType(this.hasTimePicker ? FieldType.dateTimeIsoAM_PM : FieldType.dateIso); + + // get current locale, if user defined a custom locale just use or get it the Translate Service if it exist else just use English + const currentLocale = ((this.filterOptions?.locale ?? this.translaterService?.getCurrentLanguage?.()) || this.gridOptions.locale || 'en') as string; let pickerValues: any | any[]; @@ -262,79 +269,140 @@ export class DateFilter implements Filter { // if we are preloading searchTerms, we'll keep them for reference if (Array.isArray(pickerValues)) { this._currentDateOrDates = pickerValues as Date[]; - const outFormat = mapMomentDateFormatWithFieldType(this.columnFilter.type || this.columnDef.type || FieldType.dateIso); - this._currentDateStrings = pickerValues.map(date => moment(date).format(outFormat)); + this._currentDateStrings = pickerValues.map(date => formatDateByFieldType(date, undefined, inputFieldType)); } } - const pickerOptions: FlatpickrOption = { - defaultDate: (pickerValues || '') as string | string[], - altInput: true, - altFormat: outputFormat, - dateFormat: inputFormat, - mode: this.inputFilterType === 'range' ? 'range' : 'single', - wrap: true, - closeOnSelect: true, - locale: currentLocale, - theme: this.gridOptions?.darkMode ? 'dark' : 'light', - onChange: (selectedDates: Date[] | Date, dateStr: string) => { - if (this.inputFilterType === 'compound') { - this._currentValue = dateStr; - this._currentDateOrDates = Array.isArray(selectedDates) && selectedDates[0] || undefined; - } else { - if (Array.isArray(selectedDates)) { - this._currentDateOrDates = selectedDates; - const outFormat = mapMomentDateFormatWithFieldType(this.columnDef.outputType || this.columnFilter.type || this.columnDef.type || FieldType.dateIso); - this._currentDateStrings = selectedDates.map(date => moment(date).format(outFormat)); - this._currentValue = this._currentDateStrings.join('..'); + const pickerOptions: IOptions = { + input: true, + jumpToSelectedDate: true, + type: this.inputFilterType === 'range' ? 'multiple' : 'default', + sanitizer: (dirtyHtml) => sanitizeTextByAvailableSanitizer(this.gridOptions, dirtyHtml), + toggleSelected: false, + actions: { + clickDay: (_e) => { + this._lastClickIsDate = true; + }, + changeToInput: (_e, self) => { + if (self.HTMLInputElement) { + let outDates: Array = []; + let firstDate = ''; + let lastDate = ''; // when using date range + + if (self.selectedDates[1]) { + self.selectedDates.sort((a, b) => +new Date(a) - +new Date(b)); + firstDate = self.selectedDates[0]; + lastDate = self.selectedDates[self.selectedDates.length - 1]; + const firstDisplayDate = moment(self.selectedDates[0]).format(outputFormat); + const lastDisplayDate = moment(lastDate).format(outputFormat); + self.HTMLInputElement.value = `${firstDisplayDate} — ${lastDisplayDate}`; + outDates = [firstDate, lastDate]; + } else if (self.selectedDates[0]) { + firstDate = self.selectedDates[0]; + self.HTMLInputElement.value = formatDateByFieldType(firstDate, FieldType.dateIso, outputFieldType); + outDates = self.selectedDates; + } else { + self.HTMLInputElement.value = ''; + } + + if (this.hasTimePicker && firstDate) { + const momentDate = moment(firstDate, pickerFormat); + momentDate.hours(self.selectedHours); + momentDate.minute(self.selectedMinutes); + self.HTMLInputElement.value = formatDateByFieldType(momentDate, undefined, outputFieldType); + outDates = [momentDate]; + } + + if (this.inputFilterType === 'compound') { + this._currentValue = formatDateByFieldType(outDates[0], undefined, columnFieldType); + } else { + if (Array.isArray(outDates)) { + this._currentDateStrings = outDates.map(date => formatDateByFieldType(date, undefined, columnFieldType)); + this._currentValue = this._currentDateStrings.join('..'); + } + } + this._currentDateOrDates = outDates.map(dateStr => dateStr instanceof moment ? (dateStr as moment_.Moment).toDate() : new Date(dateStr as string)); + + // when using the time picker, we can simulate a keyup event to avoid multiple backend request + // since backend request are only executed after user start typing, changing the time should be treated the same way + if (this._currentValue) { + const newEvent = this.hasTimePicker ? new Event('keyup') : undefined; + this.onTriggerEvent(newEvent); + } + + // when using date range and we're not yet having 2 dates, then don't close picker just yet + if (this.inputFilterType === 'range' && self.selectedDates.length < 2) { + this._lastClickIsDate = false; + } + // if you want to hide the calendar after picking a date + if (this._lastClickIsDate) { + self.hide(); + this._lastClickIsDate = false; + } } } - - // when using the time picker, we can simulate a keyup event to avoid multiple backend request - // since backend request are only executed after user start typing, changing the time should be treated the same way - const newEvent = pickerOptions.enableTime ? new Event('keyup') : undefined; - this.onTriggerEvent(newEvent); }, - errorHandler: (error) => { - if (error.toString().includes('invalid locale')) { - console.warn(`[Slickgrid-Universal] Flatpickr missing locale imports (${currentLocale}), will revert to English as the default locale. - See Flatpickr Localization for more info, for example if we want to use French, then we can import it with: import 'flatpickr/dist/l10n/fr';`); - } - } + settings: { + lang: currentLocale, + iso8601: false, + visibility: { + theme: this.gridOptions?.darkMode ? 'dark' : 'light', + positionToInput: 'auto', + weekend: false, + }, + }, }; + if (this.inputFilterType === 'range') { + pickerOptions.type = 'multiple'; + pickerOptions.months = 2; + pickerOptions.jumpMonths = 2; + pickerOptions.settings = { + ...pickerOptions.settings, + range: { + edgesOnly: true, + }, + selection: { + day: 'multiple-ranged', + }, + visibility: { + ...pickerOptions.settings?.visibility, + daysOutside: false, + }, + }; + } + // add the time picker when format is UTC (Z) or has the 'h' (meaning hours) - if (outputFormat && (outputFormat === 'Z' || outputFormat.toLowerCase().includes('h'))) { - pickerOptions.enableTime = true; + if (this.hasTimePicker) { + pickerOptions.settings!.selection ??= {}; + pickerOptions.settings!.selection.time = 24; } // merge options with optional user's custom options - this._flatpickrOptions = { ...pickerOptions, ...userFilterOptions }; + this._pickerOptions = extend(true, {}, pickerOptions, { settings: this.filterOptions }); let placeholder = this.gridOptions?.defaultFilterPlaceholder ?? ''; if (this.columnFilter?.placeholder) { placeholder = this.columnFilter.placeholder; } - const filterDivInputElm = createDomElement('div', { className: 'flatpickr' }); - if (this.inputFilterType === 'range') { - filterDivInputElm.classList.add('search-filter', `filter-${columnId}`); - } - filterDivInputElm.appendChild( - createDomElement('input', { - type: 'text', className: 'form-control', - placeholder, - dataset: { input: '', columnid: `${columnId}` } - }) - ); - this.flatInstance = flatpickr(filterDivInputElm, this._flatpickrOptions as unknown as Partial); - - // add dark mode CSS class when enabled - if (this.gridOptions?.darkMode) { - this.flatInstance.calendarContainer.classList.add('slick-dark-mode'); + this._dateInputElm = createDomElement('input', { + type: 'text', className: 'form-control date-picker', + placeholder, + readOnly: true, + dataset: { input: '', columnid: `${columnId}` } + }); + + this.calendarInstance = new VanillaCalendar(this._dateInputElm, this._pickerOptions); + this.calendarInstance.init(); + + if (this._pickerOptions.settings?.selected?.dates) { + pickerValues = this._pickerOptions.settings.selected.dates; } - return filterDivInputElm; + if (pickerValues) { + setPickerDates(this._dateInputElm, pickerOptions, pickerValues, this.columnDef, this.columnFilter); + } } /** Get the available operator option values to populate the operator select dropdown list */ @@ -355,42 +423,40 @@ export class DateFilter implements Filter { emptyElement(this.filterContainerElm); // create the DOM element filter container - this._filterDivInputElm = this.buildDatePickerInput(searchTerms); + this.buildDatePickerInput(searchTerms); if (this.inputFilterType === 'range') { // if there's a search term, we will add the "filled" class for styling purposes + const inputContainerElm = createDomElement('div', { className: `date-picker form-group search-filter filter-${columnId}` }); + if (Array.isArray(searchTerms) && searchTerms.length > 0 && searchTerms[0] !== '') { - this._filterDivInputElm.classList.add('filled'); this._currentDateOrDates = searchTerms as Date[]; this._currentValue = searchTerms[0] as string; } + inputContainerElm.appendChild(this._dateInputElm); // append the new DOM element to the header row - if (this._filterDivInputElm) { - this.filterContainerElm.appendChild(this._filterDivInputElm); + if (inputContainerElm) { + this.filterContainerElm.appendChild(inputContainerElm); } - return this._filterDivInputElm; + return inputContainerElm; } else { this._selectOperatorElm = buildSelectOperator(this.getOperatorOptionValues(), this.grid); - const filterContainerElm = createDomElement('div', { className: `form-group search-filter filter-${columnId}` }); - const containerInputGroupElm = createDomElement('div', { className: 'input-group flatpickr' }, filterContainerElm); + const filterContainerElm = createDomElement('div', { className: `date-picker form-group search-filter filter-${columnId}` }); + const containerInputGroupElm = createDomElement('div', { className: 'input-group date-picker' }, filterContainerElm); const operatorInputGroupAddonElm = createDomElement('div', { className: 'input-group-addon input-group-prepend operator' }, containerInputGroupElm); operatorInputGroupAddonElm.appendChild(this._selectOperatorElm); - containerInputGroupElm.appendChild(this._filterDivInputElm); + containerInputGroupElm.appendChild(this._dateInputElm); if (this.operator) { const operatorShorthand = mapOperatorToShorthandDesignation(this.operator); this._selectOperatorElm.value = operatorShorthand; } - // if there's a search term, we will add the "filled" class for styling purposes - if (searchTerms !== '') { - this._filterDivInputElm.classList.add('filled'); - this._currentDateOrDates = searchTerms as Date; - this._currentValue = searchTerms as string; - } + this._currentDateOrDates = searchTerms as Date; + this._currentValue = searchTerms as string; // append the new DOM element to the header row if (filterContainerElm) { @@ -411,7 +477,7 @@ export class DateFilter implements Filter { this.callback(e, { columnDef: this.columnDef, searchTerms: (this._currentDateStrings ? this._currentDateStrings : [this._currentValue as string]), operator: this.operator || '', shouldTriggerQuery: this._shouldTriggerQuery }); } else if (this.inputFilterType === 'compound' && this._selectOperatorElm) { const selectedOperator = this._selectOperatorElm.value as OperatorString; - (this._currentValue) ? this._filterElm.classList.add('filled') : this._filterElm.classList.remove('filled'); + this._currentValue ? this._filterElm.classList.add('filled') : this._filterElm.classList.remove('filled'); // when changing compound operator, we don't want to trigger the filter callback unless the date input is also provided const skipCompoundOperatorFilterWithNullInput = this.columnFilter.skipCompoundOperatorFilterWithNullInput ?? this.gridOptions.skipCompoundOperatorFilterWithNullInput ?? this.gridOptions.skipCompoundOperatorFilterWithNullInput === undefined; diff --git a/packages/common/src/formatters/__tests__/collectionEditorFormatter.spec.ts b/packages/common/src/formatters/__tests__/collectionEditorFormatter.spec.ts index 26b1c5064..6a31ad5b7 100644 --- a/packages/common/src/formatters/__tests__/collectionEditorFormatter.spec.ts +++ b/packages/common/src/formatters/__tests__/collectionEditorFormatter.spec.ts @@ -2,8 +2,6 @@ import { Column } from '../../interfaces/index'; import { collectionEditorFormatter } from '../collectionEditorFormatter'; import { Editors } from '../../editors'; -jest.mock('flatpickr', () => { }); - describe('the CollectionEditor Formatter', () => { let columnDef: Column; diff --git a/packages/common/src/formatters/__tests__/collectionFormatter.spec.ts b/packages/common/src/formatters/__tests__/collectionFormatter.spec.ts index e72020e24..69a9b0ad1 100644 --- a/packages/common/src/formatters/__tests__/collectionFormatter.spec.ts +++ b/packages/common/src/formatters/__tests__/collectionFormatter.spec.ts @@ -1,8 +1,6 @@ import { Column } from '../../interfaces/index'; import { collectionFormatter } from '../collectionFormatter'; -jest.mock('flatpickr', () => { }); - describe('the Collection Formatter', () => { it('should return same output when no value is passed', () => { const valueArray = null; diff --git a/packages/common/src/interfaces/flatpickrOption.interface.ts b/packages/common/src/interfaces/flatpickrOption.interface.ts deleted file mode 100644 index 8a902964a..000000000 --- a/packages/common/src/interfaces/flatpickrOption.interface.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { Instance as FlatpickrInstance } from 'flatpickr/dist/types/instance'; -import type { Locale } from 'flatpickr/dist/types/locale'; - -export interface FlatpickrOption { - /** defaults to "F j, Y", exactly the same as date format, but for the altInput field */ - altFormat?: string; - - /** default to false, show the user a readable date (as per altFormat), but return something totally different to the server. */ - altInput?: boolean; - - /** defaults to false, allows the user to enter a date directly input the input field. By default, direct entry is disabled. */ - allowInput?: boolean; - - /** defaults to false, allows the user to input date and/or date format that might be partially invalid. */ - allowInvalidPreload?: boolean; - - /** This class will be added to the input element created by the altInput option. Note that altInput already inherits classes from the original input. */ - altInputClass?: string; - - /** Instead of body, appends the calendar to the specified node instead*. */ - appendTo?: HTMLElement; - - /** defaults to "F j, Y", defines how the date will be formatted in the aria-label for calendar days, using the same tokens as dateFormat. If you change this, you should choose a value that will make sense if a screen reader reads it out loud. */ - ariaDateFormat?: string; - - /** defaults to true, whether clicking on the input should open the picker. You could disable this if you wish to open the calendar manually with.open() */ - clickOpens?: boolean; - - /** defaults to false, closes the date picker after selecting a date */ - closeOnSelect?: boolean; - - /** defaults to "Y-m-d", a string of characters which are used to define how the date will be displayed in the input box. The supported characters are defined in the table below. */ - dateFormat?: string; - - /** - * Sets the initial selected date(s). - * If you're using mode?: "multiple" or a range calendar supply an Array of Date objects or an Array of date strings which follow your dateFormat. - * Otherwise, you can supply a single Date object or a date string. - */ - defaultDate?: string | string[] | Date | Date[]; - - /** defaults to 12, initial value of the hour element. */ - defaultHour?: number; - - /** defaults to 0, initial value of the minute element. */ - defaultMinute?: number; - - /** defaults to 0, initial value of the seconds element. */ - defaultSeconds?: number; - - /** See Disabling dates */ - disable?: any[]; - - /** - * defaults to false. - * Set disableMobile to true to always use the non-native picker. - * By default, flatpickr utilizes native datetime widgets unless certain options (e.g. disable) are used. - */ - disableMobile?: boolean; - - /** See Enabling dates */ - enable?: any[]; - - /** defaults to false, enables seconds in the time picker. */ - enableSeconds?: boolean; - - /** defaults to false, enables time picker */ - enableTime?: boolean; - - /** - * callback method when Flapickr detects an error. - * For example if a minDate is specified and setDate is called with a date that is lower than minDate it will throw an error. - */ - errorHandler?: (error: any) => void; - - /** Allows using a custom date formatting function instead of the built-in handling for date formats using dateFormat, altFormat, etc. */ - formatDate?: (dateObj: Date, format: string, locale: Locale) => string; - - /** defaults to false, do we want to hide the clear date button? */ - hideClearButton?: boolean; - - /** defaults to 1, adjusts the step for the hour input (incl. scrolling) */ - hourIncrement?: number; - - /** defaults to false, displays the calendar inline */ - inline?: boolean; - - /** provide a custom set of locales */ - locale?: any; - - /** The maximum date that a user can pick to (inclusive). */ - maxDate?: string | Date; - - /** The minimum date that a user can start picking from (inclusive). */ - minDate?: string | Date; - - /** defaults to 5, adjusts the step for the minute input (incl. scrolling) */ - minuteIncrement?: number; - - /** defaults to "dropdown", the selector type to change the month */ - monthSelectorType?: 'dropdown' | 'static'; - - /** defaults to single, what mode to use the picker */ - mode?: 'single' | 'multiple' | 'range'; - - /** defaults to ">", HTML for the arrow icon, used to switch months. */ - nextArrow?: string; - - /** defaults to false, Hides the day selection in calendar. Use it along with enableTime to create a time picker. */ - noCalendar?: boolean; - - /** defaults to false, function that expects a date string and must return a Date object */ - parseDate?: (date: Date, format: string) => void; - - /** Provide external flatpickr plugin(s) */ - plugins?: any | any[]; - - /** Where the calendar is rendered relative to the input. */ - position?: 'auto' | 'above' | 'below'; - - /** defaults to "<"", HTML for the left arrow icon. */ - prevArrow?: string; - - /** defaults to false, show the month using the shorthand version (ie, Sep instead of September). */ - shorthandCurrentMonth?: boolean; - - /** defaults to 1, number of months to show */ - showMonths?: number; - - /** defaults to false, position the calendar inside the wrapper and next to the input element*. */ - static?: boolean; - - /** defaults to 'light', use a theme of your choice. */ - theme?: 'dark' | 'light' | 'material_blue' | 'material_green' | 'material_red' | 'material_orange' | 'airbnb' | 'confetti'; - - /** defaults to false, displays time picker in 24 hour mode without AM/PM selection when enabled. */ - time_24hr?: boolean; - - /** defaults to false, enables display of week numbers in calendar. */ - weekNumbers?: boolean; - - /** defaults to false, custom elements and input groups */ - wrap?: boolean; - - // -- - // Events - // ----------------- - - /** Function(s) to trigger on every date selection. See Events API */ - onChange?: (selectedDates: Date[] | Date, dateStr: string, instance: FlatpickrInstance) => void; - - /** Function(s) to trigger on every time the calendar is closed. See Events API */ - onClose?: (selectedDates: Date[] | Date, dateStr: string, instance: FlatpickrInstance) => void; - - /** Function(s) to trigger on every time the calendar gets created. See Events API */ - onDayCreate?: (date: Date | Date[]) => void; - - /** Function(s) to trigger when the date picker gets drestroyed. See Events API */ - onDestroy?: (day: Date) => void; - - /** Function(s) to trigger when the date picker gets drestroyed. See Events API */ - onKeyDown?: (selectedDates: Date[] | Date, dateStr: string, instance: FlatpickrInstance) => void; - - /** Function(s) to trigger on every time the month changes. See Events API */ - onMonthChange?: (selectedDates: Date[] | Date, dateStr: string, instance: FlatpickrInstance) => void; - - /** Function(s) to trigger on every time the calendar is opened. See Events API */ - onOpen?: (selectedDates: Date[] | Date, dateStr: string, instance: FlatpickrInstance) => void; - - /** Function to trigger when the calendar is ready. See Events API */ - onReady?: () => void; - - /** Function(s) to trigger on every time the value input associated with the calendar get updated. See Events API */ - onValueUpdate?: (selectedDates: Date[] | Date, dateStr: string, instance: FlatpickrInstance) => void; - - /** Function(s) to trigger on every time the year changes. See Events API */ - onYearChange?: (selectedDates: Date[] | Date, dateStr: string, instance: FlatpickrInstance) => void; -} diff --git a/packages/common/src/interfaces/gridOption.interface.ts b/packages/common/src/interfaces/gridOption.interface.ts index 132adc8a9..a2d525844 100644 --- a/packages/common/src/interfaces/gridOption.interface.ts +++ b/packages/common/src/interfaces/gridOption.interface.ts @@ -20,7 +20,6 @@ import type { ExcelCopyBufferOption, ExcelExportOption, ExternalResource, - FlatpickrOption, Formatter, FormatterOption, GridMenu, @@ -42,6 +41,7 @@ import type { SliderRangeOption, TextExportOption, TreeDataOption, + VanillaCalendarOption, } from './index'; import type { ColumnReorderFunction, OperatorString, OperatorType, } from '../enums/index'; import type { TranslaterService } from '../services/translater.service'; @@ -226,7 +226,7 @@ export interface GridOption { /** * Dark Mode Theme (disabled by default, which mean light mode). * Enabling this option will add `.slick-dark-mode` CSS class to the grid parent elements - * and any other elements that are appended to the html body (e.g. Flatpickr, LongTextEditor, ...) + * and any other elements that are appended to the html body (e.g. SlickCompositeEditor, LongTextEditor, ...) */ darkMode?: boolean; @@ -274,7 +274,7 @@ export interface GridOption { autocompleter?: AutocompleterOption, /** Default option(s) to use by both the CompoundDate and/or DateRange editors */ - date?: FlatpickrOption, + date?: Partial, /** Default option(s) to use by the LongText editor */ longText?: LongTextEditorOption, @@ -292,7 +292,7 @@ export interface GridOption { autocompleter?: AutocompleterOption, /** Default option(s) to use by both the CompoundDate and/or DateRange filters */ - date?: FlatpickrOption, + date?: Partial, /** Default option(s) to use by both the CompoundSelect and/or SelectRange filters */ select?: Partial, @@ -304,7 +304,7 @@ export interface GridOption { /** The default filter model to use when none is specified (defaults to input text filter). */ defaultFilter?: any; - /** Default placeholder to use in Filters that support placeholder (autocomplete, input, flatpickr, select, ...) */ + /** Default placeholder to use in Filters that support placeholder (autocomplete, input, date picker, select, ...) */ defaultFilterPlaceholder?: string; /** Defaults to 'RangeInclusive', allows to change the default filter range operator */ diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index b496438ea..4dcd4eaf2 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -67,7 +67,6 @@ export * from './filterCallback.interface'; export * from './filterChangedArgs.interface'; export * from './filterCondition.interface'; export * from './filterConditionOption.interface'; -export * from './flatpickrOption.interface'; export * from './formatter.interface'; export * from './formatterOption.interface'; export * from './formatterResultObject.interface'; @@ -145,3 +144,4 @@ export * from './textExportOption.interface'; export * from './treeDataOption.interface'; export * from './treeToggledItem.interface'; export * from './treeToggleStateChange.interface'; +export * from './vanillaCalendarOption.interface'; diff --git a/packages/common/src/interfaces/vanillaCalendarOption.interface.ts b/packages/common/src/interfaces/vanillaCalendarOption.interface.ts new file mode 100644 index 000000000..adb402ab3 --- /dev/null +++ b/packages/common/src/interfaces/vanillaCalendarOption.interface.ts @@ -0,0 +1,8 @@ +import type { IPartialSettings } from 'vanilla-calendar-picker'; + +export interface VanillaCalendarOption extends Partial { + //-- extra options used by SlickGrid + + /** defaults to false, do we want to hide the clear date button? */ + hideClearButton?: boolean; +} diff --git a/packages/common/src/services/__tests__/backend-utilities.spec.ts b/packages/common/src/services/__tests__/backend-utilities.spec.ts index e49587036..b27ce7655 100644 --- a/packages/common/src/services/__tests__/backend-utilities.spec.ts +++ b/packages/common/src/services/__tests__/backend-utilities.spec.ts @@ -4,8 +4,6 @@ import { BackendServiceApi, GridOption } from '../../interfaces/index'; import { BackendUtilityService } from '../backendUtility.service'; import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub'; -jest.mock('flatpickr', () => { }); - const graphqlServiceMock = { buildQuery: jest.fn(), updateFilters: jest.fn(), diff --git a/packages/common/src/services/__tests__/extension.service.spec.ts b/packages/common/src/services/__tests__/extension.service.spec.ts index c5cd8326b..46fe90563 100644 --- a/packages/common/src/services/__tests__/extension.service.spec.ts +++ b/packages/common/src/services/__tests__/extension.service.spec.ts @@ -25,7 +25,6 @@ import { SlickRowSelectionModel, } from '../../extensions/index'; -jest.mock('flatpickr', () => { }); const GRID_UID = 'slickgrid_12345'; const mockCellSelectionModel = { diff --git a/packages/common/src/services/__tests__/grid.service.spec.ts b/packages/common/src/services/__tests__/grid.service.spec.ts index 726843c6d..81d207bfa 100644 --- a/packages/common/src/services/__tests__/grid.service.spec.ts +++ b/packages/common/src/services/__tests__/grid.service.spec.ts @@ -6,8 +6,6 @@ import { GridOption, CellArgs, Column, OnEventArgs } from '../../interfaces/inde import { SlickRowSelectionModel } from '../../extensions/slickRowSelectionModel'; import { type SlickDataView, SlickEvent, type SlickGrid } from '../../core/index'; -jest.mock('flatpickr', () => { }); - const mockRowSelectionModel = { constructor: jest.fn(), init: jest.fn(), @@ -23,7 +21,6 @@ const mockRowSelectionModel = { jest.mock('../../extensions/slickRowSelectionModel', () => ({ SlickRowSelectionModel: jest.fn().mockImplementation(() => mockRowSelectionModel), })); -jest.mock('flatpickr', () => { }); const filterServiceStub = { clearFilters: jest.fn(), diff --git a/packages/common/src/services/__tests__/shared.service.spec.ts b/packages/common/src/services/__tests__/shared.service.spec.ts index 894b7d0f4..8449557d0 100644 --- a/packages/common/src/services/__tests__/shared.service.spec.ts +++ b/packages/common/src/services/__tests__/shared.service.spec.ts @@ -6,8 +6,6 @@ import { ExcelExportService } from '../excelExport.service'; import type { SlickDataView, SlickGrid } from '../../core'; import { SlickGroupItemMetadataProvider } from '../../extensions'; -jest.mock('flatpickr', () => { }); - const dataviewStub = { onRowCountChanged: jest.fn(), onRowsChanged: jest.fn(), diff --git a/packages/common/src/services/__tests__/utilities.spec.ts b/packages/common/src/services/__tests__/utilities.spec.ts index 168650609..b30aefaa2 100644 --- a/packages/common/src/services/__tests__/utilities.spec.ts +++ b/packages/common/src/services/__tests__/utilities.spec.ts @@ -22,7 +22,6 @@ import { getTranslationPrefix, isColumnDateType, mapMomentDateFormatWithFieldType, - mapFlatpickrDateFormatWithFieldType, mapOperatorByFieldType, mapOperatorToShorthandDesignation, mapOperatorType, @@ -825,117 +824,6 @@ describe('Service/Utilies', () => { }); }); - describe('mapFlatpickrDateFormatWithFieldType method', () => { - it('should return a Flatpickr dateTime format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTime); - expect(output).toBe('Y-m-d H:i:S'); - }); - - it('should return a Flatpickr dateTimeShortIso format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeShortIso); - expect(output).toBe('Y-m-d H:i'); - }); - - it('should return a Flatpickr dateTimeIsoAmPm/dateTimeIsoAM_PM format', () => { - const output1 = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeIsoAmPm); - const output2 = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeIsoAM_PM); - expect(output1).toBe('Y-m-d h:i:S K'); - expect(output2).toBe('Y-m-d h:i:S K'); - }); - - it('should return a Flatpickr dateEuro format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateEuro); - expect(output).toBe('d/m/Y'); - }); - - it('should return a Flatpickr dateEuroShort format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateEuroShort); - expect(output).toBe('d/m/y'); - }); - - it('should return a Flatpickr dateTimeEuro format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeEuro); - expect(output).toBe('d/m/Y H:i:S'); - }); - - it('should return a Flatpickr dateTimeShortEuro format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeShortEuro); - expect(output).toBe('d/m/y H:i'); - }); - - it('should return a Flatpickr dateTimeEuroAmPm format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeEuroAmPm); - expect(output).toBe('d/m/Y h:i:S K'); - }); - - it('should return a Flatpickr dateTimeEuroAM_PM format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeEuroAM_PM); - expect(output).toBe('d/m/Y h:i:s K'); - }); - - it('should return a Flatpickr dateTimeEuroShort format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeEuroShort); - expect(output).toBe('d/m/y H:i:s'); - }); - - it('should return a Flatpickr dateTimeEuroShortAmPm format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeEuroShortAmPm); - expect(output).toBe('d/m/y h:i:s K'); - }); - - it('should return a Flatpickr dateUs format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateUs); - expect(output).toBe('m/d/Y'); - }); - - it('should return a Flatpickr dateUsShort format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateUsShort); - expect(output).toBe('m/d/y'); - }); - - it('should return a Flatpickr dateTimeUs format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeUs); - expect(output).toBe('m/d/Y H:i:S'); - }); - - it('should return a Flatpickr dateTimeShortUs format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeShortUs); - expect(output).toBe('m/d/y H:i'); - }); - - it('should return a Flatpickr dateTimeUsAmPm format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeUsAmPm); - expect(output).toBe('m/d/Y h:i:S K'); - }); - - it('should return a Flatpickr dateTimeUsAM_PM format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeUsAM_PM); - expect(output).toBe('m/d/Y h:i:s K'); - }); - - it('should return a Flatpickr dateTimeUsShort format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeUsShort); - expect(output).toBe('m/d/y H:i:s'); - }); - - it('should return a Flatpickr dateTimeUsShortAmPm format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateTimeUsShortAmPm); - expect(output).toBe('m/d/y h:i:s K'); - }); - - it('should return a Flatpickr dateUtc format', () => { - const output = mapFlatpickrDateFormatWithFieldType(FieldType.dateUtc); - expect(output).toBe('Z'); - }); - - it('should return a Flatpickr dateédateIso format', () => { - const output1 = mapFlatpickrDateFormatWithFieldType(FieldType.date); - const output2 = mapFlatpickrDateFormatWithFieldType(FieldType.dateIso); - expect(output1).toBe('Y-m-d'); - expect(output2).toBe('Y-m-d'); - }); - }); - describe('mapOperatorType method', () => { it('should return OperatoryType associated to "<"', () => { const expectation = OperatorType.lessThan; diff --git a/packages/common/src/services/domUtilities.ts b/packages/common/src/services/domUtilities.ts index 3c9b3672f..ec473f25c 100644 --- a/packages/common/src/services/domUtilities.ts +++ b/packages/common/src/services/domUtilities.ts @@ -130,7 +130,7 @@ export function sanitizeTextByAvailableSanitizer(gridOptions: GridOption, dirtyH if (typeof gridOptions?.sanitizer === 'function') { sanitizedText = gridOptions.sanitizer(dirtyHtml || ''); } else if (typeof DOMPurify?.sanitize === 'function') { - sanitizedText = (DOMPurify.sanitize(dirtyHtml || '', sanitizerOptions || { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }) || '').toString(); + sanitizedText = (DOMPurify.sanitize(dirtyHtml || '', sanitizerOptions || { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }) || '') as string; } return sanitizedText; diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts index 6b97af9b1..5794e82c8 100644 --- a/packages/common/src/services/utilities.ts +++ b/packages/common/src/services/utilities.ts @@ -400,6 +400,20 @@ export function isColumnDateType(fieldType: typeof FieldType[keyof typeof FieldT } } +export function formatDateByFieldType(inputDate: Date | string | typeof moment, inputFieldType: typeof FieldType[keyof typeof FieldType] | undefined, outputFieldType: typeof FieldType[keyof typeof FieldType]): string { + const inputFormat = inputFieldType ? mapMomentDateFormatWithFieldType(inputFieldType) : undefined; + const outputFormat = mapMomentDateFormatWithFieldType(outputFieldType); + const momentDate = inputDate instanceof moment ? inputDate : moment(inputDate, inputFormat); + + if (momentDate.isValid() && inputDate !== undefined) { + if (outputFieldType === FieldType.dateUtc) { + return momentDate.toISOString(); + } + return momentDate.format(outputFormat); + } + return ''; +} + /** * From a Date FieldType, return it's equivalent moment.js format * refer to moment.js for the format standard used: https://momentjs.com/docs/#/parsing/string-format/ @@ -489,109 +503,6 @@ export function mapMomentDateFormatWithFieldType(fieldType: typeof FieldType[key return map; } -/** - * From a Date FieldType, return it's equivalent Flatpickr format - * refer to Flatpickr for the format standard used: https://chmln.github.io/flatpickr/formatting/#date-formatting-tokens - * also note that they seem very similar to PHP format (except for am/pm): http://php.net/manual/en/function.date.php - * @param fieldType - */ -export function mapFlatpickrDateFormatWithFieldType(fieldType: typeof FieldType[keyof typeof FieldType]): string { - /* - d: Day of the month, 2 digits with leading zeros 01 to 31 - D: A textual representation of a day Mon through Sun - l: (lowercase 'L') A full textual representation of the day of the week Sunday through Saturday - j: Day of the month without leading zeros 1 to 31 - J: Day of the month without leading zeros and ordinal suffix 1st, 2nd, to 31st - w: Numeric representation of the day of the week 0 (for Sunday) through 6 (for Saturday) - F: A full textual representation of a month January through December - m: Numeric representation of a month, with leading zero 01 through 12 - n: Numeric representation of a month, without leading zeros 1 through 12 - M: A short textual representation of a month Jan through Dec - U: The number of seconds since the Unix Epoch 1413704993 - y: A two digit representation of a year 99 or 03 - Y: A full numeric representation of a year, 4 digits 1999 or 2003 - H: Hours (24 hours) 00 to 23 - h: Hours 1 to 12 - i: Minutes 00 to 59 - S: Seconds, 2 digits 00 to 59 - s: Seconds 0, 1 to 59 - K: AM/PM AM or PM - */ - let map: string; - switch (fieldType) { - case FieldType.dateTime: - case FieldType.dateTimeIso: - map = 'Y-m-d H:i:S'; - break; - case FieldType.dateTimeShortIso: - map = 'Y-m-d H:i'; - break; - case FieldType.dateTimeIsoAmPm: - case FieldType.dateTimeIsoAM_PM: - map = 'Y-m-d h:i:S K'; // there is no lowercase in Flatpickr :( - break; - // all Euro Formats (date/month/year) - case FieldType.dateEuro: - map = 'd/m/Y'; - break; - case FieldType.dateEuroShort: - map = 'd/m/y'; - break; - case FieldType.dateTimeEuro: - map = 'd/m/Y H:i:S'; - break; - case FieldType.dateTimeShortEuro: - map = 'd/m/y H:i'; - break; - case FieldType.dateTimeEuroAmPm: - map = 'd/m/Y h:i:S K'; // there is no lowercase in Flatpickr :( - break; - case FieldType.dateTimeEuroAM_PM: - map = 'd/m/Y h:i:s K'; - break; - case FieldType.dateTimeEuroShort: - map = 'd/m/y H:i:s'; - break; - case FieldType.dateTimeEuroShortAmPm: - map = 'd/m/y h:i:s K'; // there is no lowercase in Flatpickr :( - break; - // all US Formats (month/date/year) - case FieldType.dateUs: - map = 'm/d/Y'; - break; - case FieldType.dateUsShort: - map = 'm/d/y'; - break; - case FieldType.dateTimeUs: - map = 'm/d/Y H:i:S'; - break; - case FieldType.dateTimeShortUs: - map = 'm/d/y H:i'; - break; - case FieldType.dateTimeUsAmPm: - map = 'm/d/Y h:i:S K'; // there is no lowercase in Flatpickr :( - break; - case FieldType.dateTimeUsAM_PM: - map = 'm/d/Y h:i:s K'; - break; - case FieldType.dateTimeUsShort: - map = 'm/d/y H:i:s'; - break; - case FieldType.dateTimeUsShortAmPm: - map = 'm/d/y h:i:s K'; // there is no lowercase in Flatpickr :( - break; - case FieldType.dateUtc: - map = 'Z'; - break; - case FieldType.date: - case FieldType.dateIso: - default: - map = 'Y-m-d'; - break; - } - return map; -} - /** * Mapper for query operators (ex.: <= is "le", > is "gt") * @param string operator diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss index ea46468fe..ff39f0c68 100644 --- a/packages/common/src/styles/_variables.scss +++ b/packages/common/src/styles/_variables.scss @@ -25,9 +25,9 @@ $slick-button-primary-bg-color-disabled: #bebebe !default; $slick-button-primary-color: inherit !default; $slick-button-border-color: #c7c7c7 !default; $slick-button-style-bg-color: #fff !default; -$slick-flatpickr-bgcolor: #ffffff !default; $slick-filter-placeholder-font-family: 'Segoe UI Symbol' !default; $slick-focus-color: rgb(115, 179, 229) !default; +$slick-date-picker-bg-color: #fff !default; $slick-form-control-bg-color: #fff !default; $slick-form-control-border: 1px solid #ccc !default; @@ -66,6 +66,7 @@ $slick-table-background: transparent !default $slick-scrollbar-color: #c1c1c1 #f1f1f1 !default; $slick-hover-header-color: $slick-text-color; $slick-sorting-header-color: #333; +$slick-placeholder-color: #c9c9c9 !default; /* cell */ $slick-cell-active-border: none !default; @@ -452,7 +453,6 @@ $slick-checkbox-unchecked-opacity: 0.25 !default; /* Editors */ $slick-editor-bg-color: transparent !default; -$slick-editor-placeholder-color: #c9c9c9 !default; $slick-editor-input-border-radius: 3px !default; $slick-editor-input-disabled-color: #ececec !default; $slick-editor-input-height: 24px !default; @@ -755,7 +755,7 @@ $slick-multiselect-item-hover-border: 1px solid #d5d5d5 !d $slick-multiselect-item-line-height: calc(#{$slick-multiselect-icon-font-size} + 2px) !default; $slick-multiselect-item-padding: 2px 4px !default; $slick-multiselect-placeholder-bg-color: transparent !default; -$slick-multiselect-placeholder-color: $slick-editor-placeholder-color !default; +$slick-multiselect-placeholder-color: $slick-placeholder-color !default; $slick-multiselect-placeholder-font-family: $slick-filter-placeholder-font-family !default; $slick-multiselect-ok-button-bg-color: #fff !default; $slick-multiselect-ok-button-bg-hover-color: #f9f9f9 !default; @@ -796,7 +796,7 @@ $ms-drop-hide-radio-selected-bgcolor: unset; $ms-icon-color: $slick-icon-color; $ms-icon-size: $slick-multiselect-icon-font-size; $ms-chevron-icon-size: #{$slick-multiselect-icon-font-size + 3px}; -$ms-placeholder-color: $slick-editor-placeholder-color; +$ms-placeholder-color: $slick-placeholder-color; $ms-label-padding: $slick-multiselect-item-padding; $ms-ok-button-bg-color: $slick-multiselect-ok-button-bg-color; $ms-ok-button-bg-hover-color: $slick-multiselect-ok-button-bg-hover-color; @@ -1023,7 +1023,7 @@ $slick-empty-data-warning-z-index: 10 !default; --slick-input-group-btn-border: var(--slick-base-dark-menu-item-border); --slick-input-group-append-bg-color: #383838; --slick-compound-filter-bgcolor: var(--slick-base-dark-menu-bg-color); - --slick-flatpickr-bgcolor: var(--slick-base-dark-menu-bg-color); + --slick-date-picker-bg-color: var(--slick-base-dark-menu-bg-color); --slick-footer-left-text-color: #acacac; --slick-footer-right-text-color: #acacac; --slick-grid-header-unorderable-bg-color: #1c1c1c; @@ -1044,7 +1044,7 @@ $slick-empty-data-warning-z-index: 10 !default; --slick-editing-field-bg-color: #333333; --slick-editing-field-border: 1px solid #7c7c7c; --slick-editor-input-disabled-color: #404040; - --slick-editor-placeholder-color: #999; + --slick-placeholder-color: #999; --slick-editor-modal-default-btn-disabled-bg-color: #3f3f3f; --slick-editor-modal-default-btn-disabled-color: #5b5b5b; --slick-editor-modal-detail-container-border-modified: 1px solid #cc8400; diff --git a/packages/common/src/styles/flatpickr-dark.scss b/packages/common/src/styles/flatpickr-dark.scss deleted file mode 100644 index c21192436..000000000 --- a/packages/common/src/styles/flatpickr-dark.scss +++ /dev/null @@ -1,107 +0,0 @@ -/* Flatpickr Dark Theme - only an extract to darken the picker by using .slick-dark-mode */ -.flatpickr-calendar.slick-dark-mode { - background-color: #212121; - box-shadow: 0 1px 2px rgb(255 255 255 / 20%); - border: 1px solid #505050; - - &.arrowTop:before { - border-bottom-color: #505050; - } - &.arrowTop:after { - border-bottom-color: #212121; - } - &.arrowBottom:before { - border-top-color: #20222c; - } - &.arrowBottom:after { - border-top-color: #212121; - } - - .flatpickr-day { - color: rgba(255, 255, 255, 0.95); - &.today { - border-color: #eee; - } - } - .flatpickr-months .flatpickr-prev-month, - .flatpickr-months .flatpickr-next-month { - color: #fff; - fill: #fff; - } - .flatpickr-day.flatpickr-disabled, - .flatpickr-day.flatpickr-disabled:hover, - .flatpickr-day.prevMonthDay, - .flatpickr-day.nextMonthDay, - .flatpickr-day.notAllowed, - .flatpickr-day.notAllowed.prevMonthDay, - .flatpickr-day.notAllowed.nextMonthDay { - color: rgba(255, 255, 255, 0.3); - } - .flatpickr-current-month input.cur-year, - .flatpickr-current-month .flatpickr-monthDropdown-months { - background: #212121; - color: #fff; - } - .flatpickr-time .numInputWrapper span.arrowUp:after, - .flatpickr-current-month .numInputWrapper span.arrowUp:after { - border-bottom-color: #fff; - } - .flatpickr-time .numInputWrapper span.arrowDown:after, - .flatpickr-current-month .numInputWrapper span.arrowDown:after { - border-top-color: #fff; - } - .flatpickr-day.inRange, - .flatpickr-day.prevMonthDay.inRange, - .flatpickr-day.nextMonthDay.inRange, - .flatpickr-day.today.inRange, - .flatpickr-day.prevMonthDay.today.inRange, - .flatpickr-day.nextMonthDay.today.inRange, - .flatpickr-day:hover, - .flatpickr-day.prevMonthDay:hover, - .flatpickr-day.nextMonthDay:hover, - .flatpickr-day:focus, - .flatpickr-day.prevMonthDay:focus, - .flatpickr-day.nextMonthDay:focus { - background: rgb(100, 108, 140); - border-color: rgb(100, 108, 140); - } - - .flatpickr-day.selected { - background: rgb(128, 203, 196); - color: rgb(255, 255, 255); - border-color: rgb(128, 203, 196); - } - &.hasTime .flatpickr-time { - border-top: 1px solid #20222c; - } - .flatpickr-time .flatpickr-time-separator, - .flatpickr-time .flatpickr-am-pm, - .flatpickr-time input { - color: #fff; - } - .numInputWrapper span { - border: 1px solid rgba(255, 255, 255, 0.15); - } - .numInputWrapper:hover { - background: rgba(192, 187, 167, 0.05); - } - .numInputWrapper:hover span { - opacity: 1; - } - .numInputWrapper span:hover, - .numInputWrapper:hover, - .flatpickr-am-pm:hover, - .flatpickr-time input:hover, - .flatpickr-time .flatpickr-am-pm:hover, - .flatpickr-time input:focus, - .flatpickr-time .flatpickr-am-pm:focus, - .flatpickr-current-month .flatpickr-monthDropdown-months:hover { - background: rgba(192, 187, 167, 0.1); - } - .flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month { - background-color: #2e2e2e; - } - span.flatpickr-weekday { - color: #fff; - } -} \ No newline at end of file diff --git a/packages/common/src/styles/flatpickr.min.scss b/packages/common/src/styles/flatpickr.min.scss deleted file mode 100644 index 46c57b7cc..000000000 --- a/packages/common/src/styles/flatpickr.min.scss +++ /dev/null @@ -1,13 +0,0 @@ -.flatpickr-calendar{background:transparent;opacity:0;display:none;text-align:center;visibility:hidden;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;max-height:640px;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1);animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px);}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none !important;box-shadow:none !important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.showTimeInput.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:'';height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.rightMost:after{left:auto;right:22px}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-months{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.flatpickr-months .flatpickr-month{background:transparent;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);height:34px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{text-decoration:none;cursor:pointer;position:absolute;top:0;height:34px;padding:10px;z-index:3;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{/* - /*rtl:begin:ignore*/left:0;/* - /*rtl:end:ignore*/}/* - /*rtl:begin:ignore*/ -/* - /*rtl:end:ignore*/ -.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,.flatpickr-months .flatpickr-next-month.flatpickr-next-month{/* - /*rtl:begin:ignore*/right:0;/* - /*rtl:end:ignore*/}/* - /*rtl:begin:ignore*/ -/* - /*rtl:end:ignore*/ -.flatpickr-months .flatpickr-prev-month:hover,.flatpickr-months .flatpickr-next-month:hover{color:#959ea9;}.flatpickr-months .flatpickr-prev-month:hover svg,.flatpickr-months .flatpickr-next-month:hover svg{fill:#f64747}.flatpickr-months .flatpickr-prev-month svg,.flatpickr-months .flatpickr-next-month svg{width:14px;height:14px;}.flatpickr-months .flatpickr-prev-month svg path,.flatpickr-months .flatpickr-next-month svg path{-webkit-transition:fill .1s;transition:fill .1s;fill:inherit}.numInputWrapper{position:relative;height:auto;}.numInputWrapper input,.numInputWrapper span{display:inline-block}.numInputWrapper input{width:100%;}.numInputWrapper input::-ms-clear{display:none}.numInputWrapper input::-webkit-outer-spin-button,.numInputWrapper input::-webkit-inner-spin-button{margin:0;-webkit-appearance:none}.numInputWrapper span{position:absolute;right:0;width:14px;padding:0 4px 0 2px;height:50%;line-height:50%;opacity:0;cursor:pointer;border:1px solid rgba(57,57,57,0.15);-webkit-box-sizing:border-box;box-sizing:border-box;}.numInputWrapper span:hover{background:rgba(0,0,0,0.1)}.numInputWrapper span:active{background:rgba(0,0,0,0.2)}.numInputWrapper span:after{display:block;content:"";position:absolute}.numInputWrapper span.arrowUp{top:0;border-bottom:0;}.numInputWrapper span.arrowUp:after{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:4px solid rgba(57,57,57,0.6);top:26%}.numInputWrapper span.arrowDown{top:50%;}.numInputWrapper span.arrowDown:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(57,57,57,0.6);top:40%}.numInputWrapper span svg{width:inherit;height:auto;}.numInputWrapper span svg path{fill:rgba(0,0,0,0.5)}.numInputWrapper:hover{background:rgba(0,0,0,0.05);}.numInputWrapper:hover span{opacity:1}.flatpickr-current-month{font-size:135%;line-height:inherit;font-weight:300;color:inherit;position:absolute;width:75%;left:12.5%;padding:7.48px 0 0 0;line-height:1;height:34px;display:inline-block;text-align:center;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}.flatpickr-current-month span.cur-month{font-family:inherit;font-weight:700;color:inherit;display:inline-block;margin-left:.5ch;padding:0;}.flatpickr-current-month span.cur-month:hover{background:rgba(0,0,0,0.05)}.flatpickr-current-month .numInputWrapper{width:6ch;width:7ch\0;display:inline-block;}.flatpickr-current-month .numInputWrapper span.arrowUp:after{border-bottom-color:rgba(0,0,0,0.9)}.flatpickr-current-month .numInputWrapper span.arrowDown:after{border-top-color:rgba(0,0,0,0.9)}.flatpickr-current-month input.cur-year{background:transparent;-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;cursor:text;padding:0 0 0 .5ch;margin:0;display:inline-block;font-size:inherit;font-family:inherit;font-weight:300;line-height:inherit;height:auto;border:0;border-radius:0;vertical-align:initial;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield;}.flatpickr-current-month input.cur-year:focus{outline:0}.flatpickr-current-month input.cur-year[disabled],.flatpickr-current-month input.cur-year[disabled]:hover{font-size:100%;color:rgba(0,0,0,0.5);background:transparent;pointer-events:none}.flatpickr-current-month .flatpickr-monthDropdown-months{appearance:menulist;background:transparent;border:none;border-radius:0;box-sizing:border-box;color:inherit;cursor:pointer;font-size:inherit;font-family:inherit;font-weight:300;height:auto;line-height:inherit;margin:-1px 0 0 0;outline:none;padding:0 0 0 .5ch;position:relative;vertical-align:initial;-webkit-box-sizing:border-box;-webkit-appearance:menulist;-moz-appearance:menulist;width:auto;}.flatpickr-current-month .flatpickr-monthDropdown-months:focus,.flatpickr-current-month .flatpickr-monthDropdown-months:active{outline:none}.flatpickr-current-month .flatpickr-monthDropdown-months:hover{background:rgba(0,0,0,0.05)}.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month{background-color:transparent;outline:none;padding:0}.flatpickr-weekdays{background:transparent;text-align:center;overflow:hidden;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:28px;}.flatpickr-weekdays .flatpickr-weekdaycontainer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}span.flatpickr-weekday{cursor:default;font-size:90%;background:transparent;color:rgba(0,0,0,0.54);line-height:1;margin:0;text-align:center;display:block;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;font-weight:bolder}.dayContainer,.flatpickr-weeks{padding:1px 0 0 0}.flatpickr-days{position:relative;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;width:307.875px;}.flatpickr-days:focus{outline:0}.dayContainer{padding:0;outline:0;text-align:left;width:307.875px;min-width:307.875px;max-width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;display:inline-block;display:-ms-flexbox;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-wrap:wrap;-ms-flex-pack:justify;-webkit-justify-content:space-around;justify-content:space-around;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1;}.dayContainer + .dayContainer{-webkit-box-shadow:-1px 0 0 #e6e6e6;box-shadow:-1px 0 0 #e6e6e6}.flatpickr-day{background:none;border:1px solid transparent;border-radius:150px;-webkit-box-sizing:border-box;box-sizing:border-box;color:#393939;cursor:pointer;font-weight:400;width:14.2857143%;-webkit-flex-basis:14.2857143%;-ms-flex-preferred-size:14.2857143%;flex-basis:14.2857143%;max-width:39px;height:39px;line-height:39px;margin:0;display:inline-block;position:relative;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center;}.flatpickr-day.inRange,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.today.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day:hover,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.nextMonthDay:hover,.flatpickr-day:focus,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.nextMonthDay:focus{cursor:pointer;outline:0;background:#e6e6e6;border-color:#e6e6e6}.flatpickr-day.today{border-color:#959ea9;}.flatpickr-day.today:hover,.flatpickr-day.today:focus{border-color:#959ea9;background:#959ea9;color:#fff}.flatpickr-day.selected,.flatpickr-day.startRange,.flatpickr-day.endRange,.flatpickr-day.selected.inRange,.flatpickr-day.startRange.inRange,.flatpickr-day.endRange.inRange,.flatpickr-day.selected:focus,.flatpickr-day.startRange:focus,.flatpickr-day.endRange:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange:hover,.flatpickr-day.endRange:hover,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.endRange.nextMonthDay{background:#569ff7;-webkit-box-shadow:none;box-shadow:none;color:#fff;border-color:#569ff7}.flatpickr-day.selected.startRange,.flatpickr-day.startRange.startRange,.flatpickr-day.endRange.startRange{border-radius:50px 0 0 50px}.flatpickr-day.selected.endRange,.flatpickr-day.startRange.endRange,.flatpickr-day.endRange.endRange{border-radius:0 50px 50px 0}.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n+1)),.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n+1)),.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n+1)){-webkit-box-shadow:-10px 0 0 #569ff7;box-shadow:-10px 0 0 #569ff7}.flatpickr-day.selected.startRange.endRange,.flatpickr-day.startRange.startRange.endRange,.flatpickr-day.endRange.startRange.endRange{border-radius:50px}.flatpickr-day.inRange{border-radius:0;-webkit-box-shadow:-5px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-5px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover,.flatpickr-day.prevMonthDay,.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.notAllowed.nextMonthDay{color:rgba(57,57,57,0.3);background:transparent;border-color:transparent;cursor:default}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover{cursor:not-allowed;color:rgba(57,57,57,0.1)}.flatpickr-day.week.selected{border-radius:0;-webkit-box-shadow:-5px 0 0 #569ff7,5px 0 0 #569ff7;box-shadow:-5px 0 0 #569ff7,5px 0 0 #569ff7}.flatpickr-day.hidden{visibility:hidden}.rangeMode .flatpickr-day{margin-top:1px}.flatpickr-weekwrapper{float:left;}.flatpickr-weekwrapper .flatpickr-weeks{padding:0 12px;-webkit-box-shadow:1px 0 0 #e6e6e6;box-shadow:1px 0 0 #e6e6e6}.flatpickr-weekwrapper .flatpickr-weekday{float:none;width:100%;line-height:28px}.flatpickr-weekwrapper span.flatpickr-day,.flatpickr-weekwrapper span.flatpickr-day:hover{display:block;width:100%;max-width:none;color:rgba(57,57,57,0.3);background:transparent;cursor:default;border:none}.flatpickr-innerContainer{display:block;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;}.flatpickr-rContainer{display:inline-block;padding:0;-webkit-box-sizing:border-box;box-sizing:border-box}.flatpickr-time{text-align:center;outline:0;display:block;height:0;line-height:40px;max-height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.flatpickr-time:after{content:"";display:table;clear:both}.flatpickr-time .numInputWrapper{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;width:40%;height:40px;float:left;}.flatpickr-time .numInputWrapper span.arrowUp:after{border-bottom-color:#393939}.flatpickr-time .numInputWrapper span.arrowDown:after{border-top-color:#393939}.flatpickr-time.hasSeconds .numInputWrapper{width:26%}.flatpickr-time.time24hr .numInputWrapper{width:49%}.flatpickr-time input{background:transparent;-webkit-box-shadow:none;box-shadow:none;border:0;border-radius:0;text-align:center;margin:0;padding:0;height:inherit;line-height:inherit;color:#393939;font-size:14px;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield;}.flatpickr-time input.flatpickr-hour{font-weight:bold}.flatpickr-time input.flatpickr-minute,.flatpickr-time input.flatpickr-second{font-weight:400}.flatpickr-time input:focus{outline:0;border:0}.flatpickr-time .flatpickr-time-separator,.flatpickr-time .flatpickr-am-pm{height:inherit;float:left;line-height:inherit;color:#393939;font-weight:bold;width:2%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.flatpickr-time .flatpickr-am-pm{outline:0;width:18%;cursor:pointer;text-align:center;font-weight:400}.flatpickr-time input:hover,.flatpickr-time .flatpickr-am-pm:hover,.flatpickr-time input:focus,.flatpickr-time .flatpickr-am-pm:focus{background:#eee}.flatpickr-input[readonly]{cursor:pointer}@-webkit-keyframes fpFadeInDown{from{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}@keyframes fpFadeInDown{from{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}} \ No newline at end of file diff --git a/packages/common/src/styles/slick-editors.scss b/packages/common/src/styles/slick-editors.scss index ce309617e..e3d76f2b4 100644 --- a/packages/common/src/styles/slick-editors.scss +++ b/packages/common/src/styles/slick-editors.scss @@ -33,9 +33,12 @@ margin-left: var(--slick-text-editor-right-input-margin-left, $slick-text-editor-right-input-margin-left); } - &[readonly] { + &[readonly]:not(.date-picker) { background-color: var(--slick-text-editor-readonly-color, $slick-text-editor-readonly-color); } + &.date-picker { + cursor: pointer; + } } .slider-editor { @@ -66,7 +69,7 @@ } .autocomplete-container.input-group, - .flatpickr.input-group { + .vanilla-picker.input-group { display: flex; align-items: center; height: var(--slick-date-editor-height, $slick-date-editor-height); @@ -103,24 +106,6 @@ } } } - .flatpickr-alt-input { - cursor: pointer; - height: var(--slick-date-editor-height, $slick-date-editor-height); - border-top-left-radius: var(--slick-text-editor-border-radius, $slick-text-editor-border-radius) !important; - border-bottom-left-radius: var(--slick-text-editor-border-radius, $slick-text-editor-border-radius) !important; - padding: var(--slick-date-editor-input-padding, $slick-date-editor-input-padding); - - &.editor-text { - cursor: pointer; - background-color: var(--slick-flatpickr-bgcolor, $slick-flatpickr-bgcolor); - - &:focus { - outline: 0; - border-color: var(--slick-date-editor-focus-border-color, $slick-date-editor-focus-border-color); - box-shadow: var(--slick-date-editor-focus-box-shadow, $slick-date-editor-focus-box-shadow); - } - } - } } /* Long Text Editor */ @@ -468,7 +453,7 @@ } .autocomplete-container.input-group, - .flatpickr.input-group { + .vanilla-picker.input-group { display: flex; align-items: center; height: var(--slick-date-editor-height, $slick-date-editor-height); @@ -483,8 +468,9 @@ } } } - .flatpickr-input.form-control, .flatpickr-alt-input[readonly] { - background-color: var(--slick-flatpickr-bgcolor, $slick-flatpickr-bgcolor); + .vanilla-picker.form-control { + cursor: pointer; + background-color: var(--slick-date-picker-bg-color, $slick-date-picker-bg-color); &:disabled { background-color: var(--slick-editor-input-disabled-color, $slick-editor-input-disabled-color); cursor: initial; diff --git a/packages/common/src/styles/slick-filters.scss b/packages/common/src/styles/slick-filters.scss index a27d38266..a1aa78699 100644 --- a/packages/common/src/styles/slick-filters.scss +++ b/packages/common/src/styles/slick-filters.scss @@ -10,7 +10,7 @@ $slick-filled-filter-font-weight: 400 !default; .slick-headerrow { input.search-filter.filled, .search-filter.filled input, - .search-filter.filled input.flatpickr-input, + .search-filter.filled .date-picker input, .search-filter.filled .input-group-addon.slider-value, .search-filter.filled .input-group-addon.slider-range-value, .search-filter.filled .input-group-addon select { diff --git a/packages/common/src/styles/slick-grid.scss b/packages/common/src/styles/slick-grid.scss index 05a1d143f..be669d349 100644 --- a/packages/common/src/styles/slick-grid.scss +++ b/packages/common/src/styles/slick-grid.scss @@ -395,10 +395,6 @@ } } -.flatpickr-wrapper { - z-index: 10000; -} - .interact-placeholder { background: red !important; display: inline-block; diff --git a/packages/common/src/styles/slick-plugins.scss b/packages/common/src/styles/slick-plugins.scss index 8a6392ca8..2d6fc253a 100644 --- a/packages/common/src/styles/slick-plugins.scss +++ b/packages/common/src/styles/slick-plugins.scss @@ -820,9 +820,9 @@ input.search-filter { &.compound-input { border-radius: var(--slick-compound-filter-border-radius, $slick-compound-filter-border-radius) !important; border-left: none; - &::placeholder { - color: var(--slick-editor-placeholder-color, $slick-editor-placeholder-color); - } + } + &::placeholder { + color: var(--slick-placeholder-color, $slick-placeholder-color); } } input.compound-slider { @@ -833,7 +833,7 @@ input.search-filter { .slick-headerrow { .slick-headerrow-columns { .slick-headerrow-column { - .input-group-prepend + .flatpickr { + .input-group-prepend + .date-picker { input.compound-input { border-top-left-radius: 0; border-bottom-left-radius: 0; @@ -848,50 +848,51 @@ input.search-filter { // Date Picker Filter // ---------------------------------------------- +.vanilla-calendar { + padding: 0.9rem; + z-index: 9999; +} + .search-filter.form-group { - .input-group.flatpickr input.form-control { + .input-group.date-picker input.form-control { border-bottom-left-radius: 0px; border-top-left-radius: 0px; } } -.search-filter .flatpickr { +.search-filter .date-picker { input.form-control { border-left: none; &::placeholder { - color: var(--slick-editor-placeholder-color, $slick-editor-placeholder-color); + color: var(--slick-placeholder-color, $slick-placeholder-color); } } } -.search-filter.flatpickr, -.search-filter .flatpickr { +.search-filter .date-picker { flex: 1; cursor: pointer; - input.flatpickr.form-control, - .flatpickr-input.form-control { - background-color: var(--slick-flatpickr-bgcolor, $slick-flatpickr-bgcolor); + .date-picker input.form-control { + background-color: var(--slick-date-picker-bg-color, $slick-date-picker-bg-color); font-family: var(--slick-filter-placeholder-font-family, $slick-filter-placeholder-font-family); font-size: var(--slick-font-size-base, $slick-font-size-base); border-radius: var(--slick-compound-filter-border-radius, $slick-compound-filter-border-radius); width: 100%; &[readonly] { - background-color: var(--slick-flatpickr-bgcolor, $slick-flatpickr-bgcolor); + background-color: var(--slick-date-picker-bg-color, $slick-date-picker-bg-color); } } - .form-control[readonly], - .flatpickr.form-control[readonly] { + .form-control[readonly] { cursor: pointer; - background-color: var(--slick-flatpickr-bgcolor, $slick-flatpickr-bgcolor); + background-color: var(--slick-date-picker-bg-color, $slick-date-picker-bg-color); } } -input.flatpickr-input.form-control, -input.flatpickr.form-control { +.date-picker input.form-control { cursor: pointer; font-family: var(--slick-filter-placeholder-font-family, $slick-filter-placeholder-font-family); font-size: var(--slick-font-size-base, $slick-font-size-base); border-radius: var(--slick-date-range-filter-border-radius, $slick-date-range-filter-border-radius); &[readonly] { - background-color: var(--slick-flatpickr-bgcolor, $slick-flatpickr-bgcolor); + background-color: var(--slick-date-picker-bg-color, $slick-date-picker-bg-color); } } @@ -1137,7 +1138,7 @@ input.flatpickr.form-control { height: var(--slick-header-input-height, $slick-header-input-height); &::placeholder { - color: var(--slick-editor-placeholder-color, $slick-editor-placeholder-color); + color: var(--slick-placeholder-color, $slick-placeholder-color); } .input-group-text { diff --git a/packages/common/src/styles/slickgrid-theme-bootstrap.scss b/packages/common/src/styles/slickgrid-theme-bootstrap.scss index 06d137a59..8f5335601 100644 --- a/packages/common/src/styles/slickgrid-theme-bootstrap.scss +++ b/packages/common/src/styles/slickgrid-theme-bootstrap.scss @@ -5,7 +5,7 @@ */ /** SlickGrid Bootstrap Theme */ -@import './flatpickr-dark'; +@import 'vanilla-calendar-picker/build/vanilla-calendar.min.css'; @import './slick-grid'; @import './slick-editors'; @import './slick-plugins'; diff --git a/packages/common/src/styles/slickgrid-theme-material.bare.scss b/packages/common/src/styles/slickgrid-theme-material.bare.scss index 8667390c1..e4b737fce 100644 --- a/packages/common/src/styles/slickgrid-theme-material.bare.scss +++ b/packages/common/src/styles/slickgrid-theme-material.bare.scss @@ -9,7 +9,6 @@ // - (colors, extra-styling, slickgrid-icons, slickgrid-icons-svg-utils, slick-without-bootstrap-min-styling) /** SlickGrid Material Theme */ -@import './flatpickr-dark'; @import './sass-utilities'; @import './variables-theme-material'; @import './slick-grid'; diff --git a/packages/common/src/styles/slickgrid-theme-material.lite.scss b/packages/common/src/styles/slickgrid-theme-material.lite.scss index 3f3c28b9b..dd98a79a8 100644 --- a/packages/common/src/styles/slickgrid-theme-material.lite.scss +++ b/packages/common/src/styles/slickgrid-theme-material.lite.scss @@ -6,12 +6,11 @@ /** * SlickGrid Material Theme - * (sames as `slickgrid-theme-material.scss` but without Flatpickr & Multiple-Select styling) + * (sames as `slickgrid-theme-material.scss` but without Vanilla-Calendar & Multiple-Select styling) * We also removed `slick-without-bootstrap-min-styling.scss` since that is causing issues with Bootstrap 4 */ /** SlickGrid Material Theme */ -@import './flatpickr-dark'; @import './variables-theme-material'; @import './roboto-font'; @import './slick-grid'; diff --git a/packages/common/src/styles/slickgrid-theme-material.scss b/packages/common/src/styles/slickgrid-theme-material.scss index 5b218de6e..6dfebbccc 100644 --- a/packages/common/src/styles/slickgrid-theme-material.scss +++ b/packages/common/src/styles/slickgrid-theme-material.scss @@ -6,13 +6,11 @@ /** * SlickGrid Material Theme - * (sames as `slickgrid-theme-material.lite.scss` but includes all external 3rd party lib styling that is Flatpickr & Multiple-Select) + * sames as `slickgrid-theme-material.lite.scss` but includes all external 3rd party lib styling */ +@import 'vanilla-calendar-picker/build/vanilla-calendar.min.css'; @import './roboto-font'; -@import './flatpickr.min'; -@import './flatpickr-dark'; - @import './variables-theme-material'; @import './slick-without-bootstrap-min-styling'; @import './slick-grid'; diff --git a/packages/common/src/styles/slickgrid-theme-salesforce.bare.scss b/packages/common/src/styles/slickgrid-theme-salesforce.bare.scss index 6041b8f59..9619b3b4c 100644 --- a/packages/common/src/styles/slickgrid-theme-salesforce.bare.scss +++ b/packages/common/src/styles/slickgrid-theme-salesforce.bare.scss @@ -9,7 +9,6 @@ // - (colors, extra-styling, slickgrid-icons, slickgrid-icons-svg-utils, slick-without-bootstrap-min-styling) /** SlickGrid Salesforce Theme */ -@import './flatpickr-dark'; @import './sass-utilities'; @import './variables-theme-salesforce'; @import './slick-grid'; diff --git a/packages/common/src/styles/slickgrid-theme-salesforce.lite.scss b/packages/common/src/styles/slickgrid-theme-salesforce.lite.scss index 1c5f674d2..c61529670 100644 --- a/packages/common/src/styles/slickgrid-theme-salesforce.lite.scss +++ b/packages/common/src/styles/slickgrid-theme-salesforce.lite.scss @@ -6,11 +6,10 @@ /** * SlickGrid Salesforce Theme - * (sames as `slickgrid-theme-salesforce.scss` but without Flatpickr & Multiple-Select styling) + * (sames as `slickgrid-theme-salesforce.scss` but without Vanilla-Calendar & Multiple-Select styling) * We also removed `slick-without-bootstrap-min-styling.scss` since that is causing issues with Bootstrap 4 */ -@import './flatpickr-dark'; @import './sass-utilities'; @import './variables-theme-salesforce'; @import './slick-grid'; diff --git a/packages/common/src/styles/slickgrid-theme-salesforce.scss b/packages/common/src/styles/slickgrid-theme-salesforce.scss index d9df971b2..184434d70 100644 --- a/packages/common/src/styles/slickgrid-theme-salesforce.scss +++ b/packages/common/src/styles/slickgrid-theme-salesforce.scss @@ -6,12 +6,11 @@ /** * SlickGrid Salesforce Theme - * (sames as `slickgrid-theme-salesforce.lite.scss` but includes all external 3rd party lib styling that is Flatpickr & Multiple-Select) + * (sames as `slickgrid-theme-salesforce.lite.scss` but includes all external 3rd party lib styling) */ -@import './flatpickr.min'; -@import './flatpickr-dark'; @import './sass-utilities'; +@import 'vanilla-calendar-picker/build/vanilla-calendar.min.css'; @import './variables-theme-salesforce'; @import './slick-without-bootstrap-min-styling'; diff --git a/packages/composite-editor-component/src/compositeEditor.factory.spec.ts b/packages/composite-editor-component/src/compositeEditor.factory.spec.ts index 2c2f36a9a..0672606ca 100644 --- a/packages/composite-editor-component/src/compositeEditor.factory.spec.ts +++ b/packages/composite-editor-component/src/compositeEditor.factory.spec.ts @@ -67,7 +67,7 @@ const gridStub = { const columnsMock: Column[] = [ { id: 'productName', field: 'productName', width: 100, name: 'Product', nameKey: 'PRODUCT', editorClass: Editors.text as any }, { id: 'field2', field: 'field2', width: 75, name: 'Field 2' }, - { id: 'field3', field: 'field3', width: 75, name: 'Field 3', nameKey: 'DURATION', editorClass: Editors.date as any, columnGroup: 'Group Name', columnGroupKey: 'GROUP_NAME' }, + { id: 'field3', field: 'field3', width: 75, name: 'Field 3', nameKey: 'DURATION', editorClass: Editors.float as any, columnGroup: 'Group Name', columnGroupKey: 'GROUP_NAME' }, { id: 'zip', field: 'adress.zip', width: 75, name: 'Zip', editorClass: Editors.integer as any, columnGroup: 'Group Name', columnGroupKey: 'GROUP_NAME' } ]; const compositeEditorOptionsMock = { diff --git a/packages/vanilla-bundle/package.json b/packages/vanilla-bundle/package.json index 6ab98ccc0..5630ec222 100644 --- a/packages/vanilla-bundle/package.json +++ b/packages/vanilla-bundle/package.json @@ -59,8 +59,8 @@ "@slickgrid-universal/pagination-component": "workspace:~", "@slickgrid-universal/utils": "workspace:~", "dequal": "^2.0.3", - "flatpickr": "^4.6.13", "sortablejs": "^1.15.2", + "vanilla-calendar-picker": "^2.11.2", "whatwg-fetch": "^3.6.20" }, "devDependencies": { 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 00d437107..e88f7f315 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -1,5 +1,4 @@ import { dequal } from 'dequal/lite'; -import 'flatpickr/dist/l10n/fr'; import type { BackendServiceApi, BackendServiceOption, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58de32f84..a438321bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,9 +65,6 @@ importers: eslint-plugin-n: specifier: ^17.3.1 version: 17.3.1(eslint@9.1.1) - flatpickr: - specifier: ^4.6.13 - version: 4.6.13 husky: specifier: ^9.0.11 version: 9.0.11 @@ -122,6 +119,9 @@ importers: typescript-eslint: specifier: ^7.7.1 version: 7.7.1(eslint@9.1.1)(typescript@5.4.5) + vanilla-calendar-picker: + specifier: ^2.11.2 + version: 2.11.2 whatwg-fetch: specifier: ^3.6.20 version: 3.6.20 @@ -176,9 +176,6 @@ importers: fetch-jsonp: specifier: ^1.3.0 version: 1.3.0 - flatpickr: - specifier: ^4.6.13 - version: 4.6.13 isomorphic-dompurify: specifier: ^2.7.0 version: 2.7.0 @@ -191,6 +188,9 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.1 + vanilla-calendar-picker: + specifier: ^2.11.2 + version: 2.11.2 whatwg-fetch: specifier: ^3.6.20 version: 3.6.20 @@ -246,9 +246,6 @@ importers: excel-builder-vanilla: specifier: 3.0.1 version: 3.0.1 - flatpickr: - specifier: ^4.6.13 - version: 4.6.13 isomorphic-dompurify: specifier: ^2.7.0 version: 2.7.0 @@ -264,6 +261,9 @@ importers: un-flatten-tree: specifier: ^2.0.12 version: 2.0.12 + vanilla-calendar-picker: + specifier: ^2.11.2 + version: 2.11.2 devDependencies: autoprefixer: specifier: ^10.4.19 @@ -466,12 +466,12 @@ importers: dequal: specifier: ^2.0.3 version: 2.0.3 - flatpickr: - specifier: ^4.6.13 - version: 4.6.13 sortablejs: specifier: ^1.15.2 version: 1.15.2 + vanilla-calendar-picker: + specifier: ^2.11.2 + version: 2.11.2 whatwg-fetch: specifier: ^3.6.20 version: 3.6.20 @@ -4858,9 +4858,6 @@ packages: keyv: 4.5.4 dev: true - /flatpickr@4.6.13: - resolution: {integrity: sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==} - /flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} dev: true @@ -9168,6 +9165,9 @@ packages: builtins: 5.0.1 dev: true + /vanilla-calendar-picker@2.11.2: + resolution: {integrity: sha512-ZAlQEqcuTPVj9+vHCzaMRqVY63d4mBeyyvG3rNvzD2FatbOipiz0K4oiRYwJJgURJOeWZeDPDSieJ+vPlN4crQ==} + /verror@1.10.0: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} diff --git a/test/cypress/e2e/example07.cy.ts b/test/cypress/e2e/example07.cy.ts index 30c4eddbf..113740a5b 100644 --- a/test/cypress/e2e/example07.cy.ts +++ b/test/cypress/e2e/example07.cy.ts @@ -170,7 +170,7 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => { // change Finish date cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`).should('contain', '2009-01-05').click(); - cy.get('.flatpickr-calendar:visible .flatpickr-day').contains('22').click('bottom', { force: true }); + cy.get('.vanilla-calendar:visible .vanilla-calendar-day').contains('22').click('bottom', { force: true }); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`).should('contain', '2009-01-22'); cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') @@ -288,7 +288,7 @@ describe('Example 07 - Row Move & Checkbox Selector Selector Plugins', () => { // change Finish date cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`).should('contain', '2009-01-05').click(); - cy.get('.flatpickr-calendar:visible .flatpickr-day').contains('22').click('bottom', { force: true }); + cy.get('.vanilla-calendar:visible .vanilla-calendar-day').contains('22').click('bottom', { force: true }); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`).should('contain', '2009-01-22'); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(10)`).should('contain', 'Task 0000'); diff --git a/test/cypress/e2e/example10.cy.ts b/test/cypress/e2e/example10.cy.ts index 31e4e5bdb..110710c96 100644 --- a/test/cypress/e2e/example10.cy.ts +++ b/test/cypress/e2e/example10.cy.ts @@ -24,9 +24,6 @@ describe('Example 10 - GraphQL Grid', () => { it('should have English Text inside some of the Filters', () => { cy.get('.search-filter.filter-gender .ms-choice > span') .contains('Male'); - - cy.get('.flatpickr-input') - .should('contain.value', 'to'); // date range will contains (y to z) or in French (y au z) }); it('should have GraphQL query with defined Grid Presets', () => { @@ -47,7 +44,7 @@ describe('Example 10 - GraphQL Grid', () => { cy.get('.search-filter.filter-finish') .find('input') .invoke('val') - .then(text => expect(text).to.eq(`${presetLowestDay} to ${presetHighestDay}`)); + .then(text => expect(text).to.eq(`${presetLowestDay} — ${presetHighestDay}`)); cy.get('[data-test=alert-graphql-query]').should('exist'); cy.get('[data-test=alert-graphql-query]').should('contain', 'GraphQL Query'); @@ -327,7 +324,7 @@ describe('Example 10 - GraphQL Grid', () => { cy.get('.search-filter.filter-finish') .find('input') .invoke('val') - .then(text => expect(text).to.eq(`${presetLowestDay} to ${presetHighestDay}`)); + .then(text => expect(text).to.eq(`${presetLowestDay} — ${presetHighestDay}`)); // wait for the query to finish cy.get('[data-test=status]').should('contain', 'finished'); @@ -649,7 +646,7 @@ describe('Example 10 - GraphQL Grid', () => { cy.get('.search-filter.filter-finish') .find('input') .invoke('val') - .then(text => expect(text).to.eq(`${presetLowestDay} au ${presetHighestDay}`)); + .then(text => expect(text).to.eq(`${presetLowestDay} — ${presetHighestDay}`)); // wait for the query to finish cy.get('[data-test=status]').should('contain', 'finished!!'); @@ -675,9 +672,6 @@ describe('Example 10 - GraphQL Grid', () => { cy.get('.search-filter.filter-gender .ms-choice > span') .contains('Masculin'); - - cy.get('.flatpickr-input') - .should('contain.value', 'au'); // date range will contains (y to z) or in French (y au z) }); it('should switch locale to English', () => { diff --git a/test/cypress/e2e/example11.cy.ts b/test/cypress/e2e/example11.cy.ts index 354036e3f..501f4cfa4 100644 --- a/test/cypress/e2e/example11.cy.ts +++ b/test/cypress/e2e/example11.cy.ts @@ -132,17 +132,17 @@ describe('Example 11 - Batch Editing', () => { // change Finish date to today's date cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).should('contain', '').click(); // this date should also always be initially empty - cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).should('contain', `${currentYear}-${zeroPadding(currentMonth)}-${zeroPadding(currentDate)}`) .should('have.css', 'background-color').and('eq', UNSAVED_RGB_COLOR); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(6)`).click(); - cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(6)`).should('contain', `${currentYear}-${zeroPadding(currentMonth)}-${zeroPadding(currentDate)}`) .should('have.css', 'background-color').and('eq', UNSAVED_RGB_COLOR); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(6)`).click(); - cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(6)`).should('contain', `${currentYear}-${zeroPadding(currentMonth)}-${zeroPadding(currentDate)}`) .should('have.css', 'background-color').and('eq', UNSAVED_RGB_COLOR); @@ -156,7 +156,7 @@ describe('Example 11 - Batch Editing', () => { it('should undo last edit and expect the date editor to be opened as well when clicking the associated last undo with editor button', () => { cy.get('[data-test=undo-open-editor-btn]').click(); - cy.get('.flatpickr-calendar.open') + cy.get('.vanilla-calendar') .should('exist'); cy.get('.unsaved-editable-field') @@ -169,7 +169,7 @@ describe('Example 11 - Batch Editing', () => { it('should undo last edit and expect the date editor to NOT be opened when clicking undo last edit button', () => { cy.get('[data-test=undo-last-edit-btn]').click(); - cy.get('.flatpickr-calendar.open') + cy.get('.vanilla-calendar') .should('not.exist'); cy.get('.unsaved-editable-field') @@ -212,7 +212,7 @@ describe('Example 11 - Batch Editing', () => { .invoke('val') .then(text => expect(text).to.eq('0')); - cy.get('.search-filter.filter-finish .flatpickr-input') + cy.get('.search-filter.filter-finish .date-picker input') .invoke('val') .then(text => expect(text).to.eq('')); @@ -236,7 +236,7 @@ describe('Example 11 - Batch Editing', () => { cy.get('.search-filter.filter-finish .operator .form-control') .should('have.value', '<='); - cy.get('.search-filter.filter-finish .flatpickr-input') + cy.get('.search-filter.filter-finish .date-picker input') .invoke('val') .then(text => expect(text).to.eq(`${currentYear}-01-01`)); @@ -341,7 +341,7 @@ describe('Example 11 - Batch Editing', () => { cy.get('.search-filter.filter-finish .operator .form-control') .should('have.value', '>='); - cy.get('.search-filter.filter-finish .flatpickr-input') + cy.get('.search-filter.filter-finish .date-picker input') .invoke('val') .then(text => expect(text).to.eq(`${currentYear + 1}-01-01`)); @@ -472,7 +472,7 @@ describe('Example 11 - Batch Editing', () => { .invoke('val') .then(text => expect(text).to.eq('0')); - cy.get('.search-filter.filter-finish .flatpickr-input') + cy.get('.search-filter.filter-finish .date-picker input') .invoke('val') .then(text => expect(text).to.eq('')); @@ -520,7 +520,7 @@ describe('Example 11 - Batch Editing', () => { .invoke('val') .then(text => expect(text).to.eq('0')); - cy.get('.search-filter.filter-finish .flatpickr-input') + cy.get('.search-filter.filter-finish .date-picker input') .invoke('val') .then(text => expect(text).to.eq('')); @@ -698,7 +698,7 @@ describe('Example 11 - Batch Editing', () => { cy.get('.search-filter.filter-finish .operator .form-control') .should('have.value', '<='); - cy.get('.search-filter.filter-finish .flatpickr-input') + cy.get('.search-filter.filter-finish .date-picker input') .invoke('val') .then(text => expect(text).to.eq(`${currentYear}-01-01`)); diff --git a/test/cypress/e2e/example12.cy.ts b/test/cypress/e2e/example12.cy.ts index dbb1ae57c..b51b73409 100644 --- a/test/cypress/e2e/example12.cy.ts +++ b/test/cypress/e2e/example12.cy.ts @@ -120,10 +120,10 @@ describe('Example 12 - Composite Editor Modal', () => { it('should not be able to change the "Finish" dates on first 2 rows', () => { cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`).should('contain', '').click(); // this date should also always be initially empty - cy.get(`.flatpickr-day.today:visible`).should('not.exist'); + cy.get(`.vanilla-calendar-day__btn_today:visible`).should('not.exist'); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('contain', '').click(); // this date should also always be initially empty - cy.get(`.flatpickr-day.today:visible`).should('not.exist'); + cy.get(`.vanilla-calendar-day__btn_today:visible`).should('not.exist'); }); it('should be able to change "Completed" values of row indexes 2-4', () => { @@ -155,21 +155,21 @@ describe('Example 12 - Composite Editor Modal', () => { // change Finish date to today's date cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('contain', '').click(); // this date should also always be initially empty - cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('contain', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`) .get('.editing-field') .should('have.css', 'border') .and('contain', `solid ${UNSAVED_RGB_COLOR}`); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`).click(); - cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`).should('contain', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`) .get('.editing-field') .should('have.css', 'border') .and('contain', `solid ${UNSAVED_RGB_COLOR}`); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(8)`).click(); - cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(8)`).should('contain', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`) .get('.editing-field') .should('have.css', 'border') @@ -182,7 +182,7 @@ describe('Example 12 - Composite Editor Modal', () => { it('should undo last edit and expect the date editor to be opened as well when clicking the associated last undo with editor button', () => { cy.get('[data-test=undo-open-editor-btn]').click(); - cy.get('.flatpickr-calendar.open') + cy.get('.vanilla-calendar') .should('exist'); cy.get('.unsaved-editable-field') @@ -198,7 +198,7 @@ describe('Example 12 - Composite Editor Modal', () => { it('should undo last edit and expect the date editor to NOT be opened when clicking undo last edit button', () => { cy.get('[data-test=undo-last-edit-btn]').click(); - cy.get('.flatpickr-calendar.open') + cy.get('.vanilla-calendar') .should('not.exist'); cy.get('.unsaved-editable-field') @@ -246,8 +246,9 @@ describe('Example 12 - Composite Editor Modal', () => { cy.get('.editor-checkbox').should('be.not.checked'); cy.get('.item-details-container.editor-product .autocomplete').should('be.empty'); cy.get('.item-details-container.editor-duration .editor-text').should('be.empty'); - cy.get('.item-details-container.editor-start .flatpickr-alt-input').should('be.empty'); - cy.get('.item-details-container.editor-finish .flatpickr-alt-input').should('be.empty').should('be.disabled'); + cy.get('.item-details-container.editor-start input.date-picker').invoke('val').should('be.empty'); + cy.get('.item-details-container.editor-finish input.date-picker').invoke('val').should('be.empty'); + cy.get('.item-details-container.editor-finish input.date-picker').should('be.disabled'); cy.get('.item-details-container.editor-origin .autocomplete').should('be.empty'); }); @@ -332,8 +333,8 @@ describe('Example 12 - Composite Editor Modal', () => { cy.get('.item-details-container.editor-duration .modified').should('have.length', 1); cy.get('.item-details-container.editor-finish > .item-details-validation').contains('* You must provide a "Finish" date when "Completed" is checked.'); - cy.get('.item-details-container.editor-finish .flatpickr-alt-input').click({ force: true }); - cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); + cy.get('.item-details-container.editor-finish input.date-picker').click({ force: true }); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').type('c'); @@ -418,8 +419,8 @@ describe('Example 12 - Composite Editor Modal', () => { cy.get('.item-details-container.editor-complexity .modified').should('have.length', 1); cy.get('.item-details-container.editor-finish > .item-details-validation').contains('* You must provide a "Finish" date when "Completed" is checked.'); - cy.get('.item-details-container.editor-finish .flatpickr').click().click(); - cy.get(`.flatpickr-day.today:visible`).click(); + cy.get('.item-details-container.editor-finish .date-picker').click().click(); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click(); cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').type('bel'); @@ -478,8 +479,8 @@ describe('Example 12 - Composite Editor Modal', () => { cy.get('.item-details-container.editor-complexity .modified').should('have.length', 1); cy.get('.item-details-container.editor-finish > .item-details-validation').contains('* You must provide a "Finish" date when "Completed" is checked.'); - cy.get('.item-details-container.editor-finish .flatpickr').click().click(); - cy.get(`.flatpickr-day.today:visible`).click(); + cy.get('.item-details-container.editor-finish .date-picker').click().click(); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click(); cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').type('bel'); @@ -530,8 +531,8 @@ describe('Example 12 - Composite Editor Modal', () => { cy.get('.item-details-container.editor-completed .modified').should('have.length', 1); cy.get('.item-details-container.editor-finish > .item-details-validation').contains('* You must provide a "Finish" date when "Completed" is checked.'); - cy.get('.item-details-container.editor-finish .flatpickr-alt-input').click({ force: true }); - cy.get(`.flatpickr-day.today:visible`).click('bottom', { force: true }); + cy.get('.item-details-container.editor-finish input.date-picker').click({ force: true }); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').type('ze'); @@ -600,7 +601,7 @@ describe('Example 12 - Composite Editor Modal', () => { cy.get('.item-details-container.editor-completed input.editor-checkbox:checked').should('have.length', 1); cy.get('.item-details-container.editor-completed .modified').should('have.length', 1); - cy.get('.item-details-container.editor-finish .flatpickr-alt-input').should('contain.value', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`); + cy.get('.item-details-container.editor-finish input.date-picker').should('contain.value', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`); cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); cy.get('.btn-cancel').click(); diff --git a/test/cypress/e2e/example16.cy.ts b/test/cypress/e2e/example16.cy.ts index 0f1100874..7e4390160 100644 --- a/test/cypress/e2e/example16.cy.ts +++ b/test/cypress/e2e/example16.cy.ts @@ -69,10 +69,10 @@ describe('Example 16 - Regular & Custom Tooltips', () => { }); it('should mouse over Task 6 cell on "Start" column and expect a delayed tooltip opening via async process', () => { + cy.get('.slick-custom-tooltip').should('not.exist'); cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`).as('start6-cell'); cy.get('@start6-cell').contains(/\d{4}-\d{2}-\d{2}$/); // use regexp to make sure it's a number cy.get('@start6-cell').trigger('mouseover'); - cy.get('.slick-custom-tooltip').should('not.exist'); cy.wait(10); cy.get('.slick-custom-tooltip').should('be.visible'); @@ -145,7 +145,7 @@ describe('Example 16 - Regular & Custom Tooltips', () => { cy.get('@percentage-cell').trigger('mouseover'); cy.get('.slick-custom-tooltip').should('be.visible'); - cy.get('.slick-custom-tooltip').contains(/\d+\%$/); + cy.get('.slick-custom-tooltip').contains(/\d+%$/); cy.get('@percentage-cell').trigger('mouseout'); }); diff --git a/test/jest-global-mocks.ts b/test/jest-global-mocks.ts index 266447372..70dd0af79 100644 --- a/test/jest-global-mocks.ts +++ b/test/jest-global-mocks.ts @@ -26,11 +26,21 @@ Object.defineProperty(window, 'getComputedStyle', { }) }); -// Jest has a hard time with Flatpickr/MomentJS because they export as default, to bypass this problem we can mock the require .default -jest.mock('flatpickr', () => { - const actual = jest.requireActual('flatpickr'); - return { __esModule: true, ...actual, default: actual }; +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), }); + +// Jest has a hard time with MomentJS because they export as default, to bypass this problem we can mock the require .default jest.mock('moment-mini', () => { const actual = jest.requireActual('moment-mini'); return { __esModule: true, ...actual, default: actual };