From 3181cf069d9f3bc85dc0d13ceeb9623d21ae8eff Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 24 May 2021 19:33:05 -0400 Subject: [PATCH] feat(editors): convert jQuery to native element on slider editor --- .editorconfig | 2 +- CHANGELOG.md | 1 + packages/common/CHANGELOG.md | 2 +- .../editors/__tests__/sliderEditor.spec.ts | 24 ++- packages/common/src/editors/sliderEditor.ts | 162 ++++++++++-------- .../src/services/bindingEvent.service.ts | 9 +- 6 files changed, 112 insertions(+), 88 deletions(-) diff --git a/.editorconfig b/.editorconfig index 218eee3d3..b55e99456 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,7 @@ charset = utf-8 end_of_line = lf indent_style = space indent_size = 4 -insert_final_newline = true +insert_final_newline = false trim_trailing_whitespace = true [*] diff --git a/CHANGELOG.md b/CHANGELOG.md index ee3ac3c8f..6441c5289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline * **services:** add onBeforeResizeByContent (onAfter) ([3e99fab](https://github.com/ghiscoding/slickgrid-universal/commit/3e99fabb8554161e4301c0596eaebd9e0d246de7)) * **styling:** add new marker material icons for project ([9b386fa](https://github.com/ghiscoding/slickgrid-universal/commit/9b386fa3e6af8e76cf4beb5aa0b5322db2f270af)) * **tree:** improve Tree Data speed considerably ([5487798](https://github.com/ghiscoding/slickgrid-universal/commit/548779801d06cc9ae7e319e72d351c8a868ed79f)) +* **editors:** replace jQuery with native elements ([d6e8f4e](https://github.com/ghiscoding/slickgrid-universal/commit/d6e8f4e59823673df290b179d7ee277e3d7bb1af)) # [0.13.0](https://github.com/ghiscoding/slickgrid-universal/compare/v0.12.0...v0.13.0) (2021-04-27) diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index 62117dc99..44d41aaa5 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -52,7 +52,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline * add few pubsub events to help with big dataset ([360c62c](https://github.com/ghiscoding/slickgrid-universal/commit/360c62cb0979792dddef8fab39383266c0d855e3)) * add optional child value prefix to Tree Formatter ([9da9662](https://github.com/ghiscoding/slickgrid-universal/commit/9da966298120686929ab3dd2f276574d7f6c8c7e)) * **tree:** improve Tree Data speed considerably ([5487798](https://github.com/ghiscoding/slickgrid-universal/commit/548779801d06cc9ae7e319e72d351c8a868ed79f)) - +* **editors:** replace jQuery with native elements ([d6e8f4e](https://github.com/ghiscoding/slickgrid-universal/commit/d6e8f4e59823673df290b179d7ee277e3d7bb1af)) diff --git a/packages/common/src/editors/__tests__/sliderEditor.spec.ts b/packages/common/src/editors/__tests__/sliderEditor.spec.ts index 611ccd483..49f1557f9 100644 --- a/packages/common/src/editors/__tests__/sliderEditor.spec.ts +++ b/packages/common/src/editors/__tests__/sliderEditor.spec.ts @@ -202,7 +202,7 @@ describe('SliderEditor', () => { expect(editor.getValue()).toBe('213'); expect(editorElm).toBeTruthy(); - expect(editorInputElm[0].defaultValue).toBe('0'); + expect(editorInputElm.defaultValue).toBe('0'); }); it('should update slider number every time a change event happens on the input slider', () => { @@ -231,7 +231,7 @@ describe('SliderEditor', () => { editor.setValue(45); const editorElm = divContainer.querySelector('.slider-editor input.editor-price') as HTMLInputElement; - editorElm.dispatchEvent(new CustomEvent('change')); + editorElm.dispatchEvent(new Event('change')); expect(editor.isValueChanged()).toBe(true); }); @@ -241,7 +241,7 @@ describe('SliderEditor', () => { editor.loadValue(mockItemData); const editorElm = divContainer.querySelector('.slider-editor input.editor-price') as HTMLInputElement; - editorElm.dispatchEvent(new CustomEvent('change')); + editorElm.dispatchEvent(new Event('change')); expect(editor.isValueChanged()).toBe(false); }); @@ -252,7 +252,7 @@ describe('SliderEditor', () => { editor.loadValue(mockItemData); const editorElm = divContainer.querySelector('.slider-editor input.editor-price') as HTMLInputElement; - editorElm.dispatchEvent(new CustomEvent('change')); + editorElm.dispatchEvent(new Event('change')); expect(editor.isValueChanged()).toBe(false); }); @@ -264,7 +264,7 @@ describe('SliderEditor', () => { editor.loadValue(mockItemData); const editorElm = divContainer.querySelector('.slider-editor input.editor-price') as HTMLInputElement; - editorElm.dispatchEvent(new CustomEvent('change')); + editorElm.dispatchEvent(new Event('change')); expect(editor.isValueChanged()).toBe(false); }); @@ -426,8 +426,7 @@ describe('SliderEditor', () => { const spySave = jest.spyOn(editor, 'save'); const editorElm = editor.editorInputDomElement; - editorElm.trigger('mouseup'); - editorElm[0].dispatchEvent(new (window.window as any).Event('mouseup')); + editorElm.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); jest.runAllTimers(); // fast-forward timer expect(editor.isValueTouched()).toBe(true); @@ -514,8 +513,8 @@ describe('SliderEditor', () => { formValues: { price: 0 }, editors: {}, triggeredBy: 'user', }, expect.anything()); expect(disableSpy).toHaveBeenCalledWith(true); - expect(editor.editorInputDomElement.attr('disabled')).toEqual('disabled'); - expect(editor.editorInputDomElement.val()).toEqual('0'); + expect(editor.editorInputDomElement.disabled).toBeTruthy(); + expect(editor.editorInputDomElement.value).toEqual('0'); }); it('should call "show" and expect the DOM element to become disabled and empty when "onBeforeEditCell" returns false', () => { @@ -539,8 +538,8 @@ describe('SliderEditor', () => { formValues: {}, editors: {}, triggeredBy: 'user', }, expect.anything()); expect(disableSpy).toHaveBeenCalledWith(true); - expect(editor.editorInputDomElement.attr('disabled')).toEqual('disabled'); - expect(editor.editorInputDomElement.val()).toEqual('0'); + expect(editor.editorInputDomElement.disabled).toBeTruthy(); + expect(editor.editorInputDomElement.value).toEqual('0'); }); it('should expect "onCompositeEditorChange" to have been triggered with the new value showing up in its "formValues" object', () => { @@ -555,8 +554,7 @@ describe('SliderEditor', () => { editor.loadValue(mockItemData); editor.setValue(93); const editorElm = editor.editorInputDomElement; - editorElm.trigger('mouseup'); - editorElm[0].dispatchEvent(new (window.window as any).Event('mouseup')); + editorElm.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); expect(getCellSpy).toHaveBeenCalled(); expect(editor.isValueTouched()).toBe(true); diff --git a/packages/common/src/editors/sliderEditor.ts b/packages/common/src/editors/sliderEditor.ts index 0e3d700df..59617b6fd 100644 --- a/packages/common/src/editors/sliderEditor.ts +++ b/packages/common/src/editors/sliderEditor.ts @@ -1,6 +1,7 @@ import { Column, ColumnEditor, CompositeEditorOption, Editor, EditorArguments, EditorValidator, EditorValidationResult, GridOption, SlickGrid, SlickNamespace } from '../interfaces/index'; import { getDescendantProperty, setDeepValue } from '../services/utilities'; import { sliderValidator } from '../editorValidators/sliderValidator'; +import { BindingEventService } from '../services/bindingEvent.service'; const DEFAULT_MIN_VALUE = 0; const DEFAULT_MAX_VALUE = 100; @@ -14,14 +15,15 @@ declare const Slick: SlickNamespace; * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. */ export class SliderEditor implements Editor { + protected _bindEventService: BindingEventService; protected _defaultValue = 0; protected _elementRangeInputId = ''; protected _elementRangeOutputId = ''; - protected _$editorElm: any; - protected _$input: any; + protected _editorElm!: HTMLDivElement; + protected _inputElm!: HTMLInputElement; protected _isValueTouched = false; - $sliderNumber: any; - originalValue: any; + originalValue?: number | string; + sliderNumberElm: HTMLSpanElement | null = null; /** is the Editor disabled? */ disabled = false; @@ -38,6 +40,7 @@ export class SliderEditor implements Editor { } this.grid = args.grid; this.gridOptions = (this.grid.getOptions() || {}) as GridOption; + this._bindEventService = new BindingEventService(); this.init(); } @@ -57,13 +60,13 @@ export class SliderEditor implements Editor { } /** Getter for the Editor DOM Element */ - get editorDomElement(): any { - return this._$editorElm; + get editorDomElement(): HTMLDivElement { + return this._editorElm; } /** Getter for the Editor Input DOM Element */ - get editorInputDomElement(): any { - return this._$input; + get editorInputDomElement(): HTMLInputElement { + return this._inputElm; } get hasAutoCommitEdit() { @@ -91,62 +94,44 @@ export class SliderEditor implements Editor { const compositeEditorOptions = this.args.compositeEditorOptions; // create HTML string template - const editorTemplate = this.buildTemplateHtmlString(); - this._$editorElm = $(editorTemplate); - this._$input = this._$editorElm.children('input'); - this.$sliderNumber = this._$editorElm.children('div.input-group-addon.input-group-append').children(); + this._editorElm = this.buildTemplateHtml(); + this._inputElm = this._editorElm.querySelector('input') as HTMLInputElement; + this.sliderNumberElm = this._editorElm.querySelector(`span.input-group-text.${this._elementRangeOutputId}`); if (!compositeEditorOptions) { this.focus(); } // watch on change event - this._$editorElm.appendTo(container); - - this._$editorElm.on('change mouseup touchend', (event: Event) => { - this._isValueTouched = true; - if (compositeEditorOptions) { - this.handleChangeOnCompositeEditor(event, compositeEditorOptions); - } else { - this.save(); - } - }); + container.appendChild(this._editorElm); + this._bindEventService.bind(this._editorElm, ['change', 'mouseup', 'touchend'], this.handleChangeEvent.bind(this)); // if user chose to display the slider number on the right side, then update it every time it changes // we need to use both "input" and "change" event to be all cross-browser if (!this.editorParams.hideSliderNumber) { - this._$editorElm.on('input change', (event: JQuery.Event & { target: HTMLInputElement }) => { - const value = event && event.target && event.target.value || ''; - if (value && document) { - const elements = document.getElementsByClassName(this._elementRangeOutputId || ''); - if (elements && elements.length > 0 && elements[0].innerHTML) { - elements[0].innerHTML = value; - } - } - }); + this._bindEventService.bind(this._editorElm, ['input', 'change'], this.handleChangeSliderNumber.bind(this)); } } } cancel() { - this._$input.val(this.originalValue); + if (this._inputElm) { + this._inputElm.value = `${this.originalValue}`; + } this.args.cancelChanges(); } destroy() { - if (this._$editorElm) { - this._$editorElm.off('input change mouseup touchend').remove(); - this._$editorElm = null; - } + this._bindEventService.unbindAll(); } disable(isDisabled = true) { const prevIsDisabled = this.disabled; this.disabled = isDisabled; - if (this._$input) { + if (this._inputElm) { if (isDisabled) { - this._$input.attr('disabled', 'disabled'); + this._inputElm.disabled = true; // clear value when it's newly disabled and not empty const currentValue = this.getValue(); @@ -154,14 +139,14 @@ export class SliderEditor implements Editor { this.reset(0, true, true); } } else { - this._$input.removeAttr('disabled'); + this._inputElm.disabled = false; } } } focus() { - if (this._$input) { - this._$input.focus(); + if (this._inputElm) { + this._inputElm.focus(); } } @@ -174,12 +159,16 @@ export class SliderEditor implements Editor { } getValue(): string { - return this._$input.val() || ''; + return this._inputElm?.value ?? ''; } setValue(value: number | string, isApplyingValue = false, triggerOnCompositeEditorChange = true) { - this._$input.val(value); - this.$sliderNumber.html(value); + if (this._inputElm) { + this._inputElm.value = `${value}`; + } + if (this.sliderNumberElm) { + this.sliderNumberElm.textContent = `${value}`; + } if (isApplyingValue) { this.applyValue(this.args.item, this.serializeValue()); @@ -213,7 +202,7 @@ export class SliderEditor implements Editor { } isValueChanged(): boolean { - const elmValue = this._$input.val(); + const elmValue = this._inputElm?.value ?? ''; return (!(elmValue === '' && this.originalValue === undefined)) && (+elmValue !== this.originalValue); } @@ -234,8 +223,12 @@ export class SliderEditor implements Editor { value = this._defaultValue; // load default value when item doesn't have any value } this.originalValue = +value; - this._$input.val(value); - this.$sliderNumber.html(value); + if (this._inputElm) { + this._inputElm.value = `${value}`; + } + if (this.sliderNumberElm) { + this.sliderNumberElm.textContent = `${value}`; + } } } @@ -245,10 +238,9 @@ export class SliderEditor implements Editor { */ reset(value?: number | string, triggerCompositeEventWhenExist = true, clearByDisableCommand = false) { const inputValue = value ?? this.originalValue ?? 0; - if (this._$editorElm) { - this._$editorElm.children('input').val(inputValue); - this._$editorElm.children('div.input-group-addon.input-group-append').children().html(inputValue); - this._$editorElm.val(inputValue); + if (this._editorElm) { + this._editorElm.querySelector('input')!.value = `${inputValue}`; + this._editorElm.querySelector('div.input-group-addon.input-group-append')!.textContent = `${inputValue}`; } this._isValueTouched = false; @@ -273,7 +265,7 @@ export class SliderEditor implements Editor { } serializeValue() { - const elmValue: string = this._$input.val(); + const elmValue: string = this._inputElm?.value ?? ''; return elmValue !== '' ? parseInt(elmValue, 10) : this.originalValue; } @@ -288,7 +280,7 @@ export class SliderEditor implements Editor { return { valid: true, msg: '' }; } - const elmValue = (inputValue !== undefined) ? inputValue : this._$input?.val(); + const elmValue = (inputValue !== undefined) ? inputValue : this._inputElm?.value; return sliderValidator(elmValue, { editorArgs: this.args, errorMessage: this.columnEditor.errorMessage, @@ -306,7 +298,7 @@ export class SliderEditor implements Editor { /** * Create the HTML template as a string */ - protected buildTemplateHtmlString() { + protected buildTemplateHtml(): HTMLDivElement { const columnId = this.columnDef?.id ?? ''; const title = this.columnEditor && this.columnEditor.title || ''; const minValue = this.columnEditor.hasOwnProperty('minValue') ? this.columnEditor.minValue : DEFAULT_MIN_VALUE; @@ -315,24 +307,36 @@ export class SliderEditor implements Editor { const step = this.columnEditor.hasOwnProperty('valueStep') ? this.columnEditor.valueStep : DEFAULT_STEP; this._defaultValue = defaultValue; - if (this.editorParams.hideSliderNumber) { - return ` -
- -
`; + const inputElm = document.createElement('input'); + inputElm.name = this._elementRangeInputId; + inputElm.title = title; + inputElm.type = 'range'; + inputElm.defaultValue = defaultValue; + inputElm.value = defaultValue; + inputElm.min = `${minValue}`; + inputElm.max = `${maxValue}`; + inputElm.step = `${step}`; + inputElm.className = `form-control slider-editor-input editor-${columnId} range ${this._elementRangeInputId}`; + + const divContainerElm = document.createElement('div'); + divContainerElm.className = 'slider-container slider-editor'; + divContainerElm.appendChild(inputElm); + + if (!this.editorParams.hideSliderNumber) { + divContainerElm.classList.add('input-group'); + + //
${defaultValue}
+ const spanGroupElm = document.createElement('span'); + spanGroupElm.className = `input-group-text ${this._elementRangeOutputId}`; + spanGroupElm.textContent = `${defaultValue}`; + + const divGroupAddonElm = document.createElement('div'); + divGroupAddonElm.className = 'input-group-addon input-group-append slider-value'; + divGroupAddonElm.appendChild(spanGroupElm); + divContainerElm.appendChild(divGroupAddonElm); } - return ` -
- -
${defaultValue}
-
`; + return divContainerElm; } /** 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 */ @@ -344,6 +348,24 @@ export class SliderEditor implements Editor { this.disable(isCellEditable === false); } + protected handleChangeEvent(event: Event) { + this._isValueTouched = true; + const compositeEditorOptions = this.args.compositeEditorOptions; + + if (compositeEditorOptions) { + this.handleChangeOnCompositeEditor(event, compositeEditorOptions); + } else { + this.save(); + } + } + + protected handleChangeSliderNumber(event: Event) { + const value = (event.target)?.value ?? ''; + if (value !== '' && this.sliderNumberElm) { + this.sliderNumberElm.textContent = value; + } + } + protected handleChangeOnCompositeEditor(event: Event | null, compositeEditorOptions: CompositeEditorOption, triggeredBy: 'user' | 'system' = 'user', isCalledByClearValue = false) { const activeCell = this.grid.getActiveCell(); const column = this.args.column; diff --git a/packages/common/src/services/bindingEvent.service.ts b/packages/common/src/services/bindingEvent.service.ts index 16f26dd7b..31eea1bbe 100644 --- a/packages/common/src/services/bindingEvent.service.ts +++ b/packages/common/src/services/bindingEvent.service.ts @@ -4,9 +4,12 @@ export class BindingEventService { private _boundedEvents: ElementEventListener[] = []; /** Bind an event listener to any element */ - bind(element: Element, eventName: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) { - element.addEventListener(eventName, listener, options); - this._boundedEvents.push({ element, eventName, listener }); + bind(element: Element, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) { + const eventNames = (Array.isArray(eventNameOrNames)) ? eventNameOrNames : [eventNameOrNames]; + for (const eventName of eventNames) { + element.addEventListener(eventName, listener, options); + this._boundedEvents.push({ element, eventName, listener }); + } } /** Unbind all will remove every every event handlers that were bounded earlier */