From d4da48923a9a60f9ecde787be31b2da14922866e Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Thu, 19 Sep 2024 21:04:49 +0200 Subject: [PATCH 1/6] feat: allow overriding readOnly behavior of dateEditor realted issue https://github.com/ghiscoding/slickgrid-universal/issues/1684 --- .../src/examples/example11.ts | 4 ++-- packages/common/src/editors/dateEditor.ts | 20 ++++++++++++++++++- .../src/interfaces/columnEditor.interface.ts | 5 +++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example11.ts b/examples/vite-demo-vanilla-bundle/src/examples/example11.ts index 48cb806d9..68a2b9710 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example11.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example11.ts @@ -175,7 +175,7 @@ export default class Example11 { type: FieldType.date, outputType: FieldType.dateIso, filterable: true, filter: { model: Filters.compoundDate }, - editor: { model: Editors.date, massUpdate: true }, + editor: { model: Editors.date, massUpdate: true, disabled: false, readOnly: false }, }, { id: 'finish', name: 'Finish', field: 'finish', sortable: true, minWidth: 80, @@ -293,7 +293,7 @@ export default class Example11 { } } else if ((event.target as HTMLElement).classList.contains('mdi-check-underline')) { this.slickerGridInstance?.gridService.updateItem({ ...dataContext, completed: true }); - alert(`The "${dataContext.title}" is now Completed`); + alert(`The "${dataContext.start}" is now Completed`); } } }, diff --git a/packages/common/src/editors/dateEditor.ts b/packages/common/src/editors/dateEditor.ts index 163485227..f7ed36eec 100644 --- a/packages/common/src/editors/dateEditor.ts +++ b/packages/common/src/editors/dateEditor.ts @@ -36,6 +36,7 @@ export class DateEditor implements Editor { protected _lastTriggeredByClearDate = false; protected _originalDate?: string; protected _pickerMergedOptions!: IOptions; + protected _lastInputKeyEvent?: KeyboardEvent; calendarInstance?: VanillaCalendar; defaultDate?: string; hasTimePicker = false; @@ -185,7 +186,7 @@ export class DateEditor implements Editor { title: this.columnEditor && this.columnEditor.title || '', className: inputCssClasses.replace(/\./g, ' '), dataset: { input: '', defaultdate: this.defaultDate }, - readOnly: true, + readOnly: this.columnEditor.readOnly === false ? false : true, }, this._editorInputGroupElm ); @@ -202,6 +203,18 @@ export class DateEditor implements Editor { }); } + this._bindEventService.bind(this._inputElm, 'keydown', ((event: KeyboardEvent) => { + if (this.columnEditor.readOnly !== false) { + return; + } + + this._isValueTouched = true; + this._lastInputKeyEvent = event; + if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { + event.stopImmediatePropagation(); + } + }) as EventListener); + queueMicrotask(() => { this.calendarInstance = new VanillaCalendar(this._inputElm, this._pickerMergedOptions); this.calendarInstance.init(); @@ -349,6 +362,11 @@ export class DateEditor implements Editor { let isChanged = false; const elmDateStr = this.getValue(); + const lastEventKey = this._lastInputKeyEvent?.key; + if (this.columnEditor.readOnly === false && this.columnEditor?.alwaysSaveOnEnterKey && lastEventKey === 'Enter') { + return true; + } + if (this.columnDef) { isChanged = this._lastTriggeredByClearDate || (!(elmDateStr === '' && this._originalDate === '')) && (elmDateStr !== this._originalDate); } diff --git a/packages/common/src/interfaces/columnEditor.interface.ts b/packages/common/src/interfaces/columnEditor.interface.ts index da46c8c50..a42b169fb 100644 --- a/packages/common/src/interfaces/columnEditor.interface.ts +++ b/packages/common/src/interfaces/columnEditor.interface.ts @@ -128,6 +128,11 @@ export interface ColumnEditor { */ required?: boolean; + /** + * only applicable for dateEditors. If explicitely set to false, it will allow to enter a new date in the input field. + */ + readOnly?: boolean; + /** * defaults to 'object', how do we want to serialize the editor value to the resulting dataContext object when using a complex object? * Currently only applies to Single/Multiple Select Editor. From eb176debd69b55ffd95cba8ec7198c821ecaddb2 Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Sat, 21 Sep 2024 17:41:13 +0200 Subject: [PATCH 2/6] feat: update according to review; add unit tests --- .../src/examples/example11.ts | 4 +- .../src/editors/__tests__/dateEditor.spec.ts | 45 +++++++++++++++++++ packages/common/src/editors/dateEditor.ts | 7 +-- .../src/interfaces/columnEditor.interface.ts | 5 --- .../vanillaCalendarOption.interface.ts | 3 ++ 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example11.ts b/examples/vite-demo-vanilla-bundle/src/examples/example11.ts index 68a2b9710..82b3e2ed2 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example11.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example11.ts @@ -175,7 +175,7 @@ export default class Example11 { type: FieldType.date, outputType: FieldType.dateIso, filterable: true, filter: { model: Filters.compoundDate }, - editor: { model: Editors.date, massUpdate: true, disabled: false, readOnly: false }, + editor: { model: Editors.date, massUpdate: true, disabled: false, editorOptions: { allowInput: true } }, }, { id: 'finish', name: 'Finish', field: 'finish', sortable: true, minWidth: 80, @@ -293,7 +293,7 @@ export default class Example11 { } } else if ((event.target as HTMLElement).classList.contains('mdi-check-underline')) { this.slickerGridInstance?.gridService.updateItem({ ...dataContext, completed: true }); - alert(`The "${dataContext.start}" is now Completed`); + alert(`The "${dataContext.title}" is now Completed`); } } }, diff --git a/packages/common/src/editors/__tests__/dateEditor.spec.ts b/packages/common/src/editors/__tests__/dateEditor.spec.ts index 1651467ac..48a33f4ba 100644 --- a/packages/common/src/editors/__tests__/dateEditor.spec.ts +++ b/packages/common/src/editors/__tests__/dateEditor.spec.ts @@ -141,6 +141,34 @@ describe('DateEditor', () => { expect(showSpy).toHaveBeenCalled(); }); + it('should initialize the editor and add a keydown event listener that early exists by default', () => { + editor = new DateEditor(editorArguments); + + const event = new KeyboardEvent('keydown', { key: 'Enter' }); + editor.editorDomElement.dispatchEvent(event); + + expect(editor.columnEditor.editorOptions?.allowEdit).toBeFalsy(); + expect(editor.isValueTouched()).toBeFalsy(); + }); + + it('should stop propagation on allowEdit when hitting left or right arrow keys', () => { + editor = new DateEditor({ ...editorArguments, + column: { ...editorArguments.column, + editor: { ...editorArguments.column.editor, + editorOptions: { ...editorArguments.column?.editor?.editorOptions, allowEdit: true } + }}}); + + let event = new KeyboardEvent('keydown', { key: 'ArrowLeft' }); + let propagationSpy = vi.spyOn(event, 'stopImmediatePropagation'); + editor.editorDomElement.dispatchEvent(event); + expect(propagationSpy).toHaveBeenCalled(); + + event = new KeyboardEvent('keydown', { key: 'ArrowRight' }); + propagationSpy = vi.spyOn(event, 'stopImmediatePropagation'); + editor.editorDomElement.dispatchEvent(event); + expect(propagationSpy).toHaveBeenCalled(); + }); + it('should have a placeholder when defined in its column definition', () => { const testValue = 'test placeholder'; mockColumn.editor!.placeholder = testValue; @@ -267,6 +295,23 @@ describe('DateEditor', () => { expect(editor.isValueTouched()).toBe(true); }); + it('should return True when the last key was enter and alwaysSaveOnEnterKey is active', () => { + mockItemData = { id: 1, startDate: '2001-01-02T11:02:02.000Z', isActive: true }; + + editor = new DateEditor({...editorArguments, + column: { ...mockColumn, editor: { ...editorArguments.column.editor, alwaysSaveOnEnterKey: true, + editorOptions: { ...editorArguments.column.editor?.editorOptions, allowEdit: true} + } } + }); + const event = new KeyboardEvent('keydown', { key: 'Enter' }); + vi.runAllTimers(); + + editor.editorDomElement.dispatchEvent(event); + + expect(editor.isValueChanged()).toBe(true); + + }); + it('should return True when date is reset by the clear date button', () => { mockItemData = { id: 1, startDate: '2001-01-02T11:02:02.000Z', isActive: true }; diff --git a/packages/common/src/editors/dateEditor.ts b/packages/common/src/editors/dateEditor.ts index f7ed36eec..4749bd27e 100644 --- a/packages/common/src/editors/dateEditor.ts +++ b/packages/common/src/editors/dateEditor.ts @@ -186,7 +186,7 @@ export class DateEditor implements Editor { title: this.columnEditor && this.columnEditor.title || '', className: inputCssClasses.replace(/\./g, ' '), dataset: { input: '', defaultdate: this.defaultDate }, - readOnly: this.columnEditor.readOnly === false ? false : true, + readOnly: this.columnEditor.editorOptions?.allowEdit === true ? true : false, }, this._editorInputGroupElm ); @@ -204,7 +204,7 @@ export class DateEditor implements Editor { } this._bindEventService.bind(this._inputElm, 'keydown', ((event: KeyboardEvent) => { - if (this.columnEditor.readOnly !== false) { + if (this.columnEditor.editorOptions?.allowEdit !== true) { return; } @@ -363,7 +363,8 @@ export class DateEditor implements Editor { const elmDateStr = this.getValue(); const lastEventKey = this._lastInputKeyEvent?.key; - if (this.columnEditor.readOnly === false && this.columnEditor?.alwaysSaveOnEnterKey && lastEventKey === 'Enter') { + if (this.columnEditor.editorOptions?.allowEdit === true && + this.columnEditor?.alwaysSaveOnEnterKey && lastEventKey === 'Enter') { return true; } diff --git a/packages/common/src/interfaces/columnEditor.interface.ts b/packages/common/src/interfaces/columnEditor.interface.ts index a42b169fb..da46c8c50 100644 --- a/packages/common/src/interfaces/columnEditor.interface.ts +++ b/packages/common/src/interfaces/columnEditor.interface.ts @@ -128,11 +128,6 @@ export interface ColumnEditor { */ required?: boolean; - /** - * only applicable for dateEditors. If explicitely set to false, it will allow to enter a new date in the input field. - */ - readOnly?: boolean; - /** * defaults to 'object', how do we want to serialize the editor value to the resulting dataContext object when using a complex object? * Currently only applies to Single/Multiple Select Editor. diff --git a/packages/common/src/interfaces/vanillaCalendarOption.interface.ts b/packages/common/src/interfaces/vanillaCalendarOption.interface.ts index ebc1a2b30..1d9d4afbd 100644 --- a/packages/common/src/interfaces/vanillaCalendarOption.interface.ts +++ b/packages/common/src/interfaces/vanillaCalendarOption.interface.ts @@ -12,4 +12,7 @@ export interface VanillaCalendarOption extends IPartialSettings { /** defaults to false, do we want to hide the clear date button? */ hideClearButton?: boolean; + + /** defaults to false, should keyboard entries be allowed in input field? */ + allowInput?: boolean; } From f4adea3590f02c128d769eb259524615f54a9e31 Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Sat, 21 Sep 2024 21:13:33 +0200 Subject: [PATCH 3/6] test: e2e test --- test/cypress/e2e/example11.cy.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/cypress/e2e/example11.cy.ts b/test/cypress/e2e/example11.cy.ts index 1a544c136..1423f3c9e 100644 --- a/test/cypress/e2e/example11.cy.ts +++ b/test/cypress/e2e/example11.cy.ts @@ -22,6 +22,20 @@ describe('Example 11 - Batch Editing', () => { cy.get('h3').should('contain', 'Example 11 - Batch Editing'); }); + it('should input values directly in input of datepicker', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`) + .click(); + + cy.get(`.input-group-editor`) + .focus() + .type('{backspace}'.repeat(10)) + .type('1970-01-01') + .type('{enter}'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`) + .should('contain', '1970-01-01'); + }) + describe('built-in operators', () => { it('should click on "Clear Local Storage" and expect to be back to original grid with all the columns', () => { cy.get('[data-test="clear-storage-btn"]') From fed34e7d5c96f58ce18644a873585f82879f8ab7 Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Sat, 21 Sep 2024 21:13:52 +0200 Subject: [PATCH 4/6] docs: add docs for custom vanilla calendar options --- .../editors/date-editor-(vanilla-calendar).md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/column-functionalities/editors/date-editor-(vanilla-calendar).md b/docs/column-functionalities/editors/date-editor-(vanilla-calendar).md index 13a44b171..a3baca0f5 100644 --- a/docs/column-functionalities/editors/date-editor-(vanilla-calendar).md +++ b/docs/column-functionalities/editors/date-editor-(vanilla-calendar).md @@ -39,6 +39,11 @@ prepareGrid() { } ``` +On top of the default ones provided by Vanilla-Calendar there are also: + +* hideClearButton (default: false): which toggles the visiblity of the clear button +* allowInput (default: false): which determines whether you can directly enter a date in the input element + #### 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 From 8d354e140db57d6a2163eefdfeebe4713f040df5 Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Sat, 21 Sep 2024 22:31:02 +0200 Subject: [PATCH 5/6] chore: remove wrong disabled --- examples/vite-demo-vanilla-bundle/src/examples/example11.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example11.ts b/examples/vite-demo-vanilla-bundle/src/examples/example11.ts index 82b3e2ed2..6ce2fba27 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example11.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example11.ts @@ -175,7 +175,7 @@ export default class Example11 { type: FieldType.date, outputType: FieldType.dateIso, filterable: true, filter: { model: Filters.compoundDate }, - editor: { model: Editors.date, massUpdate: true, disabled: false, editorOptions: { allowInput: true } }, + editor: { model: Editors.date, massUpdate: true, editorOptions: { allowInput: true } }, }, { id: 'finish', name: 'Finish', field: 'finish', sortable: true, minWidth: 80, From 41c3b290442c78a5356a41e12a06305dd3283294 Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Sun, 22 Sep 2024 20:58:23 +0200 Subject: [PATCH 6/6] test: re-arrange so that interdependent tests pass --- test/cypress/e2e/example11.cy.ts | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/test/cypress/e2e/example11.cy.ts b/test/cypress/e2e/example11.cy.ts index 1423f3c9e..4bb5fccfd 100644 --- a/test/cypress/e2e/example11.cy.ts +++ b/test/cypress/e2e/example11.cy.ts @@ -22,20 +22,6 @@ describe('Example 11 - Batch Editing', () => { cy.get('h3').should('contain', 'Example 11 - Batch Editing'); }); - it('should input values directly in input of datepicker', () => { - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`) - .click(); - - cy.get(`.input-group-editor`) - .focus() - .type('{backspace}'.repeat(10)) - .type('1970-01-01') - .type('{enter}'); - - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`) - .should('contain', '1970-01-01'); - }) - describe('built-in operators', () => { it('should click on "Clear Local Storage" and expect to be back to original grid with all the columns', () => { cy.get('[data-test="clear-storage-btn"]') @@ -1101,4 +1087,20 @@ describe('Example 11 - Batch Editing', () => { .click({ force: true }); }); }); + + describe('with Date Editor', () => { + it('should input values directly in input of datepicker', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`) + .click(); + + cy.get(`.input-group-editor`) + .focus() + .type('{backspace}'.repeat(10)) + .type('1970-01-01') + .type('{enter}'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`) + .should('contain', '1970-01-01'); + }); + }); });