From 76c66a3ec2da4b1ff1b296851f46bf58967adc18 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Mon, 30 Mar 2020 15:57:35 -0400 Subject: [PATCH] feat(filters): add missing Date Filters --- README.md | 25 +- packages/common/package.json | 12 +- .../__tests__/compoundDateFilter.spec.ts | 320 +++++++++++++++++ .../filters/__tests__/dateRangeFilter.spec.ts | 309 +++++++++++++++++ .../common/src/filters/compoundDateFilter.ts | 328 ++++++++++++++++++ .../common/src/filters/dateRangeFilter.ts | 304 ++++++++++++++++ packages/common/src/filters/index.ts | 10 +- .../__tests__/collection.service.spec.ts | 2 +- .../styles/se-slickgrid-theme-material.scss | 109 ++++++ packages/common/src/styles/slick-plugins.scss | 1 - .../src/styles/slickgrid-theme-material.scss | 47 +-- packages/common/tsconfig.json | 1 + .../vanilla-bundle/src/vanilla-grid-bundle.ts | 3 - packages/web-demo-vanilla-bundle/package.json | 6 +- .../src/examples/example02.ts | 12 +- .../src/examples/example03.ts | 6 +- packages/web-demo-vanilla-bundle/src/main.ts | 9 +- .../src/se-styles.scss | 2 + .../web-demo-vanilla-bundle/webpack.config.js | 2 +- 19 files changed, 1411 insertions(+), 97 deletions(-) create mode 100644 packages/common/src/filters/__tests__/compoundDateFilter.spec.ts create mode 100644 packages/common/src/filters/__tests__/dateRangeFilter.spec.ts create mode 100644 packages/common/src/filters/compoundDateFilter.ts create mode 100644 packages/common/src/filters/dateRangeFilter.ts create mode 100644 packages/common/src/styles/se-slickgrid-theme-material.scss create mode 100644 packages/web-demo-vanilla-bundle/src/se-styles.scss diff --git a/README.md b/README.md index 99132d563..a781cd774 100644 --- a/README.md +++ b/README.md @@ -71,29 +71,8 @@ npm run test:watch ## TODO #### Code - [x] Aggregators (6) -- [ ] Editors - - [x] Autocomplete - - [x] Checkbox - - [ ] Date - - [x] Float - - [x] Integer - - [x] Long Text - - [x] Multiple Select - - [x] Single Select - - [x] Slider - - [x] Text -- [ ] Filters - - [x] Autocomplete - - [ ] Compound Date - - [x] Compound Input(s) - - [x] Compound Slider - - [ ] Date Range - - [x] Input(s) - - [x] Multiple Select - - [x] Single Select - - [x] Native Select - - [x] Slider - - [x] Slider Range +- [x] Editors (11) +- [x] Filters (17) - [x] Formatters (31) - [ ] Extensions - [x] AutoTooltip diff --git a/packages/common/package.json b/packages/common/package.json index 3cd8019ca..268754aa0 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -33,6 +33,8 @@ "postsass-build-task:scss-compile:bootstrap": "postcss --use autoprefixer --output dist/styles/css/slickgrid-theme-bootstrap.css dist/styles/css/slickgrid-theme-bootstrap.css", "sass-build-task:scss-compile:material": "node-sass --source-map true src/styles/slickgrid-theme-material.scss -o dist/styles/css", "postsass-build-task:scss-compile:material": "postcss --use autoprefixer --output dist/styles/css/slickgrid-theme-material.css dist/styles/css/slickgrid-theme-material.css", + "sass-build-task:scss-compile:se-material": "node-sass --source-map true src/styles/se-slickgrid-theme-material.scss -o dist/styles/css", + "postsass-build-task:scss-compile:se-material": "postcss --use autoprefixer --output dist/styles/css/se-slickgrid-theme-material.css dist/styles/css/se-slickgrid-theme-material.css", "sass:build": "run-p sass-build-task:scss-compile:*", "sass:copy": "cross-env copyfiles -f src/styles/*.scss dist/styles/sass", "presass:watch:bootstrap": "cross-env npm run sass-build-task:scss-compile:bootstrap", @@ -70,12 +72,12 @@ "autoprefixer": "^9.7.5", "copyfiles": "^2.2.0", "cross-env": "^7.0.2", - "jest": "^25.2.3", - "jest-cli": "^25.2.3", - "jest-environment-jsdom": "^25.2.3", + "jest": "^25.2.4", + "jest-cli": "^25.2.4", + "jest-environment-jsdom": "^25.2.4", "jest-extended": "^0.11.5", "jest-junit": "^10.0.0", - "jsdom": "^16.2.1", + "jsdom": "^16.2.2", "jsdom-global": "^3.0.2", "mini-css-extract-plugin": "^0.9.0", "node-sass": "4.13.1", @@ -83,7 +85,7 @@ "npm-run-all": "^4.1.5", "postcss-cli": "^7.1.0", "rimraf": "^3.0.2", - "ts-jest": "^25.2.1", + "ts-jest": "^25.3.0", "typescript": "^3.8.3" }, "engines": { diff --git a/packages/common/src/filters/__tests__/compoundDateFilter.spec.ts b/packages/common/src/filters/__tests__/compoundDateFilter.spec.ts new file mode 100644 index 000000000..5d4f40a61 --- /dev/null +++ b/packages/common/src/filters/__tests__/compoundDateFilter.spec.ts @@ -0,0 +1,320 @@ +import 'jest-extended'; + +import { Filters } from '..'; +import { FieldType, OperatorType } from '../../enums/index'; +import { Column, FilterArguments, GridOption } from '../../interfaces/index'; +import { CompoundDateFilter } from '../compoundDateFilter'; +import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; + +const containerId = 'demo-container'; + +// define a
container to simulate the grid container +const template = `
`; + +const gridOptionMock = { + enableFiltering: true, + enableFilterTrimWhiteSpace: true, +} as GridOption; + +const gridStub = { + getOptions: () => gridOptionMock, + getColumns: jest.fn(), + getHeaderRowColumn: jest.fn(), + render: jest.fn(), +}; + +describe('CompoundDateFilter', () => { + let divContainer: HTMLDivElement; + let filter: CompoundDateFilter; + let filterArguments: FilterArguments; + let spyGetHeaderRow; + let mockColumn: Column; + let translateService: TranslateServiceStub; + + beforeEach(() => { + translateService = new TranslateServiceStub(); + + divContainer = document.createElement('div'); + divContainer.innerHTML = template; + document.body.appendChild(divContainer); + spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer); + + mockColumn = { id: 'finish', field: 'finish', filterable: true, outputType: FieldType.dateIso, filter: { model: Filters.compoundDate, operator: '>' } }; + + filterArguments = { + grid: gridStub, + columnDef: mockColumn, + callback: jest.fn() + }; + + filter = new CompoundDateFilter(translateService); + }); + + afterEach(() => { + filter.destroy(); + }); + + it('should throw an error when trying to call init without any arguments', () => { + expect(() => filter.init(null)).toThrowError('[Slickgrid-Universal] A filter must always have an "init()" with valid arguments.'); + }); + + it('should initialize the filter', () => { + filter.init(filterArguments); + const filterCount = divContainer.querySelectorAll('.form-group.search-filter.filter-finish').length; + + expect(spyGetHeaderRow).toHaveBeenCalled(); + expect(filterCount).toBe(1); + }); + + it('should have a placeholder when defined in its column definition', () => { + const testValue = 'test placeholder'; + mockColumn.filter.placeholder = testValue; + + filter.init(filterArguments); + const filterElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.flatpickr'); + + 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'); + filter.hide(); + + expect(calendarElm).toBeTruthy(); + expect(spy).toHaveBeenCalled(); + }); + + 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'); + filter.show(); + + expect(calendarElm).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: '', + locale: 'en', + onChange: expect.anything(), + wrap: true, + }); + }); + + it('should be able to call "setValues" and have that value set in the picker', () => { + const mockDate = '2001-01-02T16:02:02.239Z'; + filter.init(filterArguments); + filter.setValues(mockDate); + expect(filter.currentDate).toEqual(mockDate); + }); + + it('should be able to call "setValues" as an array and have that value set in the picker', () => { + const mockDate = '2001-01-02T16:02:02.239Z'; + filter.init(filterArguments); + filter.setValues([mockDate]); + expect(filter.currentDate).toEqual(mockDate); + }); + + it('should be able to call "setValues" with a value and an extra operator and expect it to be set as new operator', () => { + const mockDate = '2001-01-02T16:02:02.239Z'; + filter.init(filterArguments); + filter.setValues([mockDate], OperatorType.greaterThanOrEqual); + + const filterOperatorElm = divContainer.querySelector('.input-group-prepend.operator select'); + + expect(filter.currentDate).toEqual(mockDate); + expect(filterOperatorElm.value).toBe('>='); + }); + + 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.flatpickr'); + filterInputElm.value = '2001-01-02T16:02:02.239Z'; + 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(spyCallback).toHaveBeenCalledWith(undefined, { + columnDef: mockColumn, operator: '>', searchTerms: ['2001-01-02'], shouldTriggerQuery: true + }); + }); + + 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.flatpickr'); + filterInputElm.value = '2001-01-02T16:02:02.239Z'; + 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(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: '>', searchTerms: ['2001-01-02'], 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']; + mockColumn.filter.operator = '<='; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.flatpickr'); + + filterInputElm.focus(); + filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); + + expect(filterFilledElms.length).toBe(1); + expect(filter.currentDate).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 }); + }); + + it('should trigger an operator change event and expect the callback to be called with the searchTerms and operator defined', () => { + filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z']; + mockColumn.filter.operator = '>'; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterSelectElm = divContainer.querySelector('.search-filter.filter-finish select'); + + filterSelectElm.value = '<='; + filterSelectElm.dispatchEvent(new CustomEvent('change')); + const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); + + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['2000-01-01T05:00:00.000Z'], shouldTriggerQuery: true }); + }); + + it('should work with different locale when locale is changed', () => { + translateService.setLocale('fr-CA'); // will be trimmed to "fr" + filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z']; + mockColumn.filter.operator = '<='; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.flatpickr'); + const calendarElm = document.body.querySelector('.flatpickr-calendar'); + 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 })); + const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); + + expect(filterFilledElms.length).toBe(1); + expect(filter.currentDate).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(calendarElm).toBeTruthy(); + expect(selectonOptionElms.length).toBe(12); + expect(selectonOptionElms[0].textContent).toBe('janvier'); + }); + + it('should throw an error and use English locale when user tries to load an unsupported Flatpickr locale', () => { + translateService.setLocale('zx'); + const consoleSpy = jest.spyOn(global.console, 'warn').mockReturnValue(); + + filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z']; + mockColumn.filter.operator = '<='; + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.flatpickr'); + const calendarElm = document.body.querySelector('.flatpickr-calendar'); + 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 })); + + expect(consoleSpy).toHaveBeenCalledWith(expect.toInclude('[Slickgrid-Universal - CompoundDate Filter] It seems that "zx" is not a locale supported by Flatpickr')); + expect(selectonOptionElms.length).toBe(12); + expect(selectonOptionElms[0].textContent).toBe('January'); + }); + + it('should trigger a callback with the clear filter set when calling the "clear" method', () => { + filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z']; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.clear(); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.flatpickr'); + 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 }); + }); + + 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-01T05:00:00.000Z']; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.clear(false); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.flatpickr'); + 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 }); + }); + + 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.flatpickr'); + 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.currentDate.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 using no "outputType" which will default to UTC date', () => { + mockColumn.outputType = null; + filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z']; + mockColumn.filter.operator = '<='; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('.search-filter.filter-finish .flatpickr input.flatpickr'); + + filterInputElm.focus(); + filterInputElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + const filterFilledElms = divContainer.querySelectorAll('.form-group.search-filter.filter-finish.filled'); + + expect(filterFilledElms.length).toBe(1); + expect(filter.currentDate).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 }); + }); +}); diff --git a/packages/common/src/filters/__tests__/dateRangeFilter.spec.ts b/packages/common/src/filters/__tests__/dateRangeFilter.spec.ts new file mode 100644 index 000000000..f5c7a303f --- /dev/null +++ b/packages/common/src/filters/__tests__/dateRangeFilter.spec.ts @@ -0,0 +1,309 @@ +import 'jest-extended'; + +import { FieldType } from '../../enums/index'; +import { Column, FilterArguments, GridOption } from '../../interfaces/index'; +import { Filters } from '..'; +import { DateRangeFilter } from '../dateRangeFilter'; +import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; + +const containerId = 'demo-container'; + +// define a
container to simulate the grid container +const template = `
`; + +const gridOptionMock = { + enableFiltering: true, + enableFilterTrimWhiteSpace: true, +} as GridOption; + +const gridStub = { + getOptions: () => gridOptionMock, + getColumns: jest.fn(), + getHeaderRowColumn: jest.fn(), + render: jest.fn(), +}; + +describe('DateRangeFilter', () => { + let divContainer: HTMLDivElement; + let filter: DateRangeFilter; + let filterArguments: FilterArguments; + let spyGetHeaderRow; + let mockColumn: Column; + let translateService: TranslateServiceStub; + + beforeEach(() => { + translateService = new TranslateServiceStub(); + + divContainer = document.createElement('div'); + divContainer.innerHTML = template; + document.body.appendChild(divContainer); + spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer); + + mockColumn = { id: 'finish', field: 'finish', filterable: true, filter: { model: Filters.dateRange, operator: 'RangeInclusive' } }; + filterArguments = { + grid: gridStub, + columnDef: mockColumn, + callback: jest.fn() + }; + + filter = new DateRangeFilter(translateService); + }); + + afterEach(() => { + filter.destroy(); + }); + + it('should throw an error when trying to call init without any arguments', () => { + expect(() => filter.init(null)).toThrowError('[Slickgrid-Universal] A filter must always have an "init()" with valid arguments.'); + }); + + it('should initialize the filter', () => { + filter.init(filterArguments); + const filterCount = divContainer.querySelectorAll('input.flatpickr.search-filter.filter-finish').length; + + expect(spyGetHeaderRow).toHaveBeenCalled(); + expect(filterCount).toBe(1); + }); + + it('should have a placeholder when defined in its column definition', () => { + const testValue = 'test placeholder'; + mockColumn.filter.placeholder = testValue; + + filter.init(filterArguments); + const filterElm = divContainer.querySelector('.flatpickr.search-filter.filter-finish input'); + + 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'); + filter.hide(); + + expect(calendarElm).toBeTruthy(); + expect(spy).toHaveBeenCalled(); + }); + + 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'); + filter.show(); + + expect(calendarElm).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: 'Z', + altInput: true, + closeOnSelect: true, + dateFormat: 'Y-m-d', + defaultDate: [], + enableTime: true, + locale: 'en', + mode: 'range', + onChange: expect.anything(), + wrap: true, + }); + }); + + it('should be able to call "setValues" and have them set in the picker', () => { + const mockDates = ['2001-01-02T16:02:02.239Z', '2001-01-31T16:02:02.239Z']; + filter.init(filterArguments); + filter.setValues(mockDates); + expect(filter.currentDates).toEqual(mockDates); + }); + + it('should be able to call "setValues" with 2 dots (..) notation and have them set in the picker', () => { + const mockDate = '2001-01-02T16:02:02.239Z..2001-01-31T16:02:02.239Z'; + filter.init(filterArguments); + filter.setValues([mockDate]); + expect(filter.currentDates).toEqual(mockDate.split('..')); + }); + + 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'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('input.flatpickr.search-filter.filter-finish'); + 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'); + + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: ['2001-01-02', '2001-01-31'], shouldTriggerQuery: true }); + }); + + 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'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('input.flatpickr.search-filter.filter-finish'); + 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'); + + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeExclusive', searchTerms: ['2001-01-02', '2001-01-31'], 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']; + mockColumn.filter.operator = 'RangeInclusive'; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('input.flatpickr.search-filter.filter-finish'); + + 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'); + + 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 }); + }); + + 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']; + mockColumn.filter.operator = 'RangeInclusive'; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('input.flatpickr.search-filter.filter-finish'); + + 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'); + + 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 }); + }); + + it('should work with different locale when locale is changed', () => { + translateService.setLocale('fr-CA'); // will be trimmed to "fr" + filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']; + mockColumn.filter.operator = 'RangeInclusive'; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('input.flatpickr.search-filter.filter-finish'); + const calendarElm = document.body.querySelector('.flatpickr-calendar'); + 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 })); + const filterFilledElms = divContainer.querySelectorAll('.flatpickr.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(calendarElm).toBeTruthy(); + expect(selectonOptionElms.length).toBe(12); + expect(selectonOptionElms[0].textContent).toBe('janvier'); + }); + + it('should throw an error and use English locale when user tries to load an unsupported Flatpickr locale', () => { + translateService.setLocale('zx'); + const consoleSpy = jest.spyOn(global.console, 'warn').mockReturnValue(); + + 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('input.flatpickr.search-filter.filter-finish'); + const calendarElm = document.body.querySelector('.flatpickr-calendar'); + 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 })); + + expect(consoleSpy).toHaveBeenCalledWith(expect.toInclude('[Slickgrid-Universal - DateRange Filter] It seems that "zx" is not a locale supported by Flatpickr')); + expect(selectonOptionElms.length).toBe(12); + expect(selectonOptionElms[0].textContent).toBe('January'); + }); + + it('should trigger a callback with the clear filter set when calling the "clear" method', () => { + filterArguments.searchTerms = ['2000-01-01', '2000-01-31']; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.clear(); + const filterInputElm = divContainer.querySelector('input.flatpickr.search-filter.filter-finish'); + const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + + expect(filterInputElm.value).toBe(''); + expect(filterFilledElms.length).toBe(0); + 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', () => { + filterArguments.searchTerms = ['2000-01-01', '2000-01-31']; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.clear(false); + const filterInputElm = divContainer.querySelector('input.flatpickr.search-filter.filter-finish'); + const filterFilledElms = divContainer.querySelectorAll('.flatpickr.search-filter.filter-finish.filled'); + + expect(filterInputElm.value).toBe(''); + expect(filterFilledElms.length).toBe(0); + 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('input.flatpickr.search-filter.filter-finish'); + 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'); + + expect(filterFilledElms.length).toBe(1); + // expect(filter.currentDates.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 + }); + }); + + it('should have a value with date & time in the picker when using no "outputType" which will default to UTC date', () => { + mockColumn.outputType = null; + filterArguments.searchTerms = ['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']; + mockColumn.filter.operator = '<='; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterInputElm = divContainer.querySelector('input.flatpickr.search-filter.filter-finish'); + + 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'); + + expect(filterFilledElms.length).toBe(1); + expect(filter.currentDates).toEqual(['2000-01-01T05:00:00.000Z', '2000-01-31T05:00:00.000Z']); + 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: '<=', searchTerms: ['2000-01-01', '2000-01-31'], shouldTriggerQuery: true }); + }); +}); diff --git a/packages/common/src/filters/compoundDateFilter.ts b/packages/common/src/filters/compoundDateFilter.ts new file mode 100644 index 000000000..ea13c876d --- /dev/null +++ b/packages/common/src/filters/compoundDateFilter.ts @@ -0,0 +1,328 @@ +import * as flatpickr from 'flatpickr'; + +import { + FieldType, + OperatorString, + OperatorType, + SearchTerm, +} from '../enums/index'; +import { + Column, + ColumnFilter, + Filter, + FilterArguments, + FilterCallback, + FlatpickrOption, + GridOption, +} from '../interfaces/index'; +import { mapFlatpickrDateFormatWithFieldType, mapOperatorToShorthandDesignation } from '../services/utilities'; +import { TranslaterService } from '../services/translater.service'; + +declare function require(name: string): any; +declare function require(name: string[], loadedFile: any): any; + +// using external non-typed js libraries +declare const $: any; + +export class CompoundDateFilter implements Filter { + private _clearFilterTriggered = false; + private _currentDate: Date | undefined; + private _flatpickrOptions: FlatpickrOption; + private _shouldTriggerQuery = true; + private $filterElm: any; + private $filterInputElm: any; + private $selectOperatorElm: any; + private _currentValue: string; + private _operator: OperatorType | OperatorString; + flatInstance: any; + grid: any; + searchTerms: SearchTerm[]; + columnDef: Column; + callback: FilterCallback; + + constructor(private translaterService: TranslaterService) { } + + /** Getter for the Grid Options pulled through the Grid Object */ + private get gridOptions(): GridOption { + return (this.grid && this.grid.getOptions) ? this.grid.getOptions() : {}; + } + + /** Getter for the Filter Operator */ + get columnFilter(): ColumnFilter { + return this.columnDef && this.columnDef.filter || {}; + } + + /** Getter for the Current Dates selected */ + get currentDate(): Date | undefined { + return this._currentDate; + } + + /** Getter to know what would be the default operator when none is specified */ + get defaultOperator(): OperatorType | OperatorString { + return OperatorType.empty; + } + + /** Getter for the Flatpickr Options */ + get flatpickrOptions(): FlatpickrOption { + return this._flatpickrOptions || {}; + } + + /** Getter for the Filter Operator */ + get operator(): OperatorType | OperatorString { + return this._operator || this.columnFilter.operator || this.defaultOperator; + } + + /** Setter for the Filter Operator */ + set operator(op: OperatorType | OperatorString) { + this._operator = op; + } + + /** + * Initialize the Filter + */ + init(args: FilterArguments) { + if (!args) { + throw new Error('[Slickgrid-Universal] A filter must always have an "init()" with valid arguments.'); + } + this.grid = args.grid; + this.callback = args.callback; + this.columnDef = args.columnDef; + this.operator = args.operator || ''; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; + + // date input can only have 1 search term, so we will use the 1st array index if it exist + const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms.length >= 0) ? this.searchTerms[0] : ''; + + // step 1, create the DOM Element of the filter which contain the compound Operator+Input + // and initialize it if searchTerm is filled + this.$filterElm = this.createDomElement(searchTerm); + + // step 3, subscribe to the keyup event and run the callback when that happens + // also add/remove "filled" class for styling purposes + this.$filterInputElm.keyup((e: any) => { + this.onTriggerEvent(e); + }); + this.$selectOperatorElm.change((e: any) => { + this.onTriggerEvent(e); + }); + } + + /** + * Clear the filter value + */ + clear(shouldTriggerQuery = true) { + if (this.flatInstance && this.$selectOperatorElm) { + this._clearFilterTriggered = true; + this._shouldTriggerQuery = shouldTriggerQuery; + this.searchTerms = []; + this.$selectOperatorElm.val(0); + this.flatInstance.clear(); + } + } + + /** + * destroy the filter + */ + destroy() { + if (this.$filterElm) { + this.$filterElm.off('keyup').remove(); + } + if (this.flatInstance && typeof this.flatInstance.destroy === 'function') { + this.flatInstance.destroy(); + } + } + + hide() { + if (this.flatInstance && typeof this.flatInstance.close === 'function') { + this.flatInstance.close(); + } + } + + show() { + if (this.flatInstance && typeof this.flatInstance.open === 'function') { + this.flatInstance.open(); + } + } + + /** Set value(s) in the DOM element, we can optionally pass an operator and/or trigger a change event */ + setValues(values: SearchTerm | SearchTerm[], operator?: OperatorType | OperatorString) { + if (this.flatInstance && values) { + const newValue = Array.isArray(values) ? values[0] : values; + this._currentDate = newValue as Date; + this.flatInstance.setDate(newValue); + } + + // set the operator, in the DOM as well, when defined + this.operator = operator || this.defaultOperator; + if (operator && this.$selectOperatorElm) { + const operatorShorthand = mapOperatorToShorthandDesignation(this.operator); + this.$selectOperatorElm.val(operatorShorthand); + } + } + + // + // private functions + // ------------------ + + private buildDatePickerInput(searchTerm?: SearchTerm) { + const columnId = this.columnDef && this.columnDef.id; + const inputFormat = mapFlatpickrDateFormatWithFieldType(this.columnDef.type || FieldType.dateIso); + const outputFormat = mapFlatpickrDateFormatWithFieldType(this.columnDef.outputType || this.columnDef.type || FieldType.dateUtc); + const userFilterOptions = (this.columnFilter && this.columnFilter.filterOptions || {}) as FlatpickrOption; + + // 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 && userFilterOptions.locale) || (this.translaterService && this.translaterService.getCurrentLocale && this.translaterService.getCurrentLocale()) || this.gridOptions.locale || 'en'; + if (currentLocale && currentLocale.length > 2) { + currentLocale = currentLocale.substring(0, 2); + } + + // if we are preloading searchTerms, we'll keep them for reference + if (searchTerm) { + this._currentDate = searchTerm as Date; + } + + const pickerOptions: FlatpickrOption = { + defaultDate: (searchTerm as string) || '', + altInput: true, + altFormat: outputFormat, + dateFormat: inputFormat, + wrap: true, + closeOnSelect: true, + locale: (currentLocale !== 'en') ? this.loadFlatpickrLocale(currentLocale) : 'en', + onChange: (selectedDates: Date[] | Date, dateStr: string, instance: any) => { + this._currentValue = dateStr; + this._currentDate = Array.isArray(selectedDates) && selectedDates[0] || undefined; + + // 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 (pickerOptions.enableTime) { + this.onTriggerEvent(new CustomEvent('keyup')); + } else { + this.onTriggerEvent(undefined); + } + } + }; + + // add the time picker when format is UTC (Z) or has the 'h' (meaning hours) + if (outputFormat && (outputFormat === 'Z' || outputFormat.toLowerCase().indexOf('h') > -1)) { + pickerOptions.enableTime = true; + } + + // merge options with optional user's custom options + this._flatpickrOptions = { ...pickerOptions, ...userFilterOptions }; + + let placeholder = (this.gridOptions) ? (this.gridOptions.defaultFilterPlaceholder || '') : ''; + if (this.columnFilter && this.columnFilter.placeholder) { + placeholder = this.columnFilter.placeholder; + } + const $filterInputElm: any = $(`
`); + this.flatInstance = (flatpickr && $filterInputElm[0] && typeof $filterInputElm[0].flatpickr === 'function') ? $filterInputElm[0].flatpickr(this._flatpickrOptions) : null; + return $filterInputElm; + } + + private buildSelectOperatorHtmlString() { + const optionValues = this.getOptionValues(); + let optionValueString = ''; + optionValues.forEach((option) => { + optionValueString += ``; + }); + + return ``; + } + + private getOptionValues(): { operator: OperatorString, description: string }[] { + return [ + { operator: '', description: '' }, + { operator: '=', description: '=' }, + { operator: '<', description: '<' }, + { operator: '<=', description: '<=' }, + { operator: '>', description: '>' }, + { operator: '>=', description: '>=' }, + { operator: '<>', description: '<>' } + ]; + } + + /** + * Create the DOM element + */ + private createDomElement(searchTerm?: SearchTerm) { + const columnId = this.columnDef && this.columnDef.id; + const $headerElm = this.grid.getHeaderRowColumn(columnId); + $($headerElm).empty(); + + // create the DOM Select dropdown for the Operator + this.$selectOperatorElm = $(this.buildSelectOperatorHtmlString()); + this.$filterInputElm = this.buildDatePickerInput(searchTerm); + const $filterContainerElm = $(`
`); + const $containerInputGroup = $(`
`); + const $operatorInputGroupAddon = $(`
`); + + /* the DOM element final structure will be +
+
+ +
+
+ +
+
+ */ + $operatorInputGroupAddon.append(this.$selectOperatorElm); + $containerInputGroup.append($operatorInputGroupAddon); + $containerInputGroup.append(this.$filterInputElm); + + // create the DOM element & add an ID and filter class + $filterContainerElm.append($containerInputGroup); + this.$filterInputElm.data('columnId', columnId); + + if (this.operator) { + this.$selectOperatorElm.val(this.operator); + } + + // if there's a search term, we will add the "filled" class for styling purposes + if (searchTerm && searchTerm !== '') { + this.$filterInputElm.addClass('filled'); + this._currentDate = searchTerm as Date; + this._currentValue = searchTerm as string; + } + + // append the new DOM element to the header row + if ($filterContainerElm && typeof $filterContainerElm.appendTo === 'function') { + $filterContainerElm.appendTo($headerElm); + } + + return $filterContainerElm; + } + + /** Load a different set of locales for Flatpickr to be localized */ + private loadFlatpickrLocale(language: string) { + let locales = 'en'; + + try { + if (language !== 'en') { + // change locale if needed, Flatpickr reference: https://chmln.github.io/flatpickr/localization/ + const localeDefault: any = require(`flatpickr/dist/l10n/${language}.js`).default; + locales = (localeDefault && localeDefault[language]) ? localeDefault[language] : 'en'; + } + } catch (e) { + console.warn(`[Slickgrid-Universal - CompoundDate Filter] It seems that "${language}" is not a locale supported by Flatpickr, we will use "en" instead. ` + + `To avoid seeing this message, you can specifically set "filter: { filterOptions: { locale: 'en' } }" in your column definition.`); + return 'en'; + } + return locales; + } + + private onTriggerEvent(e: Event | undefined) { + if (this._clearFilterTriggered) { + this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); + this.$filterElm.removeClass('filled'); + } else { + const selectedOperator = this.$selectOperatorElm.find('option:selected').text(); + (this._currentValue) ? this.$filterElm.addClass('filled') : this.$filterElm.removeClass('filled'); + this.callback(e, { columnDef: this.columnDef, searchTerms: (this._currentValue ? [this._currentValue] : null), operator: selectedOperator || '', shouldTriggerQuery: this._shouldTriggerQuery }); + } + // reset both flags for next use + this._clearFilterTriggered = false; + this._shouldTriggerQuery = true; + } +} diff --git a/packages/common/src/filters/dateRangeFilter.ts b/packages/common/src/filters/dateRangeFilter.ts new file mode 100644 index 000000000..0a2d72451 --- /dev/null +++ b/packages/common/src/filters/dateRangeFilter.ts @@ -0,0 +1,304 @@ +import * as flatpickr from 'flatpickr'; +import * as moment_ from 'moment-mini'; +const moment = moment_; // patch to fix rollup "moment has no default export" issue, document here https://github.com/rollup/rollup/issues/670 + +import { + FieldType, + OperatorString, + OperatorType, + SearchTerm, +} from '../enums/index'; +import { + Column, + ColumnFilter, + Filter, + FilterArguments, + FilterCallback, + FlatpickrOption, + GridOption, +} from '../interfaces/index'; +import { mapFlatpickrDateFormatWithFieldType, mapMomentDateFormatWithFieldType } from '../services/utilities'; +import { TranslaterService } from '../services/translater.service'; + +// using external non-typed js libraries +declare const $: any; + +declare function require(name: string): any; +declare function require(name: string[], loadedFile: any): any; + +export class DateRangeFilter implements Filter { + private _clearFilterTriggered = false; + private _currentValue: string; + private _currentDates: Date[]; + private _currentDateStrings: string[]; + private _flatpickrOptions: FlatpickrOption; + private _shouldTriggerQuery = true; + private $filterElm: any; + private $filterInputElm: any; + flatInstance: any; + grid: any; + searchTerms: SearchTerm[]; + columnDef: Column; + callback: FilterCallback; + + constructor(private translaterService: TranslaterService) { } + + /** Getter for the Grid Options pulled through the Grid Object */ + private get gridOptions(): GridOption { + return (this.grid && this.grid.getOptions) ? this.grid.getOptions() : {}; + } + + /** Getter for the Column Filter */ + get columnFilter(): ColumnFilter { + return this.columnDef && this.columnDef.filter || {}; + } + + /** Getter for the Current Dates selected */ + get currentDates(): Date[] { + return this._currentDates; + } + + /** Getter to know what would be the default operator when none is specified */ + get defaultOperator(): OperatorType | OperatorString { + return this.gridOptions.defaultFilterRangeOperator || OperatorType.rangeExclusive; + } + + /** Getter for the Flatpickr Options */ + get flatpickrOptions(): FlatpickrOption { + return this._flatpickrOptions || {}; + } + + /** Getter of the Operator to use when doing the filter comparing */ + get operator(): OperatorType | OperatorString { + return this.columnFilter && this.columnFilter.operator || this.defaultOperator; + } + + /** Setter for the filter operator */ + set operator(operator: OperatorType | OperatorString) { + if (this.columnFilter) { + this.columnFilter.operator = operator; + } + } + + /** + * Initialize the Filter + */ + init(args: FilterArguments) { + if (!args) { + throw new Error('[Slickgrid-Universal] A filter must always have an "init()" with valid arguments.'); + } + + this.grid = args.grid; + this.callback = args.callback; + this.columnDef = args.columnDef; + this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; + + // step 1, create the DOM Element of the filter which contain the compound Operator+Input + this.$filterElm = this.createDomElement(this.searchTerms); + + // step 3, subscribe to the keyup event and run the callback when that happens + // also add/remove "filled" class for styling purposes + this.$filterInputElm.keyup((e: any) => { + this.onTriggerEvent(e); + }); + + } + + /** + * Clear the filter value + */ + clear(shouldTriggerQuery = true) { + if (this.flatInstance) { + this._clearFilterTriggered = true; + this._shouldTriggerQuery = shouldTriggerQuery; + this.searchTerms = []; + this.flatInstance.clear(); + } + } + + /** + * destroy the filter + */ + destroy() { + if (this.$filterElm) { + this.$filterElm.off('keyup').remove(); + } + if (this.flatInstance && typeof this.flatInstance.destroy === 'function') { + this.flatInstance.destroy(); + } + } + + hide() { + if (this.flatInstance && typeof this.flatInstance.close === 'function') { + this.flatInstance.close(); + } + } + + show() { + if (this.flatInstance && typeof this.flatInstance.open === 'function') { + this.flatInstance.open(); + } + } + + /** + * Set value(s) on the DOM element + * @params searchTerms + */ + setValues(searchTerms: SearchTerm[], operator?: OperatorType | OperatorString) { + let pickerValues: any[] = []; + + // 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 searchTerms === 'string' || (Array.isArray(searchTerms) && typeof searchTerms[0] === 'string') && (searchTerms[0] as string).indexOf('..') > 0) { + pickerValues = (typeof searchTerms === 'string') ? [(searchTerms as string)] : (searchTerms[0] as string).split('..'); + } else if (Array.isArray(searchTerms)) { + pickerValues = searchTerms; + } + + if (this.flatInstance && searchTerms) { + this._currentDates = pickerValues; + this.flatInstance.setDate(pickerValues); + } + + // set the operator when defined + this.operator = operator || this.defaultOperator; + } + + // + // private functions + // ------------------ + private buildDatePickerInput(searchTerms?: SearchTerm | SearchTerm[]) { + const columnId = this.columnDef && this.columnDef.id; + const inputFormat = mapFlatpickrDateFormatWithFieldType(this.columnDef.type || FieldType.dateIso); + const outputFormat = mapFlatpickrDateFormatWithFieldType(this.columnDef.outputType || this.columnDef.type || FieldType.dateUtc); + const userFilterOptions = (this.columnFilter && this.columnFilter.filterOptions || {}) as FlatpickrOption; + + // 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 && userFilterOptions.locale) || (this.translaterService && this.translaterService.getCurrentLocale && this.translaterService.getCurrentLocale()) || this.gridOptions.locale || 'en'; + if (currentLocale.length > 2) { + currentLocale = currentLocale.substring(0, 2); + } + + let pickerValues: any[] = []; + + // 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 searchTerms === 'string' || (Array.isArray(searchTerms) && typeof searchTerms[0] === 'string') && (searchTerms[0] as string).indexOf('..') > 0) { + pickerValues = (typeof searchTerms === 'string') ? [(searchTerms as string)] : (searchTerms[0] as string).split('..'); + } else if (Array.isArray(searchTerms)) { + pickerValues = searchTerms; + } + + // if we are preloading searchTerms, we'll keep them for reference + if (pickerValues) { + this._currentDates = pickerValues as Date[]; + const outFormat = mapMomentDateFormatWithFieldType(this.columnDef.type || FieldType.dateIso); + this._currentDateStrings = pickerValues.map(date => moment(date).format(outFormat)); + } + + const pickerOptions: FlatpickrOption = { + defaultDate: (pickerValues || '') as string | string[], + altInput: true, + altFormat: outputFormat, + dateFormat: inputFormat, + mode: 'range', + wrap: true, + closeOnSelect: true, + locale: (currentLocale !== 'en') ? this.loadFlatpickrLocale(currentLocale) : 'en', + onChange: (selectedDates: Date[] | Date, dateStr: string, instance: any) => { + if (Array.isArray(selectedDates)) { + this._currentDates = selectedDates; + const outFormat = mapMomentDateFormatWithFieldType(this.columnDef.outputType || this.columnDef.type || FieldType.dateIso); + this._currentDateStrings = selectedDates.map(date => moment(date).format(outFormat)); + this._currentValue = this._currentDateStrings.join('..'); + } + + // 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 CustomEvent('keyup') : undefined; + this.onTriggerEvent(newEvent); + } + }; + + // 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; + } + + // merge options with optional user's custom options + this._flatpickrOptions = { ...pickerOptions, ...userFilterOptions }; + + let placeholder = (this.gridOptions) ? (this.gridOptions.defaultFilterPlaceholder || '') : ''; + if (this.columnFilter && this.columnFilter.placeholder) { + placeholder = this.columnFilter.placeholder; + } + const $filterInputElm: any = $(`
`); + this.flatInstance = (flatpickr && $filterInputElm[0] && typeof $filterInputElm[0].flatpickr === 'function') ? $filterInputElm[0].flatpickr(this._flatpickrOptions) : null; + return $filterInputElm; + } + + /** + * Create the DOM element + * @params searchTerms + */ + private createDomElement(searchTerms?: SearchTerm[]) { + const columnId = this.columnDef && this.columnDef.id; + const $headerElm = this.grid.getHeaderRowColumn(columnId); + $($headerElm).empty(); + + // create the DOM Select dropdown for the Operator + this.$filterInputElm = this.buildDatePickerInput(searchTerms); + + /* the DOM element final structure will be +
+ +
+ */ + + // create the DOM element & add an ID and filter class + this.$filterInputElm.data('columnId', columnId); + + // if there's a search term, we will add the "filled" class for styling purposes + if (Array.isArray(searchTerms) && searchTerms.length > 0 && searchTerms[0] !== '') { + this.$filterInputElm.addClass('filled'); + this._currentDates = searchTerms as Date[]; + this._currentValue = searchTerms[0] as string; + } + + // append the new DOM element to the header row + if (this.$filterInputElm && typeof this.$filterInputElm.appendTo === 'function') { + this.$filterInputElm.appendTo($headerElm); + } + + return this.$filterInputElm; + } + + /** Load a different set of locales for Flatpickr to be localized */ + private loadFlatpickrLocale(language: string) { + let locales = 'en'; + + try { + if (language !== 'en') { + // change locale if needed, Flatpickr reference: https://chmln.github.io/flatpickr/localization/ + const localeDefault: any = require(`flatpickr/dist/l10n/${language}.js`).default; + locales = (localeDefault && localeDefault[language]) ? localeDefault[language] : 'en'; + } + } catch (e) { + console.warn(`[Slickgrid-Universal - DateRange Filter] It seems that "${language}" is not a locale supported by Flatpickr, we will use "en" instead. ` + + `To avoid seeing this message, you can specifically set "filter: { filterOptions: { locale: 'en' } }" in your column definition.`); + return 'en'; + } + return locales; + } + + private onTriggerEvent(e: Event | undefined) { + if (this._clearFilterTriggered) { + this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery }); + this.$filterElm.removeClass('filled'); + } else { + (this._currentDateStrings) ? this.$filterElm.addClass('filled') : this.$filterElm.removeClass('filled'); + this.callback(e, { columnDef: this.columnDef, searchTerms: (this._currentDateStrings ? this._currentDateStrings : [this._currentValue]), operator: this.operator || '', shouldTriggerQuery: this._shouldTriggerQuery }); + } + // reset both flags for next use + this._clearFilterTriggered = false; + this._shouldTriggerQuery = true; + } +} diff --git a/packages/common/src/filters/index.ts b/packages/common/src/filters/index.ts index 2bca1f4d2..a7f17894f 100644 --- a/packages/common/src/filters/index.ts +++ b/packages/common/src/filters/index.ts @@ -1,6 +1,6 @@ -import { Column, Filter } from '../interfaces/index'; +// import { Column, Filter } from '../interfaces/index'; import { AutoCompleteFilter } from './autoCompleteFilter'; -// import { CompoundDateFilter } from './compoundDateFilter'; +import { CompoundDateFilter } from './compoundDateFilter'; import { CompoundInputFilter } from './compoundInputFilter'; import { CompoundInputNumberFilter } from './compoundInputNumberFilter'; import { CompoundInputPasswordFilter } from './compoundInputPasswordFilter'; @@ -11,7 +11,7 @@ import { InputNumberFilter } from './inputNumberFilter'; import { InputPasswordFilter } from './inputPasswordFilter'; import { MultipleSelectFilter } from './multipleSelectFilter'; import { NativeSelectFilter } from './nativeSelectFilter'; -// import { DateRangeFilter } from './dateRangeFilter'; +import { DateRangeFilter } from './dateRangeFilter'; import { SingleSelectFilter } from './singleSelectFilter'; import { SliderFilter } from './sliderFilter'; import { SliderRangeFilter } from './sliderRangeFilter'; @@ -21,7 +21,7 @@ export const Filters = { autoComplete: AutoCompleteFilter, /** Compound Date Filter (compound of Operator + Date picker) */ - // compoundDate: CompoundDateFilter, + compoundDate: CompoundDateFilter, /** Alias to compoundInputText to Compound Input Filter (compound of Operator + Input Text) */ compoundInput: CompoundInputFilter, @@ -39,7 +39,7 @@ export const Filters = { compoundSlider: CompoundSliderFilter, /** Range Date Filter (uses the Flactpickr Date picker with range option) */ - // dateRange: DateRangeFilter, + dateRange: DateRangeFilter, /** Alias to inputText, input type text filter */ input: InputFilter, diff --git a/packages/common/src/services/__tests__/collection.service.spec.ts b/packages/common/src/services/__tests__/collection.service.spec.ts index 81da9e6ec..e6027ad66 100644 --- a/packages/common/src/services/__tests__/collection.service.spec.ts +++ b/packages/common/src/services/__tests__/collection.service.spec.ts @@ -1,5 +1,5 @@ import { CollectionService } from '../collection.service'; -import { FieldType, FilterMultiplePassType, OperatorType, } from '../../enums' +import { FieldType, FilterMultiplePassType, OperatorType, } from '../../enums/index'; import { CollectionFilterBy, CollectionSortBy, Column, } from '../../interfaces/index'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; diff --git a/packages/common/src/styles/se-slickgrid-theme-material.scss b/packages/common/src/styles/se-slickgrid-theme-material.scss new file mode 100644 index 000000000..76f04910e --- /dev/null +++ b/packages/common/src/styles/se-slickgrid-theme-material.scss @@ -0,0 +1,109 @@ +/** internally SE material theme for work */ +@import './multiple-select'; +@import './roboto-font.scss'; + +$font-family: Roboto, "Helvetica Neue", sans-serif; +$font-size-base-value: 14; +$icon-font-family: "Material Design Icons"; +$icon-font-size: 20px; +$group-totals-formatter-color: #666666; +$cell-border-top: none; +$header-background-color: #F3F3F3; +$header-border-bottom: 1px solid #ccc; +$header-font-size: 12px; +$header-font-weight: 500; +$header-text-color: rgba(0, 0, 0, 0.87); +$cell-text-color: rgba(0, 0, 0, 0.87); +$cell-font-weight: 400; +$cell-odd-background-color: #f5f5f5; +$container-border-top: 1px solid #ccc; +$container-border-bottom: 1px solid #ccc; +$container-border-left: 1px solid #ccc; +$container-border-right: 1px solid #ccc; +$icon-sort-asc: "\F0360"; +$icon-sort-desc: "\F035D"; +$icon-sort-font-size: 22px; +$icon-sort-position-right: 18px; +$checkbox-selector-size: 22px; +$checkbox-selector-icon-font-weight: normal; +$checkbox-selector-icon-checked: "\F0132"; +$checkbox-selector-icon-unchecked: "\F0131"; +$checkbox-selector-opacity: 1; +$checkbox-selector-opacity-hover: 0.9; +$cell-menu-icon-margin-right: 6px; +$column-picker-checkbox-size: 18px; +$column-picker-checkbox-font-weight: normal; +$column-picker-checkbox-icon-checked: "\F012c"; +$column-picker-checkbox-icon-unchecked: "\F012c"; +$column-picker-checkbox-width: 18px; +$column-picker-title-font-size: 18px; +$context-menu-item-font-size: 16px; +$context-menu-icon-font-size: 16px; +$icon-group-collapsed: "\F0142"; +$icon-group-expanded: "\F0140"; +$icon-group-font-weight: normal; +$icon-group-margin-right: 8px; +$draggable-group-toggle-collapsed-icon: "\F0142"; +$draggable-group-toggle-expanded-icon: "\F0140"; +$draggable-group-title-height: 24px; +$draggable-group-title-line-height: 24px; +$draggable-group-title-vertical-align: top; +$grid-menu-checkbox-size: 18px; +$grid-menu-checkbox-font-weight: normal; +$grid-menu-checkbox-icon-checked: "\F012c"; +$grid-menu-checkbox-icon-unchecked: "\F012c"; +$grid-menu-checkbox-width: 18px; +$grid-menu-icon-font-size: 20px; +$grid-menu-icon-width: 20px; +$grid-menu-title-font-size: 18px; +$header-menu-button-icon-font-size: 20px; +$header-menu-icon-font-size: 18px; +$header-menu-icon-width: 18px; +$header-menu-button-icon: "\F0140"; +$header-menu-button-margin-right: 6px; +$header-menu-display: inline-block; +$multiselect-icon-arrow-down: "\F035D"; +$multiselect-icon-arrow-up: "\F0360"; +$multiselect-icon-arrow-font-size: $icon-font-size - 4px; +$multiselect-icon-checked: "\F0132"; +$multiselect-icon-unchecked: "\F0131"; +$multiselect-icon-radio-checked: "\F043E"; +$multiselect-icon-radio-unchecked: "\F0130"; +$multiselect-icon-search: "\F0349"; +$multiselect-unchecked-opacity: 0.8; +$row-move-plugin-cursor: grab; +$row-move-plugin-icon: "\F0278"; +$slider-editor-height: 26px; +$primary-color: #009530; +$row-mouse-hover-color: #ebfaef; +$row-selected-color: #d4f6d7; /*rgba(0, 149, 48, 0.2);*/ + +@import './slick-material'; +@import './slick-grid'; +@import './slick-controls'; +@import './slick-editors'; +@import './slick-plugins'; +@import './slick-default-theme'; +@import './slick-pagination'; +@import './slick-footer'; +@import './slickgrid-examples'; +@import './slick-bootstrap'; +@import './bootstrap-jquery-ui-autocomplete'; + +$link-color: #0099ff; + +.cell-effort-driven { + text-align: center; +} + +.editable-field { + background-color: #d5e8e9 !important; +} +.fake-hyperlink { + cursor: pointer; + color: $link-color; + &:hover { + text-decoration: underline; + } +} + diff --git a/packages/common/src/styles/slick-plugins.scss b/packages/common/src/styles/slick-plugins.scss index da6c9adb8..facf743cb 100644 --- a/packages/common/src/styles/slick-plugins.scss +++ b/packages/common/src/styles/slick-plugins.scss @@ -1,6 +1,5 @@ /* plugin variables */ @import './variables'; -@import './multiple-select'; // ---------------------------------------------- // Cell Menu Plugin (Action menu diff --git a/packages/common/src/styles/slickgrid-theme-material.scss b/packages/common/src/styles/slickgrid-theme-material.scss index 6e543f09b..d964009ff 100644 --- a/packages/common/src/styles/slickgrid-theme-material.scss +++ b/packages/common/src/styles/slickgrid-theme-material.scss @@ -4,10 +4,9 @@ $icon-font-family: "Material Design Icons"; $icon-font-size: 20px; $group-totals-formatter-color: #666666; $cell-border-top: none; -$header-background-color: #F3F3F3; $header-border-bottom: 1px solid #ccc; $header-font-size: 12px; -$header-font-weight: 500; +$header-font-weight: 700; $header-text-color: rgba(0, 0, 0, 0.87); $cell-text-color: rgba(0, 0, 0, 0.87); $cell-font-weight: 400; @@ -74,7 +73,7 @@ $primary-color: #009530; $row-mouse-hover-color: #ebfaef; $row-selected-color: #d4f6d7; /*rgba(0, 149, 48, 0.2);*/ -@import './roboto-font.scss'; +@import './roboto-font'; @import './slick-material'; @import './slick-grid'; @import './slick-controls'; @@ -86,45 +85,3 @@ $row-selected-color: #d4f6d7; /*rgba(0, 149, 48, 0.2);*/ @import './slickgrid-examples'; @import './slick-bootstrap'; @import './bootstrap-jquery-ui-autocomplete'; - -$link-color: #0099ff; - -.cell-effort-driven { - text-align: center; -} - -.editable-field { - background-color: #d5e8e9 !important; -} -.fake-hyperlink { - cursor: pointer; - color: $link-color; - &:hover { - text-decoration: underline; - } -} - -.toggle { - height: 24px; - width: 24px; - display: inline-block; - - &.expand { - cursor: pointer; - &:before { - font-family: "Material Design Icons"; - font-size: 24px; - content: "\F0142"; - } - } - - &.collapse{ - cursor: pointer; - &:before { - font-family: "Material Design Icons"; - font-size: 24px; - content: "\F0140"; - } - } -} - diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json index a9520dc1b..4858049d7 100644 --- a/packages/common/tsconfig.json +++ b/packages/common/tsconfig.json @@ -8,6 +8,7 @@ "target": "es2015", "module": "esnext", "sourceMap": true, + "allowSyntheticDefaultImports": false, "noImplicitReturns": true, "lib": [ "es2020", diff --git a/packages/vanilla-bundle/src/vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/vanilla-grid-bundle.ts index db4f45386..a2c1311b9 100644 --- a/packages/vanilla-bundle/src/vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/vanilla-grid-bundle.ts @@ -41,9 +41,6 @@ import { CollectionService, GroupingAndColspanService, SlickgridConfig, - - // utilities - getScrollBarWidth, } from '@slickgrid-universal/common'; import { TranslateService } from './services/translate.service'; diff --git a/packages/web-demo-vanilla-bundle/package.json b/packages/web-demo-vanilla-bundle/package.json index 9edac151d..c2528578a 100644 --- a/packages/web-demo-vanilla-bundle/package.json +++ b/packages/web-demo-vanilla-bundle/package.json @@ -34,12 +34,12 @@ "@types/jquery": "^3.3.34", "@types/moment": "^2.13.0", "@types/node": "^12.12.32", - "@types/webpack": "^4.41.8", + "@types/webpack": "^4.41.9", "clean-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "^5.1.1", "css-loader": "^3.4.2", "html-loader": "^1.0.0", - "html-webpack-plugin": "^4.0.2", + "html-webpack-plugin": "^4.0.3", "json-loader": "^0.5.7", "mini-css-extract-plugin": "^0.9.0", "node-sass": "4.13.1", @@ -53,4 +53,4 @@ "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3" } -} \ No newline at end of file +} diff --git a/packages/web-demo-vanilla-bundle/src/examples/example02.ts b/packages/web-demo-vanilla-bundle/src/examples/example02.ts index 7d639b7b4..00f75b04f 100644 --- a/packages/web-demo-vanilla-bundle/src/examples/example02.ts +++ b/packages/web-demo-vanilla-bundle/src/examples/example02.ts @@ -1,4 +1,4 @@ -import { Aggregators, Column, Editors, FieldType, Filters, Sorters, SortDirectionNumber, Grouping, GroupTotalFormatters, Formatters, GridOption } from '@slickgrid-universal/common'; +import { Aggregators, Column, FieldType, Filters, Sorters, SortDirectionNumber, Grouping, GroupTotalFormatters, Formatters, GridOption } from '@slickgrid-universal/common'; import { Slicker } from '@slickgrid-universal/vanilla-bundle'; const NB_ITEMS = 500; @@ -66,8 +66,8 @@ export class Example2 { id: 'start', name: 'Start', field: 'start', minWidth: 60, maxWidth: 130, - // filterable: true, - // filter: { model: Filters.compoundDate }, + filterable: true, + filter: { model: Filters.compoundDate }, sortable: true, type: FieldType.dateIso, formatter: Formatters.dateIso, @@ -77,8 +77,8 @@ export class Example2 { id: 'finish', name: 'Finish', field: 'finish', minWidth: 60, maxWidth: 130, - // filterable: true, - // filter: { model: Filters.compoundDate }, + filterable: true, + filter: { model: Filters.compoundDate }, sortable: true, type: FieldType.dateIso, formatter: Formatters.dateIso, @@ -96,7 +96,7 @@ export class Example2 { exportWithFormatter: true, formatter: Formatters.dollar, groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollar, - params: { groupFormatterPrefix: 'Total: ' /*, groupFormatterSuffix: ' USD'*/ } + params: { groupFormatterPrefix: 'Total: ' /* , groupFormatterSuffix: ' USD' */ } }, { id: 'effortDriven', name: 'Effort Driven', diff --git a/packages/web-demo-vanilla-bundle/src/examples/example03.ts b/packages/web-demo-vanilla-bundle/src/examples/example03.ts index f53faf5e7..e9796ad1a 100644 --- a/packages/web-demo-vanilla-bundle/src/examples/example03.ts +++ b/packages/web-demo-vanilla-bundle/src/examples/example03.ts @@ -129,7 +129,8 @@ export class Example3 { }, { id: 'start', name: 'Start', field: 'start', sortable: true, - formatter: Formatters.dateIso, type: FieldType.dateUtc, outputType: FieldType.dateIso, + formatter: Formatters.dateIso, type: FieldType.date, outputType: FieldType.dateIso, + editor: { model: Editors.date, }, grouping: { getter: 'start', formatter: (g) => `Start: ${g.value} (${g.count} items)`, @@ -142,7 +143,8 @@ export class Example3 { }, { id: 'finish', name: 'Finish', field: 'finish', sortable: true, - formatter: Formatters.dateIso, type: FieldType.dateUtc, outputType: FieldType.dateIso, + editor: { model: Editors.date, }, + formatter: Formatters.dateIso, type: FieldType.date, outputType: FieldType.dateIso, grouping: { getter: 'finish', formatter: (g) => `Finish: ${g.value} (${g.count} items)`, diff --git a/packages/web-demo-vanilla-bundle/src/main.ts b/packages/web-demo-vanilla-bundle/src/main.ts index 2abdc51f6..fe53c25e5 100644 --- a/packages/web-demo-vanilla-bundle/src/main.ts +++ b/packages/web-demo-vanilla-bundle/src/main.ts @@ -1,7 +1,12 @@ -import 'jquery'; -import 'jquery-ui-dist/jquery-ui'; +// import all CSS required by Slickgrid-Universal import 'bulma/css/bulma.css'; +import 'multiple-select-adapted/src/multiple-select.css'; +import 'flatpickr/dist/flatpickr.min.css'; import './styles.scss'; + +// import all other 3rd party libs required by Slickgrid-Universal +import 'jquery'; +import 'jquery-ui-dist/jquery-ui'; import { Renderer } from './renderer'; import * as SlickerModule from '@slickgrid-universal/vanilla-bundle'; import { App } from './app'; diff --git a/packages/web-demo-vanilla-bundle/src/se-styles.scss b/packages/web-demo-vanilla-bundle/src/se-styles.scss new file mode 100644 index 000000000..7642b32f3 --- /dev/null +++ b/packages/web-demo-vanilla-bundle/src/se-styles.scss @@ -0,0 +1,2 @@ +/* make sure to add the @import the SlickGrid Bootstrap Theme AFTER the variables changes */ +@import '@slickgrid-universal/common/dist/styles/sass/se-slickgrid-theme-material.scss'; diff --git a/packages/web-demo-vanilla-bundle/webpack.config.js b/packages/web-demo-vanilla-bundle/webpack.config.js index d9ad07c4c..652afbc51 100644 --- a/packages/web-demo-vanilla-bundle/webpack.config.js +++ b/packages/web-demo-vanilla-bundle/webpack.config.js @@ -55,9 +55,9 @@ module.exports = ({ production } = {}, { extractCss, analyze, tests, hmr, port, issuer: [{ not: [{ test: /\.html$/i }] }], use: extractCss ? [{ loader: MiniCssExtractPlugin.loader }, 'css-loader'] : ['style-loader', ...cssRules] }, - { test: /\.(png|gif|jpg|cur)$/i, loader: 'url-loader', options: { limit: 8192 } }, { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'], issuer: /\.[tj]s$/i }, { test: /\.scss$/, use: ['css-loader', 'sass-loader'], issuer: /\.html?$/i }, + { test: /\.(png|gif|jpg|cur)$/i, loader: 'url-loader', options: { limit: 8192 } }, { test: /\.html$/i, loader: 'html-loader' }, { test: /\.ts?$/, use: 'ts-loader', exclude: nodeModulesDir, }, ],