diff --git a/packages/common/src/editors/checkboxEditor.ts b/packages/common/src/editors/checkboxEditor.ts index 0be25b910..a12723d39 100644 --- a/packages/common/src/editors/checkboxEditor.ts +++ b/packages/common/src/editors/checkboxEditor.ts @@ -1,5 +1,6 @@ import { Constants } from './../constants'; import { Column, ColumnEditor, CompositeEditorOption, Editor, EditorArguments, EditorValidator, EditorValidationResult, GridOption, SlickGrid, SlickNamespace } from './../interfaces/index'; +import { createDomElement } from '../services/domUtilities'; import { getDescendantProperty, setDeepValue, toSentenceCase } from '../services/utilities'; import { BindingEventService } from '../services/bindingEvent.service'; @@ -67,17 +68,16 @@ export class CheckboxEditor implements Editor { init(): void { const columnId = this.columnDef?.id ?? ''; - const title = this.columnEditor?.title ?? ''; const compositeEditorOptions = this.args.compositeEditorOptions; this._checkboxContainerElm = document.createElement('div'); this._checkboxContainerElm.className = `checkbox-editor-container editor-${columnId}`; - this._input = document.createElement('input'); - this._input.className = `editor-checkbox editor-${columnId}`; - this._input.title = title; - this._input.type = 'checkbox'; - this._input.value = 'true'; + this._input = createDomElement('input', { + type: 'checkbox', value: 'true', + className: `editor-checkbox editor-${columnId}`, + title: this.columnEditor?.title ?? '', + }); this._input.setAttribute('aria-label', this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Checkbox Editor`); const cellContainer = this.args?.container; diff --git a/packages/common/src/editors/dateEditor.ts b/packages/common/src/editors/dateEditor.ts index 46f805e2a..8bd8529cd 100644 --- a/packages/common/src/editors/dateEditor.ts +++ b/packages/common/src/editors/dateEditor.ts @@ -21,6 +21,7 @@ import { SlickNamespace, } from './../interfaces/index'; import { + createDomElement, destroyObjectDomElementProps, emptyElement, } from '../services/domUtilities'; @@ -121,8 +122,6 @@ export class DateEditor implements Editor { if (this.args && this.columnDef) { const compositeEditorOptions = this.args.compositeEditorOptions; const columnId = this.columnDef?.id ?? ''; - const placeholder = this.columnEditor?.placeholder ?? ''; - const title = this.columnEditor && this.columnEditor.title || ''; const gridOptions = (this.args.grid.getOptions() || {}) as GridOption; this.defaultDate = (this.args.item) ? this.args.item[this.columnDef.field] : null; const inputFormat = mapFlatpickrDateFormatWithFieldType(this.columnEditor.type || this.columnDef.type || FieldType.dateUtc); @@ -158,23 +157,15 @@ export class DateEditor implements Editor { this._pickerMergedOptions.altInputClass = 'flatpickr-alt-input form-control'; } - this._editorInputGroupElm = document.createElement('div'); - this._editorInputGroupElm.className = 'flatpickr input-group'; - - const closeButtonGroupElm = document.createElement('span'); - closeButtonGroupElm.className = 'input-group-btn input-group-append'; - closeButtonGroupElm.dataset.clear = ''; - - this._closeButtonElm = document.createElement('button'); - this._closeButtonElm.type = 'button'; - this._closeButtonElm.className = 'btn btn-default icon-clear'; - - this._inputElm = document.createElement('input'); - this._inputElm.dataset.input = ''; - this._inputElm.dataset.defaultdate = this.defaultDate; - this._inputElm.className = inputCssClasses.replace(/\./g, ' '); - this._inputElm.placeholder = placeholder; - this._inputElm.title = title; + this._editorInputGroupElm = createDomElement('div', { className: 'flatpickr input-group' }); + const closeButtonGroupElm = createDomElement('span', { className: 'input-group-btn input-group-append', dataset: { clear: '' } }); + this._closeButtonElm = createDomElement('button', { type: 'button', className: 'btn btn-default icon-clear' }); + this._inputElm = createDomElement('input', { + placeholder: this.columnEditor?.placeholder ?? '', + title: this.columnEditor && this.columnEditor.title || '', + className: inputCssClasses.replace(/\./g, ' '), + dataset: { input: '', defaultdate: this.defaultDate } + }); this._editorInputGroupElm.appendChild(this._inputElm); diff --git a/packages/common/src/editors/dualInputEditor.ts b/packages/common/src/editors/dualInputEditor.ts index 1ec851ff3..945b09012 100644 --- a/packages/common/src/editors/dualInputEditor.ts +++ b/packages/common/src/editors/dualInputEditor.ts @@ -17,6 +17,7 @@ import { import { getDescendantProperty, setDeepValue, toSentenceCase } from '../services/utilities'; import { floatValidator, integerValidator, textValidator } from '../editorValidators'; import { BindingEventService } from '../services/bindingEvent.service'; +import { createDomElement } from '../services/domUtilities'; // using external non-typed js libraries declare const Slick: SlickNamespace; @@ -187,20 +188,22 @@ export class DualInputEditor implements Editor { fieldType = 'number'; } - const input = document.createElement('input') as HTMLInputElement; - input.id = `item-${itemId}-${position}`; - input.className = `dual-editor-text editor-${columnId} ${position.replace(/input/gi, '')}`; + const input = createDomElement('input', { + type: fieldType || 'text', + id: `item-${itemId}-${position}`, + className: `dual-editor-text editor-${columnId} ${position.replace(/input/gi, '')}`, + autocomplete: 'off', + placeholder: editorSideParams.placeholder || '', + title: editorSideParams.title || '', + }); + input.setAttribute('role', 'presentation'); + input.setAttribute('aria-label', this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Input Editor`); + if (fieldType === 'readonly') { // when the custom type is defined as readonly, we'll make a readonly text input input.readOnly = true; fieldType = 'text'; } - input.type = fieldType || 'text'; - input.setAttribute('role', 'presentation'); - input.setAttribute('aria-label', this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Input Editor`); - input.autocomplete = 'off'; - input.placeholder = editorSideParams.placeholder || ''; - input.title = editorSideParams.title || ''; if (fieldType === 'number') { input.step = this.getInputDecimalSteps(position); } diff --git a/packages/common/src/editors/floatEditor.ts b/packages/common/src/editors/floatEditor.ts index bb1ca88c3..61f30199c 100644 --- a/packages/common/src/editors/floatEditor.ts +++ b/packages/common/src/editors/floatEditor.ts @@ -2,6 +2,7 @@ import { KeyCode } from '../enums/index'; import { EditorArguments, EditorValidationResult } from '../interfaces/index'; import { floatValidator } from '../editorValidators/floatValidator'; import { InputEditor } from './inputEditor'; +import { createDomElement } from '../services/domUtilities'; import { getDescendantProperty, toSentenceCase } from '../services/utilities'; const DEFAULT_DECIMAL_PLACES = 0; @@ -15,19 +16,16 @@ export class FloatEditor extends InputEditor { init() { if (this.columnDef && this.columnEditor && this.args) { const columnId = this.columnDef?.id ?? ''; - const placeholder = this.columnEditor?.placeholder ?? ''; - const title = this.columnEditor?.title ?? ''; - const inputStep = (this.columnEditor.valueStep !== undefined) ? this.columnEditor.valueStep : this.getInputDecimalSteps(); const compositeEditorOptions = this.args.compositeEditorOptions; - this._input = document.createElement('input') as HTMLInputElement; - this._input.className = `editor-text editor-${columnId}`; - this._input.type = 'number'; + this._input = createDomElement('input', { + type: 'number', autocomplete: 'off', + className: `editor-text editor-${columnId}`, + placeholder: this.columnEditor?.placeholder ?? '', + title: this.columnEditor?.title ?? '', + step: `${(this.columnEditor.valueStep !== undefined) ? this.columnEditor.valueStep : this.getInputDecimalSteps()}`, + }); this._input.setAttribute('role', 'presentation'); - this._input.autocomplete = 'off'; - this._input.placeholder = placeholder; - this._input.title = title; - this._input.step = `${inputStep}`; this._input.setAttribute('aria-label', this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Number Editor`); const cellContainer = this.args.container; if (cellContainer && typeof cellContainer.appendChild === 'function') { diff --git a/packages/common/src/editors/inputEditor.ts b/packages/common/src/editors/inputEditor.ts index b8cfa674f..21e8fa374 100644 --- a/packages/common/src/editors/inputEditor.ts +++ b/packages/common/src/editors/inputEditor.ts @@ -3,6 +3,7 @@ import { Column, ColumnEditor, CompositeEditorOption, Editor, EditorArguments, E import { getDescendantProperty, setDeepValue, toSentenceCase } from '../services/utilities'; import { textValidator } from '../editorValidators/textValidator'; import { BindingEventService } from '../services/bindingEvent.service'; +import { createDomElement } from '../services/domUtilities'; // using external non-typed js libraries declare const Slick: SlickNamespace; @@ -81,17 +82,16 @@ export class InputEditor implements Editor { init() { const columnId = this.columnDef?.id ?? ''; - const placeholder = this.columnEditor?.placeholder ?? ''; - const title = this.columnEditor?.title ?? ''; const compositeEditorOptions = this.args.compositeEditorOptions; - this._input = document.createElement('input') as HTMLInputElement; - this._input.className = `editor-text editor-${columnId}`; - this._input.type = this._inputType || 'text'; + this._input = createDomElement('input', { + type: this._inputType || 'text', + autocomplete: 'off', + placeholder: this.columnEditor?.placeholder ?? '', + title: this.columnEditor?.title ?? '', + className: `editor-text editor-${columnId}`, + }); this._input.setAttribute('role', 'presentation'); - this._input.autocomplete = 'off'; - this._input.placeholder = placeholder; - this._input.title = title; this._input.setAttribute('aria-label', this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Input Editor`); const cellContainer = this.args.container; if (cellContainer && typeof cellContainer.appendChild === 'function') { diff --git a/packages/common/src/editors/integerEditor.ts b/packages/common/src/editors/integerEditor.ts index 40ae8b751..de4dd63ea 100644 --- a/packages/common/src/editors/integerEditor.ts +++ b/packages/common/src/editors/integerEditor.ts @@ -2,6 +2,7 @@ import { KeyCode } from '../enums/index'; import { EditorArguments, EditorValidationResult } from '../interfaces/index'; import { integerValidator } from '../editorValidators/integerValidator'; import { InputEditor } from './inputEditor'; +import { createDomElement } from '../services/domUtilities'; import { getDescendantProperty, toSentenceCase } from '../services/utilities'; export class IntegerEditor extends InputEditor { @@ -13,19 +14,16 @@ export class IntegerEditor extends InputEditor { init() { if (this.columnDef && this.columnEditor && this.args) { const columnId = this.columnDef?.id ?? ''; - const placeholder = this.columnEditor?.placeholder ?? ''; - const title = this.columnEditor?.title ?? ''; - const inputStep = (this.columnEditor.valueStep !== undefined) ? this.columnEditor.valueStep : '1'; const compositeEditorOptions = this.args.compositeEditorOptions; - this._input = document.createElement('input') as HTMLInputElement; - this._input.className = `editor-text editor-${columnId}`; - this._input.type = 'number'; + this._input = createDomElement('input', { + type: 'number', autocomplete: 'off', + placeholder: this.columnEditor?.placeholder ?? '', + title: this.columnEditor?.title ?? '', + step: `${(this.columnEditor.valueStep !== undefined) ? this.columnEditor.valueStep : '1'}`, + className: `editor-text editor-${columnId}`, + }); this._input.setAttribute('role', 'presentation'); - this._input.autocomplete = 'off'; - this._input.placeholder = placeholder; - this._input.title = title; - this._input.step = `${inputStep}`; this._input.setAttribute('aria-label', this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Slider Editor`); const cellContainer = this.args.container; if (cellContainer && typeof cellContainer.appendChild === 'function') { diff --git a/packages/common/src/editors/longTextEditor.ts b/packages/common/src/editors/longTextEditor.ts index 392788e8f..2695d30b6 100644 --- a/packages/common/src/editors/longTextEditor.ts +++ b/packages/common/src/editors/longTextEditor.ts @@ -16,7 +16,7 @@ import { SlickGrid, SlickNamespace, } from '../interfaces/index'; -import { getHtmlElementOffset, } from '../services/domUtilities'; +import { createDomElement, getHtmlElementOffset, } from '../services/domUtilities'; import { getDescendantProperty, getTranslationPrefix, setDeepValue, toSentenceCase, } from '../services/utilities'; import { BindingEventService } from '../services/bindingEvent.service'; import { TranslaterService } from '../services/translater.service'; @@ -119,57 +119,42 @@ export class LongTextEditor implements Editor { const compositeEditorOptions = this.args.compositeEditorOptions; const columnId = this.columnDef?.id ?? ''; - const placeholder = this.columnEditor?.placeholder ?? ''; - const title = this.columnEditor?.title ?? ''; const maxLength = this.columnEditor?.maxLength; - const textAreaCols = this.editorOptions?.cols ?? 40; const textAreaRows = this.editorOptions?.rows ?? 4; const containerElm = compositeEditorOptions ? this.args.container : document.body; - this._wrapperElm = document.createElement('div'); - this._wrapperElm.className = `slick-large-editor-text editor-${columnId}`; + this._wrapperElm = createDomElement('div', { className: `slick-large-editor-text editor-${columnId}` }); this._wrapperElm.style.position = compositeEditorOptions ? 'relative' : 'absolute'; containerElm.appendChild(this._wrapperElm); - this._textareaElm = document.createElement('textarea'); - this._textareaElm.cols = textAreaCols; // use textarea row if defined but don't go over 3 rows with composite editor modal - this._textareaElm.rows = (compositeEditorOptions && textAreaRows > 3) ? 3 : textAreaRows; - this._textareaElm.placeholder = placeholder; - this._textareaElm.title = title; + this._textareaElm = createDomElement('textarea', { + cols: this.editorOptions?.cols ?? 40, + rows: (compositeEditorOptions && textAreaRows > 3) ? 3 : textAreaRows, + placeholder: this.columnEditor?.placeholder ?? '', + title: this.columnEditor?.title ?? '', + }); this._textareaElm.setAttribute('aria-label', this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Text Editor`); this._wrapperElm.appendChild(this._textareaElm); - const editorFooterElm = document.createElement('div'); - editorFooterElm.className = 'editor-footer'; - - const countContainerElm = document.createElement('span'); - countContainerElm.className = 'counter'; - - this._currentLengthElm = document.createElement('span'); - this._currentLengthElm.className = 'text-length'; - this._currentLengthElm.textContent = '0'; + const editorFooterElm = createDomElement('div', { className: 'editor-footer' }); + const countContainerElm = createDomElement('span', { className: 'counter' }); + this._currentLengthElm = createDomElement('span', { className: 'text-length', textContent: '0' }); countContainerElm.appendChild(this._currentLengthElm); if (maxLength !== undefined) { - const maxLengthSeparatorElm = document.createElement('span'); - maxLengthSeparatorElm.className = 'separator'; - maxLengthSeparatorElm.textContent = '/'; - const maxLengthElm = document.createElement('span'); - maxLengthElm.className = 'max-length'; - maxLengthElm.textContent = `${maxLength}`; - countContainerElm.appendChild(maxLengthSeparatorElm); - countContainerElm.appendChild(maxLengthElm); + countContainerElm.appendChild( + createDomElement('span', { className: 'separator', textContent: '/' }) + ); + countContainerElm.appendChild( + createDomElement('span', { className: 'max-length', textContent: `${maxLength}` }) + ); } editorFooterElm.appendChild(countContainerElm); if (!compositeEditorOptions) { - const cancelBtnElm = document.createElement('button'); - cancelBtnElm.className = 'btn btn-cancel btn-default btn-xs'; - cancelBtnElm.textContent = cancelText; - const saveBtnElm = document.createElement('button'); - saveBtnElm.className = 'btn btn-save btn-primary btn-xs'; - saveBtnElm.textContent = saveText; + const cancelBtnElm = createDomElement('button', { className: 'btn btn-cancel btn-default btn-xs', textContent: cancelText }); + const saveBtnElm = createDomElement('button', { className: 'btn btn-save btn-primary btn-xs', textContent: saveText }); editorFooterElm.appendChild(cancelBtnElm); editorFooterElm.appendChild(saveBtnElm); this._bindEventService.bind(cancelBtnElm, 'click', this.cancel.bind(this) as EventListener); diff --git a/packages/common/src/editors/sliderEditor.ts b/packages/common/src/editors/sliderEditor.ts index e8d6eefa9..8f4be7458 100644 --- a/packages/common/src/editors/sliderEditor.ts +++ b/packages/common/src/editors/sliderEditor.ts @@ -2,6 +2,7 @@ import { Column, ColumnEditor, CompositeEditorOption, Editor, EditorArguments, E import { getDescendantProperty, setDeepValue, toSentenceCase } from '../services/utilities'; import { sliderValidator } from '../editorValidators/sliderValidator'; import { BindingEventService } from '../services/bindingEvent.service'; +import { createDomElement } from '../services/domUtilities'; const DEFAULT_MIN_VALUE = 0; const DEFAULT_MAX_VALUE = 100; @@ -305,36 +306,27 @@ export class SliderEditor implements Editor { const minValue = this.columnEditor.hasOwnProperty('minValue') ? this.columnEditor.minValue : DEFAULT_MIN_VALUE; const maxValue = this.columnEditor.hasOwnProperty('maxValue') ? this.columnEditor.maxValue : DEFAULT_MAX_VALUE; const defaultValue = this.editorParams.hasOwnProperty('sliderStartValue') ? this.editorParams.sliderStartValue : minValue; - const step = this.columnEditor.hasOwnProperty('valueStep') ? this.columnEditor.valueStep : DEFAULT_STEP; this._defaultValue = defaultValue; - 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 inputElm = createDomElement('input', { + type: 'range', name: this._elementRangeInputId, title, + defaultValue, value: defaultValue, min: `${minValue}`, max: `${maxValue}`, + step: `${this.columnEditor.hasOwnProperty('valueStep') ? this.columnEditor.valueStep : DEFAULT_STEP}`, + className: `form-control slider-editor-input editor-${columnId} range ${this._elementRangeInputId}`, + }); inputElm.setAttribute('aria-label', this.columnEditor?.ariaLabel ?? `${toSentenceCase(columnId + '')} Slider Editor`); - const divContainerElm = document.createElement('div'); - divContainerElm.className = 'slider-container slider-editor'; + const divContainerElm = createDomElement('div', { 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); + const divGroupAddonElm = createDomElement('div', { className: 'input-group-addon input-group-append slider-value' }); + divGroupAddonElm.appendChild( + createDomElement('span', { className: `input-group-text ${this._elementRangeOutputId}`, textContent: `${defaultValue}` }) + ); divContainerElm.appendChild(divGroupAddonElm); } diff --git a/packages/common/src/enums/index.ts b/packages/common/src/enums/index.ts index 83725b536..52f6bc429 100644 --- a/packages/common/src/enums/index.ts +++ b/packages/common/src/enums/index.ts @@ -12,6 +12,7 @@ export * from './filterMultiplePassType.enum'; export * from './filterMultiplePassTypeString.type'; export * from './gridAutosizeColsMode.enum'; export * from './gridStateType.enum'; +export * from './infer.type'; export * from './keyCode.enum'; export * from './operatorString.type'; export * from './operatorType.enum'; diff --git a/packages/common/src/enums/infer.type.ts b/packages/common/src/enums/infer.type.ts new file mode 100644 index 000000000..337a66115 --- /dev/null +++ b/packages/common/src/enums/infer.type.ts @@ -0,0 +1 @@ +export type InferType = T extends infer R ? R : any; \ No newline at end of file diff --git a/packages/common/src/extensions/extensionCommonUtils.ts b/packages/common/src/extensions/extensionCommonUtils.ts index 7dd20da1f..4963fecb2 100644 --- a/packages/common/src/extensions/extensionCommonUtils.ts +++ b/packages/common/src/extensions/extensionCommonUtils.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-this-alias */ import { Column, ColumnPickerOption, DOMEvent, GridMenuOption } from '../interfaces/index'; -import { sanitizeTextByAvailableSanitizer } from '../services/domUtilities'; +import { createDomElement, sanitizeTextByAvailableSanitizer } from '../services/domUtilities'; import { SlickColumnPicker } from './slickColumnPicker'; import { SlickGridMenu } from './slickGridMenu'; import { titleCase } from '../services/utilities'; @@ -8,15 +8,13 @@ import { titleCase } from '../services/utilities'; /** Create a Close button element and add it to the Menu element */ export function addCloseButtomElement(this: SlickColumnPicker | SlickGridMenu, menuElm: HTMLDivElement) { const context: any = this; - const closePickerButtonElm = document.createElement('button'); - closePickerButtonElm.className = 'close'; - closePickerButtonElm.type = 'button'; - closePickerButtonElm.dataset.dismiss = context instanceof SlickColumnPicker ? 'slick-columnpicker' : 'slick-grid-menu'; + const closePickerButtonElm = createDomElement('button', { + type: 'button', className: 'close', + dataset: { dismiss: context instanceof SlickColumnPicker ? 'slick-columnpicker' : 'slick-grid-menu' } + }); closePickerButtonElm.setAttribute('aria-label', 'Close'); - const closeSpanElm = document.createElement('span'); - closeSpanElm.className = 'close'; - closeSpanElm.innerHTML = '×'; + const closeSpanElm = createDomElement('span', { className: 'close', innerHTML: '×' }); closeSpanElm.setAttribute('aria-hidden', 'true'); closePickerButtonElm.appendChild(closeSpanElm); menuElm.appendChild(closePickerButtonElm); @@ -26,9 +24,7 @@ export function addCloseButtomElement(this: SlickColumnPicker | SlickGridMenu, m export function addColumnTitleElementWhenDefined(this: SlickColumnPicker | SlickGridMenu, menuElm: HTMLDivElement) { const context: any = this; if (context.addonOptions?.columnTitle) { - context._columnTitleElm = document.createElement('div'); - context._columnTitleElm.className = 'title'; - context._columnTitleElm.textContent = context.addonOptions?.columnTitle ?? context._defaults.columnTitle; + context._columnTitleElm = createDomElement('div', { className: 'title', textContent: context.addonOptions?.columnTitle ?? context._defaults.columnTitle }); menuElm.appendChild(context._columnTitleElm); } } @@ -121,13 +117,12 @@ export function populateColumnPicker(this: SlickColumnPicker | SlickGridMenu, ad for (const column of context.columns) { const columnId = column.id; - const columnLiElm = document.createElement('li'); - columnLiElm.className = column.excludeFromColumnPicker ? 'hidden' : ''; + const columnLiElm = createDomElement('li', { className: column.excludeFromColumnPicker ? 'hidden' : '' }); - const colInputElm = document.createElement('input'); - colInputElm.type = 'checkbox'; - colInputElm.id = `${context._gridUid}-${menuPrefix}colpicker-${columnId}`; - colInputElm.dataset.columnid = `${columnId}`; + const colInputElm = createDomElement('input', { + type: 'checkbox', id: `${context._gridUid}-${menuPrefix}colpicker-${columnId}`, + dataset: { columnid: `${columnId}` } + }); const colIndex = context.grid.getColumnIndex(columnId); if (colIndex >= 0) { colInputElm.checked = true; @@ -138,10 +133,12 @@ export function populateColumnPicker(this: SlickColumnPicker | SlickGridMenu, ad const headerColumnValueExtractorFn = typeof addonOptions?.headerColumnValueExtractor === 'function' ? addonOptions.headerColumnValueExtractor : context._defaults.headerColumnValueExtractor; const columnLabel = headerColumnValueExtractorFn!(column, context.gridOptions); - const labelElm = document.createElement('label'); - labelElm.htmlFor = `${context._gridUid}-${menuPrefix}colpicker-${columnId}`; - labelElm.innerHTML = sanitizeTextByAvailableSanitizer(context.gridOptions, columnLabel); - columnLiElm.appendChild(labelElm); + columnLiElm.appendChild( + createDomElement('label', { + htmlFor: `${context._gridUid}-${menuPrefix}colpicker-${columnId}`, + innerHTML: sanitizeTextByAvailableSanitizer(context.gridOptions, columnLabel), + }) + ); context._listElm.appendChild(columnLiElm); } @@ -150,43 +147,38 @@ export function populateColumnPicker(this: SlickColumnPicker | SlickGridMenu, ad } if (!(addonOptions?.hideForceFitButton)) { - const forceFitTitle = addonOptions?.forceFitTitle; - - const fitInputElm = document.createElement('input'); - fitInputElm.type = 'checkbox'; - fitInputElm.id = `${context._gridUid}-${menuPrefix}colpicker-forcefit`; - fitInputElm.dataset.option = 'autoresize'; - - const labelElm = document.createElement('label'); - labelElm.htmlFor = `${context._gridUid}-${menuPrefix}colpicker-forcefit`; - labelElm.textContent = forceFitTitle ?? ''; - if (context.gridOptions.forceFitColumns) { - fitInputElm.checked = true; - } - - const fitLiElm = document.createElement('li'); - fitLiElm.appendChild(fitInputElm); - fitLiElm.appendChild(labelElm); + const fitLiElm = createDomElement('li'); + fitLiElm.appendChild( + createDomElement('input', { + type: 'checkbox', id: `${context._gridUid}-${menuPrefix}colpicker-forcefit`, + checked: context.gridOptions.forceFitColumns, + dataset: { option: 'autoresize' } + }) + ); + fitLiElm.appendChild( + createDomElement('label', { + htmlFor: `${context._gridUid}-${menuPrefix}colpicker-forcefit`, + textContent: addonOptions?.forceFitTitle ?? '', + }) + ); context._listElm.appendChild(fitLiElm); } if (!(addonOptions?.hideSyncResizeButton)) { - const syncResizeTitle = (addonOptions?.syncResizeTitle) || addonOptions.syncResizeTitle; - const labelElm = document.createElement('label'); - labelElm.htmlFor = `${context._gridUid}-${menuPrefix}colpicker-syncresize`; - labelElm.textContent = syncResizeTitle ?? ''; - - const syncInputElm = document.createElement('input'); - syncInputElm.type = 'checkbox'; - syncInputElm.id = `${context._gridUid}-${menuPrefix}colpicker-syncresize`; - syncInputElm.dataset.option = 'syncresize'; - if (context.gridOptions.syncColumnCellResize) { - syncInputElm.checked = true; - } - const syncLiElm = document.createElement('li'); - syncLiElm.appendChild(syncInputElm); - syncLiElm.appendChild(labelElm); + syncLiElm.appendChild( + createDomElement('input', { + type: 'checkbox', id: `${context._gridUid}-${menuPrefix}colpicker-syncresize`, + checked: context.gridOptions.syncColumnCellResize, + dataset: { option: 'syncresize' } + }) + ); + syncLiElm.appendChild( + createDomElement('label', { + htmlFor: `${context._gridUid}-${menuPrefix}colpicker-syncresize`, + textContent: addonOptions?.syncResizeTitle ?? '' + }) + ); context._listElm.appendChild(syncLiElm); } } diff --git a/packages/common/src/extensions/menuBaseClass.ts b/packages/common/src/extensions/menuBaseClass.ts index 0a58b6ba8..b995373d4 100644 --- a/packages/common/src/extensions/menuBaseClass.ts +++ b/packages/common/src/extensions/menuBaseClass.ts @@ -18,6 +18,7 @@ import { BindingEventService } from '../services/bindingEvent.service'; import { ExtensionUtility } from '../extensions/extensionUtility'; import { PubSubService } from '../services/pubSub.service'; import { SharedService } from '../services/shared.service'; +import { createDomElement } from '../services/domUtilities'; import { hasData, toSentenceCase } from '../services/utilities'; // using external SlickGrid JS libraries @@ -121,9 +122,10 @@ export class MenuBaseClass extends Men this._menuElm.style.left = `${event.pageX}px`; this._menuElm.style.display = 'none'; - const closeButtonElm = document.createElement('button'); - closeButtonElm.className = 'close'; - closeButtonElm.type = 'button'; - closeButtonElm.dataset.dismiss = this._menuCssPrefix; + const closeButtonElm = createDomElement('button', { className: 'close', type: 'button', dataset: { dismiss: this._menuCssPrefix } }); closeButtonElm.setAttribute('aria-label', 'Close'); - const closeSpanElm = document.createElement('span'); - closeSpanElm.className = 'close'; - closeSpanElm.innerHTML = '×'; + const closeSpanElm = createDomElement('span', { className: 'close', innerHTML: '×' }); closeSpanElm.setAttribute('aria-hidden', 'true'); closeButtonElm.appendChild(closeSpanElm); // -- Option List section if (!(this.addonOptions as CellMenu | ContextMenu).hideOptionSection && isColumnOptionAllowed && optionItems.length > 0) { - const optionMenuElm = document.createElement('div'); - optionMenuElm.className = `${this._menuCssPrefix}-option-list`; + const optionMenuElm = createDomElement('div', { className: `${this._menuCssPrefix}-option-list` }); if (!this.addonOptions.hideCloseButton) { this._bindEventService.bind(closeButtonElm, 'click', ((e: DOMMouseEvent) => this.handleCloseButtonClicked(e)) as EventListener); this._menuElm.appendChild(closeButtonElm); @@ -127,8 +121,7 @@ export class MenuFromCellBaseClass extends Men // -- Command List section if (!(this.addonOptions as CellMenu | ContextMenu).hideCommandSection && isColumnCommandAllowed && commandItems.length > 0) { - const commandMenuElm = document.createElement('div'); - commandMenuElm.className = `${this._menuCssPrefix}-command-list`; + const commandMenuElm = createDomElement('div', { className: `${this._menuCssPrefix}-command-list` }); if (!this.addonOptions.hideCloseButton && (!isColumnOptionAllowed || optionItems.length === 0 || (this.addonOptions as CellMenu | ContextMenu).hideOptionSection)) { this._bindEventService.bind(closeButtonElm, 'click', ((e: DOMMouseEvent) => this.handleCloseButtonClicked(e)) as EventListener); this._menuElm.appendChild(closeButtonElm); diff --git a/packages/common/src/extensions/slickCellRangeDecorator.ts b/packages/common/src/extensions/slickCellRangeDecorator.ts index 48bb06652..7c1d2d9e5 100644 --- a/packages/common/src/extensions/slickCellRangeDecorator.ts +++ b/packages/common/src/extensions/slickCellRangeDecorator.ts @@ -2,6 +2,7 @@ import * as assign_ from 'assign-deep'; const assign = (assign_ as any)['default'] || assign_; import { CellRange, CellRangeDecoratorOption, CSSStyleDeclarationWritable, SlickGrid } from '../interfaces/index'; +import { createDomElement } from '../services/domUtilities'; /** * Displays an overlay on top of a given cell range. @@ -54,8 +55,7 @@ export class SlickCellRangeDecorator { show(range: CellRange) { if (!this._elem) { - this._elem = document.createElement('div'); - this._elem.className = this._addonOptions.selectionCssClass; + this._elem = createDomElement('div', { className: this._addonOptions.selectionCssClass }); Object.keys(this._addonOptions.selectionCss as CSSStyleDeclaration).forEach((cssStyleKey) => { this._elem!.style[cssStyleKey as CSSStyleDeclarationWritable] = this._addonOptions.selectionCss[cssStyleKey as CSSStyleDeclarationWritable]; }); diff --git a/packages/common/src/extensions/slickCheckboxSelectColumn.ts b/packages/common/src/extensions/slickCheckboxSelectColumn.ts index 2d220c773..8daaa3029 100644 --- a/packages/common/src/extensions/slickCheckboxSelectColumn.ts +++ b/packages/common/src/extensions/slickCheckboxSelectColumn.ts @@ -1,7 +1,7 @@ import { KeyCode } from '../enums/keyCode.enum'; import { CheckboxSelectorOption, Column, GridOption, SelectableOverrideCallback, SlickEventData, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index'; import { SlickRowSelectionModel } from './slickRowSelectionModel'; -import { emptyElement } from '../services/domUtilities'; +import { createDomElement, emptyElement } from '../services/domUtilities'; import { BindingEventService } from '../services/bindingEvent.service'; // using external SlickGrid JS libraries @@ -236,18 +236,13 @@ export class SlickCheckboxSelectColumn { emptyElement(args.node); // - const spanElm = document.createElement('span'); - spanElm.id = 'filter-checkbox-selectall-container'; - - const inputElm = document.createElement('input'); - inputElm.type = 'checkbox'; - inputElm.id = `header-filter-selector${this._selectAll_UID}`; - - const labelElm = document.createElement('label'); - labelElm.htmlFor = `header-filter-selector${this._selectAll_UID}`; - - spanElm.appendChild(inputElm); - spanElm.appendChild(labelElm); + const spanElm = createDomElement('span', { id: 'filter-checkbox-selectall-container' }); + spanElm.appendChild( + createDomElement('input', { type: 'checkbox', id: `header-filter-selector${this._selectAll_UID}` }) + ); + spanElm.appendChild( + createDomElement('label', { htmlFor: `header-filter-selector${this._selectAll_UID}` }) + ); args.node.appendChild(spanElm); this._headerRowNode = args.node; diff --git a/packages/common/src/extensions/slickColumnPicker.ts b/packages/common/src/extensions/slickColumnPicker.ts index e5b2fc810..b8615b039 100644 --- a/packages/common/src/extensions/slickColumnPicker.ts +++ b/packages/common/src/extensions/slickColumnPicker.ts @@ -11,7 +11,7 @@ import { ExtensionUtility } from '../extensions/extensionUtility'; import { BindingEventService } from '../services/bindingEvent.service'; import { PubSubService } from '../services/pubSub.service'; import { SharedService } from '../services/shared.service'; -import { emptyElement, findWidthOrDefault } from '../services/domUtilities'; +import { createDomElement, emptyElement, findWidthOrDefault } from '../services/domUtilities'; import { addColumnTitleElementWhenDefined, addCloseButtomElement, handleColumnPickerItemClick, populateColumnPicker, updateColumnPickerOrder } from '../extensions/extensionCommonUtils'; // using external SlickGrid JS libraries @@ -101,8 +101,7 @@ export class SlickColumnPicker { this._eventHandler.subscribe(this.grid.onHeaderContextMenu, this.handleHeaderContextMenu.bind(this) as EventListener); this._eventHandler.subscribe(this.grid.onColumnsReordered, updateColumnPickerOrder.bind(this) as EventListener); - this._menuElm = document.createElement('div'); - this._menuElm.className = `slick-columnpicker ${this._gridUid}`; + this._menuElm = createDomElement('div', { className: `slick-columnpicker ${this._gridUid}` }); this._menuElm.setAttribute('aria-expanded', 'false'); this._menuElm.style.display = 'none'; @@ -110,11 +109,9 @@ export class SlickColumnPicker { addCloseButtomElement.call(this, this._menuElm); addColumnTitleElementWhenDefined.call(this, this._menuElm); + this._listElm = createDomElement('span', { className: 'slick-columnpicker-list' }); this._bindEventService.bind(this._menuElm, 'click', handleColumnPickerItemClick.bind(this) as EventListener); - this._listElm = document.createElement('span'); - this._listElm.className = 'slick-columnpicker-list'; - // Hide the menu on outside click. this._bindEventService.bind(document.body, 'mousedown', this.handleBodyMouseDown.bind(this) as EventListener); diff --git a/packages/common/src/extensions/slickDraggableGrouping.ts b/packages/common/src/extensions/slickDraggableGrouping.ts index dd457d99e..72c35567e 100644 --- a/packages/common/src/extensions/slickDraggableGrouping.ts +++ b/packages/common/src/extensions/slickDraggableGrouping.ts @@ -18,7 +18,7 @@ import { import { BindingEventService } from '../services/bindingEvent.service'; import { PubSubService } from '../services/pubSub.service'; import { SharedService } from '../services/shared.service'; -import { emptyElement } from '../services/domUtilities'; +import { createDomElement, emptyElement } from '../services/domUtilities'; import { isEmptyObject } from '../services/utilities'; // using external SlickGrid JS libraries @@ -155,11 +155,9 @@ export class SlickDraggableGrouping { // add optional group "Toggle All" with its button & text when provided if (!this._addonOptions.hideToggleAllButton) { - this._groupToggler = document.createElement('div'); - this._groupToggler.className = 'slick-group-toggle-all'; + this._groupToggler = createDomElement('div', { className: 'slick-group-toggle-all', title: this._addonOptions.toggleAllPlaceholderText ?? '' }); this._groupToggler.style.display = 'none'; - const groupTogglerIconElm = document.createElement('span'); - groupTogglerIconElm.className = 'slick-group-toggle-all-icon expanded mdi mdi-close'; + const groupTogglerIconElm = createDomElement('span', { className: 'slick-group-toggle-all-icon expanded mdi mdi-close' }); this._groupToggler.appendChild(groupTogglerIconElm); if (this.gridOptions.enableTranslate && this._addonOptions.toggleAllButtonTextKey) { @@ -171,10 +169,12 @@ export class SlickDraggableGrouping { this._groupToggler.title = this._addonOptions.toggleAllPlaceholderText ?? ''; if (this._addonOptions.toggleAllButtonText) { - const groupTogglerTextElm = document.createElement('span'); - groupTogglerTextElm.className = 'slick-group-toggle-all-text'; - groupTogglerTextElm.textContent = this._addonOptions.toggleAllButtonText || ''; - this._groupToggler.appendChild(groupTogglerTextElm); + this._groupToggler.appendChild( + createDomElement('span', { + className: 'slick-group-toggle-all-text', + textContent: this._addonOptions.toggleAllButtonText || '' + }) + ); } this._dropboxElm.appendChild(this._groupToggler); @@ -185,8 +185,7 @@ export class SlickDraggableGrouping { ); } - this._dropboxPlaceholderElm = document.createElement('div'); - this._dropboxPlaceholderElm.className = 'slick-draggable-dropbox-toggle-placeholder'; + this._dropboxPlaceholderElm = createDomElement('div', { className: 'slick-draggable-dropbox-toggle-placeholder' }); if (this.gridOptions.enableTranslate && this._addonOptions?.dropPlaceHolderTextKey) { this._addonOptions.dropPlaceHolderText = this.extensionUtility.translateWhenEnabledAndServiceExist(this._addonOptions.dropPlaceHolderTextKey, 'TEXT_TOGGLE_ALL_GROUPS'); } @@ -196,16 +195,14 @@ export class SlickDraggableGrouping { this.setupColumnDropbox(); this._eventHandler.subscribe(grid.onHeaderCellRendered, (_e, args) => { - const column = args.column; const node = args.node; - if (!isEmptyObject(column.grouping)) { + if (!isEmptyObject(args.column?.grouping) && node) { node.style.cursor = 'pointer'; // add the pointer cursor on each column title // also optionally add an icon beside each column title that can be dragged if (this._addonOptions.groupIconCssClass || this._addonOptions.groupIconImage) { - const groupableIconElm = document.createElement('span'); - groupableIconElm.className = 'slick-column-groupable'; + const groupableIconElm = createDomElement('span', { className: 'slick-column-groupable' }); if (this._addonOptions.groupIconCssClass) { groupableIconElm.classList.add(...this._addonOptions.groupIconCssClass.split(' ')); } @@ -375,17 +372,14 @@ export class SlickDraggableGrouping { this._gridColumns.forEach(col => { if (col.id === columnid) { if (col.grouping !== null && !isEmptyObject(col.grouping)) { - const entryElm = document.createElement('div'); - entryElm.id = `${this._gridUid}_${col.id}_entry`; - entryElm.dataset.id = `${col.id}`; - entryElm.className = 'slick-dropped-grouping'; const columnName = column.children('.slick-column-name').first(); + const entryElm = createDomElement('div', { id: `${this._gridUid}_${col.id}_entry`, className: 'slick-dropped-grouping', dataset: { id: `${col.id}` } }); const groupTextElm = document.createElement('div'); groupTextElm.style.display = 'inline-flex'; groupTextElm.textContent = columnName.length ? columnName.text() : column.text(); entryElm.appendChild(groupTextElm); - const groupRemoveIconElm = document.createElement('div'); - groupRemoveIconElm.className = 'slick-groupby-remove'; + + const groupRemoveIconElm = createDomElement('div', { className: 'slick-groupby-remove' }); if (this._addonOptions.deleteIconCssClass) { groupRemoveIconElm.classList.add(...this._addonOptions.deleteIconCssClass.split(' ')); } @@ -395,6 +389,7 @@ export class SlickDraggableGrouping { if (!this._addonOptions.deleteIconCssClass && !this._addonOptions.deleteIconImage) { groupRemoveIconElm.classList.add('slick-groupby-remove-image'); } + entryElm.appendChild(groupRemoveIconElm); entryElm.appendChild(document.createElement('div')); container.appendChild(entryElm); diff --git a/packages/common/src/extensions/slickGridMenu.ts b/packages/common/src/extensions/slickGridMenu.ts index 986cd9671..aa74b54f5 100644 --- a/packages/common/src/extensions/slickGridMenu.ts +++ b/packages/common/src/extensions/slickGridMenu.ts @@ -12,7 +12,7 @@ import { } from '../interfaces/index'; import { DelimiterType, FileType } from '../enums/index'; import { ExtensionUtility } from '../extensions/extensionUtility'; -import { emptyElement, findWidthOrDefault, getHtmlElementOffset, getTranslationPrefix, } from '../services/index'; +import { createDomElement, emptyElement, findWidthOrDefault, getHtmlElementOffset, getTranslationPrefix, } from '../services/index'; import { ExcelExportService } from '../services/excelExport.service'; import { FilterService } from '../services/filter.service'; import { PubSubService } from '../services/pubSub.service'; @@ -46,6 +46,7 @@ export class SlickGridMenu extends MenuBaseClass { protected _commandMenuElm!: HTMLDivElement; protected _gridMenuOptions: GridMenu | null = null; protected _gridMenuButtonElm!: HTMLButtonElement; + protected _headerElm: HTMLDivElement | null = null; protected _isMenuOpen = false; protected _listElm!: HTMLSpanElement; protected _userOriginalGridMenu!: GridMenu; @@ -157,6 +158,10 @@ export class SlickGridMenu extends MenuBaseClass { if (gridMenuElm) { gridMenuElm.style.display = 'none'; } + if (this._headerElm) { + // put back original width (fixes width and frozen+gridMenu on left header) + this._headerElm.style.width = '100%'; + } this._gridMenuButtonElm?.remove(); this._menuElm?.remove(); this._commandMenuElm?.remove(); @@ -167,8 +172,7 @@ export class SlickGridMenu extends MenuBaseClass { // user could pass a title on top of the columns list addColumnTitleElementWhenDefined.call(this, this._menuElm); - this._listElm = document.createElement('span'); - this._listElm.className = 'slick-grid-menu-list'; + this._listElm = createDomElement('span', { className: 'slick-grid-menu-list' }); // update all columns on any of the column title button click from column picker this._bindEventService.bind(this._menuElm, 'click', handleColumnPickerItemClick.bind(this) as EventListener); @@ -180,14 +184,14 @@ export class SlickGridMenu extends MenuBaseClass { const gridUidSelector = this._gridUid ? `.${this._gridUid}` : ''; const gridMenuWidth = this._gridMenuOptions?.menuWidth || this._defaults.menuWidth; const headerSide = (this.gridOptions.hasOwnProperty('frozenColumn') && this.gridOptions.frozenColumn! >= 0) ? 'right' : 'left'; - this._menuElm = document.querySelector(`${gridUidSelector} .slick-header-${headerSide}`) as HTMLDivElement; + this._headerElm = document.querySelector(`${gridUidSelector} .slick-header-${headerSide}`); - if (this._menuElm && this._gridMenuOptions) { + if (this._headerElm && this._gridMenuOptions) { // resize the header row to include the hamburger menu icon - this._menuElm.style.width = `calc(100% - ${gridMenuWidth}px)`; + this._headerElm.style.width = `calc(100% - ${gridMenuWidth}px)`; // if header row is enabled, we also need to resize its width - const enableResizeHeaderRow = (this._gridMenuOptions && this._gridMenuOptions.resizeOnShowHeaderRow !== undefined) ? this._gridMenuOptions.resizeOnShowHeaderRow : this._defaults.resizeOnShowHeaderRow; + const enableResizeHeaderRow = this._gridMenuOptions.resizeOnShowHeaderRow ?? this._defaults.resizeOnShowHeaderRow; if (enableResizeHeaderRow && this.gridOptions.showHeaderRow) { const headerRowElm = document.querySelector(`${gridUidSelector} .slick-headerrow`); if (headerRowElm) { @@ -195,19 +199,17 @@ export class SlickGridMenu extends MenuBaseClass { } } - const showButton = (this._gridMenuOptions?.showButton !== undefined) ? this._gridMenuOptions.showButton : this._defaults.showButton; + const showButton = this._gridMenuOptions.showButton ?? this._defaults.showButton; if (showButton) { - this._gridMenuButtonElm = document.createElement('button'); - this._gridMenuButtonElm.className = 'slick-grid-menu-button'; - if (this._gridMenuOptions && this._gridMenuOptions.iconCssClass) { + this._gridMenuButtonElm = createDomElement('button', { className: 'slick-grid-menu-button' }); + if (this._gridMenuOptions?.iconCssClass) { this._gridMenuButtonElm.classList.add(...this._gridMenuOptions.iconCssClass.split(' ')); } else { - const iconImage = (this._gridMenuOptions && this._gridMenuOptions.iconImage) ? this._gridMenuOptions.iconImage : ''; - const iconImageElm = document.createElement('img'); - iconImageElm.src = iconImage; + const iconImage = this._gridMenuOptions?.iconImage ?? ''; + const iconImageElm = createDomElement('img', { src: iconImage }); this._gridMenuButtonElm.appendChild(iconImageElm); } - this._menuElm.parentElement!.insertBefore(this._gridMenuButtonElm, this._menuElm.parentElement!.firstChild); + this._headerElm.parentElement!.insertBefore(this._gridMenuButtonElm, this._headerElm.parentElement!.firstChild); // show the Grid Menu when hamburger menu is clicked this._gridMenuOptions.commandTitle = this._gridMenuOptions.customTitle || this._gridMenuOptions.commandTitle; @@ -227,8 +229,7 @@ export class SlickGridMenu extends MenuBaseClass { // add Close button addCloseButtomElement.call(this, this._menuElm); - this._commandMenuElm = document.createElement('div'); - this._commandMenuElm.className = 'slick-grid-menu-command-list'; + this._commandMenuElm = createDomElement('div', { className: 'slick-grid-menu-command-list' }); this._menuElm.appendChild(this._commandMenuElm); this.populateCommandOrOptionItems( diff --git a/packages/common/src/extensions/slickHeaderMenu.ts b/packages/common/src/extensions/slickHeaderMenu.ts index 830605f72..21f212c06 100644 --- a/packages/common/src/extensions/slickHeaderMenu.ts +++ b/packages/common/src/extensions/slickHeaderMenu.ts @@ -13,7 +13,7 @@ import { MultiColumnSort, OnHeaderCellRenderedEventArgs, } from '../interfaces/index'; -import { arrayRemoveItemByIndex, emptyElement, getElementOffsetRelativeToParent, getTranslationPrefix } from '../services/index'; +import { arrayRemoveItemByIndex, createDomElement, emptyElement, getElementOffsetRelativeToParent, getTranslationPrefix } from '../services/index'; import { ExtensionUtility } from '../extensions/extensionUtility'; import { FilterService } from '../services/filter.service'; import { PubSubService } from '../services/pubSub.service'; @@ -141,8 +141,7 @@ export class SlickHeaderMenu extends MenuBaseClass { } if (!this._menuElm) { - this._menuElm = document.createElement('div'); - this._menuElm.className = 'slick-header-menu'; + this._menuElm = createDomElement('div', { className: 'slick-header-menu' }); this._menuElm.style.minWidth = `${this.addonOptions.minWidth}px`; this._menuElm.setAttribute('aria-expanded', 'true'); this.grid.getContainerNode()?.appendChild(this._menuElm); @@ -179,8 +178,7 @@ export class SlickHeaderMenu extends MenuBaseClass { return; } - const headerButtonDivElm = document.createElement('div'); - headerButtonDivElm.className = 'slick-header-menu-button'; + const headerButtonDivElm = createDomElement('div', { className: 'slick-header-menu-button' }); if (this.addonOptions.buttonCssClass) { headerButtonDivElm.classList.add(...this.addonOptions.buttonCssClass.split(' ')); diff --git a/packages/common/src/extensions/slickRowMoveManager.ts b/packages/common/src/extensions/slickRowMoveManager.ts index 5c3621e85..c37acb89b 100644 --- a/packages/common/src/extensions/slickRowMoveManager.ts +++ b/packages/common/src/extensions/slickRowMoveManager.ts @@ -11,7 +11,7 @@ import { SlickGrid, SlickNamespace, } from '../interfaces/index'; -import { findWidthOrDefault, getHtmlElementOffset } from '../services/domUtilities'; +import { createDomElement, findWidthOrDefault, getHtmlElementOffset } from '../services/domUtilities'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; @@ -268,8 +268,7 @@ export class SlickRowMoveManager { const rowHeight = this.gridOptions.rowHeight as number; dd.selectedRows = selectedRows; - const reorderProxyElm = document.createElement('div'); - reorderProxyElm.className = 'slick-reorder-proxy'; + const reorderProxyElm = createDomElement('div', { className: 'slick-reorder-proxy' }); reorderProxyElm.style.display = 'none'; reorderProxyElm.style.position = 'absolute'; reorderProxyElm.style.zIndex = '99999'; @@ -278,8 +277,7 @@ export class SlickRowMoveManager { dd.selectionProxy = reorderProxyElm; this._canvas.appendChild(reorderProxyElm); - const reorderGuideElm = document.createElement('div'); - reorderGuideElm.className = 'slick-reorder-guide'; + const reorderGuideElm = createDomElement('div', { className: 'slick-reorder-guide' }); reorderGuideElm.style.position = 'absolute'; reorderGuideElm.style.zIndex = '99999'; reorderGuideElm.style.width = `${this._canvas.clientWidth}px`; diff --git a/packages/common/src/filters/compoundDateFilter.ts b/packages/common/src/filters/compoundDateFilter.ts index 425abfe14..8d5e25472 100644 --- a/packages/common/src/filters/compoundDateFilter.ts +++ b/packages/common/src/filters/compoundDateFilter.ts @@ -18,7 +18,7 @@ import { import { FieldType, OperatorString, OperatorType, SearchTerm } from '../enums/index'; import { Constants } from '../constants'; import { buildSelectOperator } from './filterUtilities'; -import { destroyObjectDomElementProps, emptyElement, } from '../services/domUtilities'; +import { createDomElement, destroyObjectDomElementProps, emptyElement, } from '../services/domUtilities'; import { getTranslationPrefix, mapFlatpickrDateFormatWithFieldType, mapOperatorToShorthandDesignation } from '../services/utilities'; import { TranslaterService } from '../services/translater.service'; import { BindingEventService } from '../services/bindingEvent.service'; @@ -245,16 +245,13 @@ export class CompoundDateFilter implements Filter { placeholder = this.columnFilter.placeholder; } - const filterDivInputElm = document.createElement('div'); - filterDivInputElm.className = 'flatpickr'; - - const inputElm = document.createElement('input'); - inputElm.type = 'text'; - inputElm.className = 'form-control'; - inputElm.dataset.input = ''; - inputElm.placeholder = placeholder; - - filterDivInputElm.appendChild(inputElm); + const filterDivInputElm = createDomElement('div', { className: 'flatpickr' }); + filterDivInputElm.appendChild( + createDomElement('input', { + type: 'text', className: 'form-control', + placeholder, dataset: { input: '' } + }) + ); this.flatInstance = flatpickr(filterDivInputElm, this._flatpickrOptions as unknown as Partial); return filterDivInputElm; @@ -298,14 +295,9 @@ export class CompoundDateFilter implements Filter { // create the DOM Select dropdown for the Operator this._selectOperatorElm = buildSelectOperator(this.getOperatorOptionValues(), this.gridOptions); this._filterDivInputElm = this.buildDatePickerInput(searchTerm); - const filterContainerElm = document.createElement('div'); - filterContainerElm.className = `form-group search-filter filter-${columnId}`; - - const containerInputGroupElm = document.createElement('div'); - containerInputGroupElm.className = 'input-group flatpickr'; - - const operatorInputGroupAddonElm = document.createElement('div'); - operatorInputGroupAddonElm.className = 'input-group-addon input-group-prepend operator'; + const filterContainerElm = createDomElement('div', { className: `form-group search-filter filter-${columnId}` }); + const containerInputGroupElm = createDomElement('div', { className: 'input-group flatpickr' }); + const operatorInputGroupAddonElm = createDomElement('div', { className: 'input-group-addon input-group-prepend operator' }); /* the DOM element final structure will be
diff --git a/packages/common/src/filters/compoundInputFilter.ts b/packages/common/src/filters/compoundInputFilter.ts index d6d57d93e..35896e777 100644 --- a/packages/common/src/filters/compoundInputFilter.ts +++ b/packages/common/src/filters/compoundInputFilter.ts @@ -12,7 +12,7 @@ import { SlickGrid, } from '../interfaces/index'; import { buildSelectOperator } from './filterUtilities'; -import { emptyElement } from '../services/domUtilities'; +import { createDomElement, emptyElement } from '../services/domUtilities'; import { getTranslationPrefix, mapOperatorToShorthandDesignation, toSentenceCase } from '../services/utilities'; import { BindingEventService } from '../services/bindingEvent.service'; import { TranslaterService } from '../services/translater.service'; @@ -177,11 +177,11 @@ export class CompoundInputFilter implements Filter { placeholder = this.columnFilter.placeholder; } - const inputElm = document.createElement('input'); - inputElm.type = this._inputType || 'text'; - inputElm.className = `form-control compound-input filter-${columnId}`; - inputElm.autocomplete = 'off'; - inputElm.placeholder = placeholder; + const inputElm = createDomElement('input', { + type: this._inputType || 'text', + autocomplete: 'off', placeholder, + className: `form-control compound-input filter-${columnId}`, + }); inputElm.setAttribute('role', 'presentation'); inputElm.setAttribute('aria-label', this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter`); @@ -247,16 +247,11 @@ export class CompoundInputFilter implements Filter { // create the DOM Select dropdown for the Operator this._selectOperatorElm = buildSelectOperator(this.getOperatorOptionValues(), this.gridOptions); this._filterInputElm = this.buildInputElement(); - const emptySpanElm = document.createElement('span'); + const emptySpanElm = createDomElement('span'); - const filterContainerElm = document.createElement('div'); - filterContainerElm.className = `form-group search-filter filter-${columnId}`; - - const containerInputGroupElm = document.createElement('div'); - containerInputGroupElm.className = 'input-group'; - - const operatorInputGroupAddonElm = document.createElement('div'); - operatorInputGroupAddonElm.className = 'input-group-addon input-group-prepend operator'; + const filterContainerElm = createDomElement('div', { className: `form-group search-filter filter-${columnId}` }); + const containerInputGroupElm = createDomElement('div', { className: 'input-group' }); + const operatorInputGroupAddonElm = createDomElement('div', { className: 'input-group-addon input-group-prepend operator' }); /* the DOM element final structure will be
diff --git a/packages/common/src/filters/compoundSliderFilter.ts b/packages/common/src/filters/compoundSliderFilter.ts index 981b34db6..ee6f00a09 100644 --- a/packages/common/src/filters/compoundSliderFilter.ts +++ b/packages/common/src/filters/compoundSliderFilter.ts @@ -12,7 +12,7 @@ import { import { Constants } from '../constants'; import { OperatorString, OperatorType, SearchTerm } from '../enums/index'; import { buildSelectOperator } from './filterUtilities'; -import { emptyElement } from '../services/domUtilities'; +import { createDomElement, emptyElement } from '../services/domUtilities'; import { getTranslationPrefix, hasData, mapOperatorToShorthandDesignation, toSentenceCase } from '../services/utilities'; import { BindingEventService } from '../services/bindingEvent.service'; import { TranslaterService } from '../services/translater.service'; @@ -254,27 +254,20 @@ export class CompoundSliderFilter implements Filter { // create the DOM Select dropdown for the Operator this.selectOperatorElm = buildSelectOperator(this.getOperatorOptionValues(), this.gridOptions); - const spanPrependElm = document.createElement('span'); - spanPrependElm.className = 'input-group-addon input-group-prepend operator'; + const spanPrependElm = createDomElement('span', { className: 'input-group-addon input-group-prepend operator' }); spanPrependElm.appendChild(this.selectOperatorElm); // create the DOM element - this.filterInputElm = document.createElement('input'); - this.filterInputElm.type = 'range'; - this.filterInputElm.className = `form-control slider-filter-input range compound-slider ${this._elementRangeInputId}`; - this.filterInputElm.defaultValue = defaultValue; - this.filterInputElm.value = searchTermInput; - this.filterInputElm.min = `${minValue}`; - this.filterInputElm.max = `${maxValue}`; - this.filterInputElm.step = `${step}`; - this.filterInputElm.name = this._elementRangeInputId; + this.filterInputElm = createDomElement('input', { + type: 'range', name: this._elementRangeInputId, + className: `form-control slider-filter-input range compound-slider ${this._elementRangeInputId}`, + defaultValue, value: searchTermInput, + min: `${minValue}`, max: `${maxValue}`, step: `${step}`, + }); this.filterInputElm.setAttribute('aria-label', this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter`); - this.divContainerFilterElm = document.createElement('div'); - this.divContainerFilterElm.className = `form-group search-filter slider-container filter-${columnId}`; - - this.containerInputGroupElm = document.createElement('div'); - this.containerInputGroupElm.className = `input-group search-filter filter-${columnId}`; + this.divContainerFilterElm = createDomElement('div', { className: `form-group search-filter slider-container filter-${columnId}` }); + this.containerInputGroupElm = createDomElement('div', { className: `input-group search-filter filter-${columnId}` }); this.containerInputGroupElm.appendChild(spanPrependElm); this.containerInputGroupElm.appendChild(this.filterInputElm); this.divContainerFilterElm.appendChild(this.containerInputGroupElm); @@ -283,12 +276,11 @@ export class CompoundSliderFilter implements Filter { this.containerInputGroupElm.classList.add('input-group'); this.filterInputElm.value = searchTermInput; - const divGroupAppendElm = document.createElement('div'); - divGroupAppendElm.className = 'input-group-addon input-group-append slider-value'; - - this.filterNumberElm = document.createElement('span'); - this.filterNumberElm.className = `input-group-text ${this._elementRangeOutputId}`; - this.filterNumberElm.textContent = searchTermInput; + const divGroupAppendElm = createDomElement('div', { className: 'input-group-addon input-group-append slider-value' }); + this.filterNumberElm = createDomElement('span', { + className: `input-group-text ${this._elementRangeOutputId}`, + textContent: searchTermInput + }); divGroupAppendElm.appendChild(this.filterNumberElm); this.containerInputGroupElm.appendChild(divGroupAppendElm); } diff --git a/packages/common/src/filters/dateRangeFilter.ts b/packages/common/src/filters/dateRangeFilter.ts index 51f1c9c6c..ad43c3daf 100644 --- a/packages/common/src/filters/dateRangeFilter.ts +++ b/packages/common/src/filters/dateRangeFilter.ts @@ -21,7 +21,7 @@ import { GridOption, SlickGrid, } from '../interfaces/index'; -import { destroyObjectDomElementProps, emptyElement, } from '../services/domUtilities'; +import { createDomElement, destroyObjectDomElementProps, emptyElement, } from '../services/domUtilities'; import { mapFlatpickrDateFormatWithFieldType, mapMomentDateFormatWithFieldType } from '../services/utilities'; import { BindingEventService } from '../services/bindingEvent.service'; import { TranslaterService } from '../services/translater.service'; @@ -257,16 +257,10 @@ export class DateRangeFilter implements Filter { placeholder = this.columnFilter.placeholder; } - const filterDivInputElm = document.createElement('div'); - filterDivInputElm.className = `flatpickr search-filter filter-${columnId}`; - - const inputElm = document.createElement('input'); - inputElm.type = 'text'; - inputElm.className = 'form-control'; - inputElm.dataset.input = ''; - inputElm.placeholder = placeholder; - - filterDivInputElm.appendChild(inputElm); + const filterDivInputElm = createDomElement('div', { className: `flatpickr search-filter filter-${columnId}` }); + filterDivInputElm.appendChild( + createDomElement('input', { type: 'text', className: 'form-control', placeholder, dataset: { input: '' } }) + ); this.flatInstance = flatpickr(filterDivInputElm, this._flatpickrOptions as unknown as Partial); return filterDivInputElm; diff --git a/packages/common/src/filters/filterUtilities.ts b/packages/common/src/filters/filterUtilities.ts index 35dddd389..9b0c7eec2 100644 --- a/packages/common/src/filters/filterUtilities.ts +++ b/packages/common/src/filters/filterUtilities.ts @@ -1,7 +1,7 @@ import { OperatorString } from '../enums/operatorString.type'; import { Column, GridOption } from '../interfaces/index'; import { Observable, RxJsFacade, Subject, Subscription } from '../services/rxjsFacade'; -import { htmlEncodedStringWithPadding, sanitizeTextByAvailableSanitizer, } from '../services/domUtilities'; +import { createDomElement, htmlEncodedStringWithPadding, sanitizeTextByAvailableSanitizer, } from '../services/domUtilities'; import { castObservableToPromise, getDescendantProperty, } from '../services/utilities'; /** @@ -10,14 +10,15 @@ import { castObservableToPromise, getDescendantProperty, } from '../services/uti * @returns {Object} selectElm - Select Dropdown HTML Element */ export function buildSelectOperator(optionValues: Array<{ operator: OperatorString, description: string }>, gridOptions: GridOption): HTMLSelectElement { - const selectElm = document.createElement('select'); - selectElm.className = 'form-control'; + const selectElm = createDomElement('select', { className: 'form-control' }); for (const option of optionValues) { - const selectOption = document.createElement('option'); - selectOption.value = option.operator; - selectOption.innerHTML = sanitizeTextByAvailableSanitizer(gridOptions, `${htmlEncodedStringWithPadding(option.operator, 3)}${option.description}`); - selectElm.appendChild(selectOption); + selectElm.appendChild( + createDomElement('option', { + value: option.operator, + innerHTML: sanitizeTextByAvailableSanitizer(gridOptions, `${htmlEncodedStringWithPadding(option.operator, 3)}${option.description}`) + }) + ); } return selectElm; diff --git a/packages/common/src/filters/inputFilter.ts b/packages/common/src/filters/inputFilter.ts index b1d3642c5..7e37b9b49 100644 --- a/packages/common/src/filters/inputFilter.ts +++ b/packages/common/src/filters/inputFilter.ts @@ -9,7 +9,7 @@ import { } from '../interfaces/index'; import { OperatorType, OperatorString, SearchTerm } from '../enums/index'; import { BindingEventService } from '../services/bindingEvent.service'; -import { emptyElement, toSentenceCase } from '../services'; +import { createDomElement, emptyElement, toSentenceCase } from '../services'; export class InputFilter implements Filter { protected _bindEventService: BindingEventService; @@ -195,16 +195,16 @@ export class InputFilter implements Filter { placeholder = this.columnFilter.placeholder; } - const inputElm = document.createElement('input'); - inputElm.type = this._inputType || 'text'; - inputElm.className = `form-control search-filter filter-${columnId}`; - inputElm.autocomplete = 'off'; - inputElm.placeholder = placeholder; + const inputElm = createDomElement('input', { + type: this._inputType || 'text', + autocomplete: 'off', placeholder, + className: `form-control search-filter filter-${columnId}`, + value: (searchTerm ?? '') as string, + dataset: { columnid: `${columnId}` } + }); inputElm.setAttribute('aria-label', this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter`); inputElm.setAttribute('role', 'presentation'); - inputElm.value = (searchTerm ?? '') as string; - inputElm.dataset.columnid = `${columnId}`; // if there's a search term, we will add the "filled" class for styling purposes if (searchTerm) { diff --git a/packages/common/src/filters/nativeSelectFilter.ts b/packages/common/src/filters/nativeSelectFilter.ts index 993d42bc2..49dd8cc35 100644 --- a/packages/common/src/filters/nativeSelectFilter.ts +++ b/packages/common/src/filters/nativeSelectFilter.ts @@ -8,7 +8,7 @@ import { SlickGrid, } from '../interfaces/index'; import { OperatorType, OperatorString, SearchTerm } from '../enums/index'; -import { emptyElement, } from '../services/domUtilities'; +import { createDomElement, emptyElement, } from '../services/domUtilities'; import { toSentenceCase } from '../services/utilities'; import { TranslaterService } from '../services/translater.service'; import { BindingEventService } from '../services/bindingEvent.service'; @@ -146,9 +146,8 @@ export class NativeSelectFilter implements Filter { */ buildFilterSelectFromCollection(collection: any[]): HTMLSelectElement { const columnId = this.columnDef?.id ?? ''; - const selectElm = document.createElement('select'); + const selectElm = createDomElement('select', { className: `form-control search-filter filter-${columnId}` }); selectElm.setAttribute('aria-label', this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter`); - selectElm.className = `form-control search-filter filter-${columnId}`; const labelName = this.columnFilter.customStructure?.label ?? 'label'; const valueName = this.columnFilter.customStructure?.value ?? 'value'; @@ -157,11 +156,9 @@ export class NativeSelectFilter implements Filter { // collection could be an Array of Strings OR Objects if (collection.every(x => typeof x === 'string')) { for (const option of collection) { - const selectOption = document.createElement('option'); - selectOption.value = option; - selectOption.label = option; - selectOption.textContent = option; - selectElm.appendChild(selectOption); + selectElm.appendChild( + createDomElement('option', { value: option, label: option, textContent: option }) + ); } } else { for (const option of collection) { @@ -172,10 +169,9 @@ export class NativeSelectFilter implements Filter { const labelKey = option.labelKey || option[labelName]; const textLabel = ((option.labelKey || isEnabledTranslate) && this.translater && this.translater.translate && this.translater.getCurrentLanguage && this.translater.getCurrentLanguage()) ? this.translater.translate(labelKey || ' ') : labelKey; - const selectOption = document.createElement('option'); - selectOption.value = option[valueName]; - selectOption.textContent = textLabel; - selectElm.appendChild(selectOption); + selectElm.appendChild( + createDomElement('option', { value: option[valueName], textContent: textLabel }) + ); } } diff --git a/packages/common/src/filters/sliderFilter.ts b/packages/common/src/filters/sliderFilter.ts index 5795e135d..cfff81b95 100644 --- a/packages/common/src/filters/sliderFilter.ts +++ b/packages/common/src/filters/sliderFilter.ts @@ -7,7 +7,7 @@ import { FilterCallback, SlickGrid, } from './../interfaces/index'; -import { emptyElement, } from '../services/domUtilities'; +import { createDomElement, emptyElement, } from '../services/domUtilities'; import { hasData, toSentenceCase } from '../services/utilities'; import { BindingEventService } from '../services/bindingEvent.service'; @@ -194,31 +194,23 @@ export class SliderFilter implements Filter { this._currentValue = +searchTermInput; // create the DOM element - this.filterInputElm = document.createElement('input'); - this.filterInputElm.type = 'range'; - this.filterInputElm.className = `form-control slider-filter-input range ${this._elementRangeInputId}`; - this.filterInputElm.defaultValue = defaultValue; - this.filterInputElm.value = searchTermInput; - this.filterInputElm.min = `${minValue}`; - this.filterInputElm.max = `${maxValue}`; - this.filterInputElm.step = `${step}`; - this.filterInputElm.name = this._elementRangeInputId; + this.filterInputElm = createDomElement('input', { + type: 'range', name: this._elementRangeInputId, + className: `form-control slider-filter-input range ${this._elementRangeInputId}`, + defaultValue, value: searchTermInput, + min: `${minValue}`, max: `${maxValue}`, step: `${step}`, + }); this.filterInputElm.setAttribute('aria-label', this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter`); - this.divContainerFilterElm = document.createElement('div'); - this.divContainerFilterElm.className = `search-filter slider-container filter-${columnId}`; + this.divContainerFilterElm = createDomElement('div', { className: `search-filter slider-container filter-${columnId}` }); this.divContainerFilterElm.appendChild(this.filterInputElm); if (!this.filterParams.hideSliderNumber) { this.divContainerFilterElm.classList.add('input-group'); this.filterInputElm.value = searchTermInput; - const divGroupAppendElm = document.createElement('div'); - divGroupAppendElm.className = 'input-group-addon input-group-append slider-value'; - - this.filterNumberElm = document.createElement('span'); - this.filterNumberElm.className = `input-group-text ${this._elementRangeOutputId}`; - this.filterNumberElm.textContent = searchTermInput; + const divGroupAppendElm = createDomElement('div', { className: 'input-group-addon input-group-append slider-value' }); + this.filterNumberElm = createDomElement('span', { className: `input-group-text ${this._elementRangeOutputId}`, textContent: searchTermInput }); divGroupAppendElm.appendChild(this.filterNumberElm); this.divContainerFilterElm.appendChild(divGroupAppendElm); } diff --git a/packages/common/src/interfaces/slickEventHandler.interface.ts b/packages/common/src/interfaces/slickEventHandler.interface.ts index b7662821e..5bec376f3 100644 --- a/packages/common/src/interfaces/slickEventHandler.interface.ts +++ b/packages/common/src/interfaces/slickEventHandler.interface.ts @@ -1,18 +1,6 @@ import { SlickEvent, SlickEventData } from './index'; -/** - * TypeScript Helper to get the Generic Type T of the SlickEvent - * for example GetType> will return { columnDef: Column } - */ -/* eslint-disable @typescript-eslint/indent */ -// disable eslint indent rule until this issue is fixed: https://github.com/typescript-eslint/typescript-eslint/issues/1824 -export type GetSlickEventType = - T extends (infer U)[] ? U : - T extends (...args: any[]) => infer U ? U : - T extends SlickEvent ? U : T; -/* eslint-enable @typescript-eslint/indent */ - -type Handler = (e: SlickEventData, data: H) => void +type Handler = (e: SlickEventData, data: H) => void; export interface SlickEventHandler { /** Subscribe to a SlickGrid Event and execute its handler callback */ diff --git a/packages/common/src/services/__tests__/utilities.spec.ts b/packages/common/src/services/__tests__/utilities.spec.ts index 424ad563e..0d03cc9ac 100644 --- a/packages/common/src/services/__tests__/utilities.spec.ts +++ b/packages/common/src/services/__tests__/utilities.spec.ts @@ -1,3 +1,4 @@ +import 'jest-extended'; import { of } from 'rxjs'; import { FieldType, OperatorType } from '../../enums/index'; @@ -9,6 +10,7 @@ import { addWhiteSpaces, arrayRemoveItemByIndex, cancellablePromise, + CancelledException, castObservableToPromise, flattenToParentChildArray, unflattenParentChildArrayToTree, @@ -29,6 +31,7 @@ import { mapOperatorByFieldType, mapOperatorToShorthandDesignation, mapOperatorType, + mergeDeep, parseBoolean, parseUtcDate, removeAccentFromText, @@ -42,7 +45,6 @@ import { unsubscribeAll, uniqueArray, uniqueObjectArray, - CancelledException, } from '../utilities'; describe('Service/Utilies', () => { @@ -1200,6 +1202,32 @@ describe('Service/Utilies', () => { }); }); + describe('mergeDeep method', () => { + it('should have undefined object when input object is also undefined', () => { + const inputObj = undefined; + mergeDeep(inputObj, { firstName: 'John' }); + expect(inputObj).toEqual(undefined); + }); + + it('should provide empty object as input and expect output object to include 2nd object', () => { + const inputObj = {}; + mergeDeep(inputObj, { firstName: 'John' }); + expect(inputObj).toEqual({ firstName: 'John' }); + }); + + it('should provide filled object and return same object when 2nd object is also an object', () => { + const inputObj = { firstName: 'Jane' }; + mergeDeep(inputObj, { firstName: { name: 'John' } }); + expect(inputObj).toEqual({ firstName: 'Jane' }); + }); + + it('should provide input object with undefined property and expect output object to return merged object from 2nd object when that one is filled', () => { + const inputObj = { firstName: undefined }; + mergeDeep(inputObj, { firstName: {} }); + expect(inputObj).toEqual({ firstName: {} }); + }); + }); + describe('parseBoolean method', () => { it('should return false when input value is not parseable to a boolean', () => { const output = parseBoolean('abc'); diff --git a/packages/common/src/services/domUtilities.ts b/packages/common/src/services/domUtilities.ts index d6fee9af6..ae614afb5 100644 --- a/packages/common/src/services/domUtilities.ts +++ b/packages/common/src/services/domUtilities.ts @@ -1,9 +1,10 @@ import * as DOMPurify_ from 'dompurify'; const DOMPurify = DOMPurify_; // patch to fix rollup to work -import { SearchTerm } from '../enums/index'; +import { InferType, SearchTerm } from '../enums/index'; import { Column, GridOption, HtmlElementPosition, SelectOption, SlickGrid, } from '../interfaces/index'; import { TranslaterService } from './translater.service'; +import { mergeDeep } from './utilities'; /** * Create the HTML DOM Element for a Select Editor or Filter, this is specific to these 2 types only and the unit tests are directly under them @@ -155,6 +156,22 @@ export function calculateAvailableSpace(element: HTMLElement): { top: number; bo return { top, bottom, left, right }; } +/** Create a DOM Element with any optional attributes or properties */ +export function createDomElement(tagName: T, elementOptions?: { [P in K]: InferType }): HTMLElementTagNameMap[T] { + const elm = document.createElement(tagName); + + if (elementOptions) { + Object.keys(elementOptions).forEach((elmOptionKey) => { + const elmValue = (elementOptions as any)[elmOptionKey]; + if (typeof elmValue === 'object') { + mergeDeep(elm[elmOptionKey as K], elmValue); + } else { + elm[elmOptionKey as K] = (elementOptions as any)[elmOptionKey]; + } + }); + } + return elm; +} /** * Loop through all properties of an object and nullify any properties that are instanceof HTMLElement, diff --git a/packages/common/src/services/groupingAndColspan.service.ts b/packages/common/src/services/groupingAndColspan.service.ts index 91ce2d5a5..0ca65972c 100644 --- a/packages/common/src/services/groupingAndColspan.service.ts +++ b/packages/common/src/services/groupingAndColspan.service.ts @@ -10,7 +10,7 @@ import { } from './../interfaces/index'; import { ExtensionUtility } from '../extensions/extensionUtility'; import { PubSubService } from './pubSub.service'; -import { emptyElement } from './domUtilities'; +import { createDomElement, emptyElement } from './domUtilities'; // using external non-typed js libraries declare const Slick: SlickNamespace; @@ -154,13 +154,10 @@ export class GroupingAndColspanService { } } else { widthTotal = colDef.width || 0; - headerElm = document.createElement('div'); - headerElm.className = `ui-state-default slick-header-column ${isFrozenGrid ? 'frozen' : ''}`; + headerElm = createDomElement('div', { className: `ui-state-default slick-header-column ${isFrozenGrid ? 'frozen' : ''}` }); headerElm.style.width = `${widthTotal - headerColumnWidthDiff}px`; - const spanColumnNameElm = document.createElement('span'); - spanColumnNameElm.className = 'slick-column-name'; - spanColumnNameElm.textContent = colDef.columnGroup || ''; + const spanColumnNameElm = createDomElement('span', { className: 'slick-column-name', textContent: colDef.columnGroup || '' }); headerElm.appendChild(spanColumnNameElm); preHeaderPanel.appendChild(headerElm); diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts index 7c4e0ef84..6c184c3bc 100644 --- a/packages/common/src/services/utilities.ts +++ b/packages/common/src/services/utilities.ts @@ -279,6 +279,15 @@ export function isEmptyObject(obj: any): boolean { return Object.entries(obj).length === 0; } +/** + * Simple object check. + * @param item + * @returns {boolean} + */ +export function isObject(item: any) { + return (item && typeof item === 'object' && !Array.isArray(item)); +} + /** * @deprecated use `findItemInTreeStructure()` instead. Find an item from a hierarchical (tree) view structure (a parent that can have children array which themseleves can children and so on) * @param treeArray @@ -826,6 +835,33 @@ export function mapOperatorByFieldType(fieldType: typeof FieldType[keyof typeof return map; } +/** + * Deep merge two objects. + * @param {*} target + * @param {*} ...sources + */ +export function mergeDeep(target: any, ...sources: any[]): any { + if (!sources.length) { + return target; + } + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) { + Object.assign(target, { [key]: {} }); + } + mergeDeep(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return mergeDeep(target, ...sources); +} + /** * Takes an object and allow to provide a property key to omit from the original object * @param {Object} obj - input object diff --git a/packages/composite-editor-component/src/slick-composite-editor.component.ts b/packages/composite-editor-component/src/slick-composite-editor.component.ts index 78db2255f..d95cefde1 100644 --- a/packages/composite-editor-component/src/slick-composite-editor.component.ts +++ b/packages/composite-editor-component/src/slick-composite-editor.component.ts @@ -10,6 +10,7 @@ import { CompositeEditorOption, Constants, ContainerService, + createDomElement, CurrentRowSelection, deepCopy, DOMEvent, @@ -359,55 +360,45 @@ export class SlickCompositeEditorComponent implements ExternalResource { const parsedHeaderTitle = headerTitle.replace(/\{\{(.*?)\}\}/g, (_match, group) => getDescendantProperty(dataContext, group)); const layoutColCount = viewColumnLayout === 'auto' ? this.autoCalculateLayoutColumnCount(modalColumns.length) : viewColumnLayout; - this._modalElm = document.createElement('div'); - this._modalElm.className = `slick-editor-modal ${gridUid}`; - - const modalContentElm = document.createElement('div'); - modalContentElm.className = 'slick-editor-modal-content'; + this._modalElm = createDomElement('div', { className: `slick-editor-modal ${gridUid}` }); + const modalContentElm = createDomElement('div', { className: 'slick-editor-modal-content' }); if (viewColumnLayout > 1 || (viewColumnLayout === 'auto' && layoutColCount > 1)) { const splitClassName = layoutColCount === 2 ? 'split-view' : 'triple-split-view'; modalContentElm.classList.add(splitClassName); } - const modalHeaderTitleElm = document.createElement('div'); - modalHeaderTitleElm.className = 'slick-editor-modal-title'; - modalHeaderTitleElm.innerHTML = sanitizeTextByAvailableSanitizer(this.gridOptions, parsedHeaderTitle); - - const modalCloseButtonElm = document.createElement('button'); - modalCloseButtonElm.type = 'button'; - modalCloseButtonElm.textContent = '×'; - modalCloseButtonElm.className = 'close'; - modalCloseButtonElm.dataset.action = 'close'; + const modalHeaderTitleElm = createDomElement('div', { + className: 'slick-editor-modal-title', + innerHTML: sanitizeTextByAvailableSanitizer(this.gridOptions, parsedHeaderTitle), + }); + const modalCloseButtonElm = createDomElement('button', { type: 'button', textContent: '×', className: 'close', dataset: { action: 'close' } }); modalCloseButtonElm.setAttribute('aria-label', 'Close'); if (this._options.showCloseButtonOutside) { modalHeaderTitleElm?.classList?.add('outside'); modalCloseButtonElm?.classList?.add('outside'); } - const modalHeaderElm = document.createElement('div'); - modalHeaderElm.className = 'slick-editor-modal-header'; + const modalHeaderElm = createDomElement('div', { className: 'slick-editor-modal-header' }); modalHeaderElm.setAttribute('aria-label', 'Close'); modalHeaderElm.appendChild(modalHeaderTitleElm); modalHeaderElm.appendChild(modalCloseButtonElm); - const modalBodyElm = document.createElement('div'); - modalBodyElm.className = 'slick-editor-modal-body'; + const modalBodyElm = createDomElement('div', { className: 'slick-editor-modal-body' }); - this._modalBodyTopValidationElm = document.createElement('div'); - this._modalBodyTopValidationElm.className = 'validation-summary'; + this._modalBodyTopValidationElm = createDomElement('div', { className: 'validation-summary' }); this._modalBodyTopValidationElm.style.display = 'none'; modalBodyElm.appendChild(this._modalBodyTopValidationElm); - const modalFooterElm = document.createElement('div'); - modalFooterElm.className = 'slick-editor-modal-footer'; + const modalFooterElm = createDomElement('div', { className: 'slick-editor-modal-footer' }); - const modalCancelButtonElm = document.createElement('button'); - modalCancelButtonElm.type = 'button'; - modalCancelButtonElm.className = 'btn btn-cancel btn-default btn-sm'; - modalCancelButtonElm.dataset.action = 'cancel'; + const modalCancelButtonElm = createDomElement('button', { + type: 'button', + className: 'btn btn-cancel btn-default btn-sm', + textContent: this.getLabelText('cancelButton', 'TEXT_CANCEL', 'Cancel'), + dataset: { action: 'cancel' }, + }); modalCancelButtonElm.setAttribute('aria-label', this.getLabelText('cancelButton', 'TEXT_CANCEL', 'Cancel')); - modalCancelButtonElm.textContent = this.getLabelText('cancelButton', 'TEXT_CANCEL', 'Cancel'); let leftFooterText = ''; let saveButtonText = ''; @@ -429,20 +420,18 @@ export class SlickCompositeEditorComponent implements ExternalResource { saveButtonText = this.getLabelText('saveButton', 'TEXT_SAVE', 'Save'); } - const selectionCounterElm = document.createElement('div'); - selectionCounterElm.className = 'footer-status-text'; - selectionCounterElm.textContent = leftFooterText; - - this._modalSaveButtonElm = document.createElement('button'); - this._modalSaveButtonElm.type = 'button'; - this._modalSaveButtonElm.className = 'btn btn-save btn-primary btn-sm'; - this._modalSaveButtonElm.dataset.action = (modalType === 'create' || modalType === 'edit') ? 'save' : modalType; - this._modalSaveButtonElm.dataset.ariaLabel = saveButtonText; - this._modalSaveButtonElm.textContent = saveButtonText; + const selectionCounterElm = createDomElement('div', { className: 'footer-status-text', textContent: leftFooterText }); + this._modalSaveButtonElm = createDomElement('button', { + type: 'button', className: 'btn btn-save btn-primary btn-sm', + textContent: saveButtonText, + dataset: { + action: (modalType === 'create' || modalType === 'edit') ? 'save' : modalType, + ariaLabel: saveButtonText + } + }); this._modalSaveButtonElm.setAttribute('aria-label', saveButtonText); - const footerContainerElm = document.createElement('div'); - footerContainerElm.className = 'footer-buttons'; + const footerContainerElm = createDomElement('div', { className: 'footer-buttons' }); if (modalType === 'mass-update' || modalType === 'mass-selection') { modalFooterElm.appendChild(selectionCounterElm); @@ -458,8 +447,7 @@ export class SlickCompositeEditorComponent implements ExternalResource { for (const columnDef of modalColumns) { if (columnDef.editor) { - const itemContainer = document.createElement('div'); - itemContainer.className = `item-details-container editor-${columnDef.id}`; + const itemContainer = createDomElement('div', { className: `item-details-container editor-${columnDef.id}` }); if (layoutColCount === 1) { itemContainer.classList.add('slick-col-medium-12'); @@ -467,16 +455,15 @@ export class SlickCompositeEditorComponent implements ExternalResource { itemContainer.classList.add('slick-col-medium-6', `slick-col-xlarge-${12 / layoutColCount}`); } - const templateItemLabelElm = document.createElement('div'); - templateItemLabelElm.className = `item-details-label editor-${columnDef.id}`; - templateItemLabelElm.textContent = this.getColumnLabel(columnDef) || 'n/a'; - - const templateItemEditorElm = document.createElement('div'); - templateItemEditorElm.className = 'item-details-editor-container slick-cell'; - templateItemEditorElm.dataset.editorid = `${columnDef.id}`; - - const templateItemValidationElm = document.createElement('div'); - templateItemValidationElm.className = `item-details-validation editor-${columnDef.id}`; + const templateItemLabelElm = createDomElement('div', { + className: `item-details-label editor-${columnDef.id}`, + textContent: this.getColumnLabel(columnDef) || 'n/a' + }); + const templateItemEditorElm = createDomElement('div', { + className: 'item-details-editor-container slick-cell', + dataset: { editorid: `${columnDef.id}` }, + }); + const templateItemValidationElm = createDomElement('div', { className: `item-details-validation editor-${columnDef.id}` }); // optionally add a reset button beside each editor if (this._options?.showResetButtonOnEachEditor) { @@ -643,11 +630,11 @@ export class SlickCompositeEditorComponent implements ExternalResource { * @returns {Object} - html button */ protected createEditorResetButtonElement(columnId: string): HTMLButtonElement { - const resetButtonElm = document.createElement('button'); - resetButtonElm.type = 'button'; - resetButtonElm.name = columnId; - resetButtonElm.title = this._options?.labels?.resetFormButton ?? 'Reset Form Input'; - resetButtonElm.className = 'btn btn-xs btn-editor-reset'; + const resetButtonElm = createDomElement('button', { + type: 'button', name: columnId, + title: this._options?.labels?.resetFormButton ?? 'Reset Form Input', + className: 'btn btn-xs btn-editor-reset' + }); resetButtonElm.setAttribute('aria-label', 'Reset'); if (this._options?.resetEditorButtonCssClass) { @@ -666,15 +653,9 @@ export class SlickCompositeEditorComponent implements ExternalResource { * @returns {Object} - html button */ protected createFormResetButtonElement(): HTMLDivElement { - const resetButtonContainerElm = document.createElement('div'); - resetButtonContainerElm.className = 'reset-container'; - - const resetButtonElm = document.createElement('button'); - resetButtonElm.type = 'button'; - resetButtonElm.className = 'btn btn-sm reset-form'; - - const resetIconSpanElm = document.createElement('span'); - resetIconSpanElm.className = this._options?.resetFormButtonIconCssClass ?? ''; + const resetButtonContainerElm = createDomElement('div', { className: 'reset-container' }); + const resetButtonElm = createDomElement('button', { type: 'button', className: 'btn btn-sm reset-form' }); + const resetIconSpanElm = createDomElement('span', { className: this._options?.resetFormButtonIconCssClass ?? '' }); resetButtonElm.appendChild(resetIconSpanElm); resetButtonElm.appendChild(document.createTextNode(' Reset Form')); resetButtonContainerElm.appendChild(resetButtonElm); diff --git a/packages/custom-footer-component/src/slick-footer.component.ts b/packages/custom-footer-component/src/slick-footer.component.ts index 2fb14cbe7..24bc8293e 100644 --- a/packages/custom-footer-component/src/slick-footer.component.ts +++ b/packages/custom-footer-component/src/slick-footer.component.ts @@ -3,6 +3,7 @@ const moment = (moment_ as any)['default'] || moment_; // patch to fix rollup "m import { Constants, + createDomElement, CustomFooterOption, GridOption, Locale, @@ -169,19 +170,17 @@ export class SlickFooterComponent { /** Create the Footer Container */ protected createFooterContainer(gridParentContainerElm: HTMLElement) { - const footerElm = document.createElement('div'); - footerElm.className = `slick-custom-footer ${this.gridUid}`; + const footerElm = createDomElement('div', { className: `slick-custom-footer ${this.gridUid}` }); footerElm.style.width = '100%'; footerElm.style.height = `${this.customFooterOptions.footerHeight || 20}px`; - const leftFooterElm = document.createElement('div'); - leftFooterElm.className = `left-footer ${this.customFooterOptions.leftContainerClass}`; - leftFooterElm.innerHTML = sanitizeTextByAvailableSanitizer(this.gridOptions, this.customFooterOptions.leftFooterText || ''); - - const rightFooterElm = this.createFooterRightContainer(); - - footerElm.appendChild(leftFooterElm); - footerElm.appendChild(rightFooterElm); + footerElm.appendChild( + createDomElement('div', { + className: `left-footer ${this.customFooterOptions.leftContainerClass}`, + innerHTML: sanitizeTextByAvailableSanitizer(this.gridOptions, this.customFooterOptions.leftFooterText || '') + }) + ); + footerElm.appendChild(this.createFooterRightContainer()); this._footerElement = footerElm; if (gridParentContainerElm?.appendChild && this._footerElement) { @@ -191,15 +190,13 @@ export class SlickFooterComponent { /** Create the Right Section Footer */ protected createFooterRightContainer(): HTMLDivElement { - const rightFooterElm = document.createElement('div'); - rightFooterElm.className = `right-footer ${this.customFooterOptions.rightContainerClass || ''}`; + const rightFooterElm = createDomElement('div', { className: `right-footer ${this.customFooterOptions.rightContainerClass || ''}` }); if (!this._isRightFooterOriginallyEmpty) { rightFooterElm.innerHTML = sanitizeTextByAvailableSanitizer(this.gridOptions, this.customFooterOptions.rightFooterText || ''); } else if (!this.customFooterOptions.hideMetrics) { rightFooterElm.classList.add('metrics'); - const lastUpdateElm = document.createElement('span'); - lastUpdateElm.className = 'timestamp'; + const lastUpdateElm = createDomElement('span', { className: 'timestamp' }); if (!this.customFooterOptions.hideLastUpdateTimestamp) { const footerLastUpdateElm = this.createFooterLastUpdate(); @@ -208,41 +205,32 @@ export class SlickFooterComponent { } } - const itemCountElm = document.createElement('span'); - itemCountElm.className = 'item-count'; - itemCountElm.textContent = `${this.metrics?.itemCount ?? '0'}`; - // last update elements rightFooterElm.appendChild(lastUpdateElm); - rightFooterElm.appendChild(itemCountElm); + rightFooterElm.appendChild( + createDomElement('span', { className: 'item-count', textContent: `${this.metrics?.itemCount ?? '0'}` }) + ); // total count element (unless hidden) if (!this.customFooterOptions.hideTotalItemCount) { // add carriage return which will add a space before the span rightFooterElm.appendChild(document.createTextNode('\r\n')); - - const textOfElm = document.createElement('span'); - textOfElm.className = 'text-of'; - textOfElm.textContent = ` ${this.customFooterOptions.metricTexts?.of ?? 'of'} `; - rightFooterElm.appendChild(textOfElm); + rightFooterElm.appendChild( + createDomElement('span', { className: 'text-of', textContent: ` ${this.customFooterOptions.metricTexts?.of ?? 'of'} ` }) + ); // add another carriage return which will add a space after the span rightFooterElm.appendChild(document.createTextNode('\r\n')); - - const totalCountElm = document.createElement('span'); - totalCountElm.className = 'total-count'; - totalCountElm.textContent = `${this.metrics?.totalItemCount ?? '0'}`; - - rightFooterElm.appendChild(totalCountElm); + rightFooterElm.appendChild( + createDomElement('span', { className: 'total-count', textContent: `${this.metrics?.totalItemCount ?? '0'}` }) + ); } // add carriage return which will add a space before the span rightFooterElm.appendChild(document.createTextNode('\r\n')); - - const textItemsElm = document.createElement('span'); - textItemsElm.className = 'text-items'; - textItemsElm.textContent = ` ${this.customFooterOptions.metricTexts?.items ?? 'items'} `; - rightFooterElm.appendChild(textItemsElm); + rightFooterElm.appendChild( + createDomElement('span', { className: 'text-items', textContent: ` ${this.customFooterOptions.metricTexts?.items ?? 'items'} ` }) + ); } return rightFooterElm; @@ -253,24 +241,17 @@ export class SlickFooterComponent { // get translated text & last timestamp const lastUpdateText = this.customFooterOptions?.metricTexts?.lastUpdate ?? 'Last Update'; const lastUpdateTimestamp = moment(this.metrics?.endTime).format(this.customFooterOptions.dateFormat); + const lastUpdateContainerElm = createDomElement('span'); - const lastUpdateElm = document.createElement('span'); - lastUpdateElm.className = 'text-last-update'; - lastUpdateElm.textContent = lastUpdateText; - - const lastUpdateTimestampElm = document.createElement('span'); - lastUpdateTimestampElm.className = 'last-update-timestamp'; - lastUpdateTimestampElm.textContent = lastUpdateTimestamp; - - const separatorElm = document.createElement('span'); - separatorElm.className = 'separator'; - separatorElm.textContent = ` ${this.customFooterOptions.metricSeparator || ''} `; - - const lastUpdateContainerElm = document.createElement('span'); - lastUpdateContainerElm.appendChild(lastUpdateElm); + lastUpdateContainerElm.appendChild( + createDomElement('span', { className: 'text-last-update', textContent: lastUpdateText })); lastUpdateContainerElm.appendChild(document.createTextNode('\r\n')); - lastUpdateContainerElm.appendChild(lastUpdateTimestampElm); - lastUpdateContainerElm.appendChild(separatorElm); + lastUpdateContainerElm.appendChild( + createDomElement('span', { className: 'last-update-timestamp', textContent: lastUpdateTimestamp }) + ); + lastUpdateContainerElm.appendChild( + createDomElement('span', { className: 'separator', textContent: ` ${this.customFooterOptions.metricSeparator || ''} ` }) + ); return lastUpdateContainerElm; } diff --git a/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts b/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts index 605a5b065..67370c58b 100644 --- a/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts +++ b/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts @@ -5,6 +5,7 @@ import { CancelledException, Column, ContainerService, + createDomElement, CustomTooltipOption, findFirstElementAttribute, Formatter, @@ -300,8 +301,7 @@ export class SlickCustomTooltip { * also clear the "title" attribute from the grid div text content so that it won't show also as a 2nd browser tooltip */ protected renderRegularTooltip(formatterOrText: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: any) { - const tmpDiv = document.createElement('div'); - tmpDiv.innerHTML = this.parseFormatterAndSanitize(formatterOrText, cell, value, columnDef, item); + const tmpDiv = createDomElement('div', { innerHTML: this.parseFormatterAndSanitize(formatterOrText, cell, value, columnDef, item) }); let tooltipText = columnDef?.toolTip ?? ''; let tmpTitleElm: HTMLDivElement | null | undefined; @@ -338,8 +338,7 @@ export class SlickCustomTooltip { protected renderTooltipFormatter(formatter: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: unknown, tooltipText?: string, inputTitleElm?: Element | null) { // create the tooltip DOM element with the text returned by the Formatter - this._tooltipElm = document.createElement('div'); - this._tooltipElm.className = this.className; + this._tooltipElm = createDomElement('div', { className: this.className }); this._tooltipElm.classList.add(this.gridUid); this._tooltipElm.classList.add('l' + cell.cell); this._tooltipElm.classList.add('r' + cell.cell); diff --git a/packages/pagination-component/src/slick-pagination.component.ts b/packages/pagination-component/src/slick-pagination.component.ts index 0a0f3ce22..899e8ebc2 100644 --- a/packages/pagination-component/src/slick-pagination.component.ts +++ b/packages/pagination-component/src/slick-pagination.component.ts @@ -1,5 +1,6 @@ import { Constants, + createDomElement, getTranslationPrefix, GridOption, Locale, @@ -134,8 +135,7 @@ export class SlickPaginationComponent { renderPagination(gridParentContainerElm: HTMLElement) { const paginationElm = this.createPaginationContainer(); - const divNavContainerElm = document.createElement('div'); - divNavContainerElm.className = 'slick-pagination-nav'; + const divNavContainerElm = createDomElement('div', { className: 'slick-pagination-nav' }); const leftNavigationElm = this.createPageNavigation('Page navigation', [ { liClass: 'page-item seek-first', aClass: 'page-link icon-seek-first', ariaLabel: 'First Page' }, { liClass: 'page-item seek-prev', aClass: 'page-link icon-seek-prev', ariaLabel: 'Previous Page' }, @@ -169,10 +169,9 @@ export class SlickPaginationComponent { const selectElm = document.querySelector(`.${this.gridUid} .items-per-page`); if (selectElm && Array.isArray(this.availablePageSizes)) { for (const option of this.availablePageSizes) { - const opt = document.createElement('option'); - opt.value = `${option}`; - opt.text = `${option}`; - selectElm.appendChild(opt); + selectElm.appendChild( + createDomElement('option', { value: `${option}`, text: `${option}` }) + ); } } } @@ -256,13 +255,10 @@ export class SlickPaginationComponent { /** Create the Pagination Container */ protected createPaginationContainer() { - const paginationContainerElm = document.createElement('div'); - paginationContainerElm.className = `slick-pagination-container ${this.gridUid} pager`; - paginationContainerElm.id = 'pager'; + const paginationContainerElm = createDomElement('div', { id: 'pager', className: `slick-pagination-container ${this.gridUid} pager` }); paginationContainerElm.style.width = '100%'; - const paginationElm = document.createElement('div'); - paginationElm.className = 'slick-pagination'; + const paginationElm = createDomElement('div', { className: 'slick-pagination' }); paginationContainerElm.appendChild(paginationElm); this._paginationElement = paginationContainerElm; // keep internal ref @@ -270,16 +266,12 @@ export class SlickPaginationComponent { } protected createPageNavigation(navAriaLabel: string, liElements: Array<{ liClass: string, aClass: string, ariaLabel: string }>) { - const navElm = document.createElement('nav'); - navElm.ariaLabel = navAriaLabel; - const ulElm = document.createElement('ul'); - ulElm.className = 'pagination'; + const navElm = createDomElement('nav', { ariaLabel: navAriaLabel }); + const ulElm = createDomElement('ul', { className: 'pagination' }); for (const li of liElements) { - const liElm = document.createElement('li'); - liElm.className = li.liClass; - const aElm = document.createElement('a'); - aElm.className = li.aClass; + const liElm = createDomElement('li', { className: li.liClass }); + const aElm = createDomElement('a', { className: li.aClass }); aElm.setAttribute('aria-label', li.ariaLabel); liElm.appendChild(aElm); ulElm.appendChild(liElm); @@ -290,24 +282,16 @@ export class SlickPaginationComponent { } protected createPageNumberSection() { - const divElm = document.createElement('div'); - divElm.className = 'slick-page-number'; - const spanTextPageElm = document.createElement('span'); - spanTextPageElm.className = 'text-page'; - spanTextPageElm.textContent = 'Page'; - const input = document.createElement('input'); - input.type = 'text'; - input.className = 'form-control page-number'; - input.dataset.test = 'page-number-input'; + const divElm = createDomElement('div', { className: 'slick-page-number' }); + const spanTextPageElm = createDomElement('span', { className: 'text-page', textContent: 'Page' }); + const input = createDomElement('input', { + type: 'text', className: 'form-control page-number', + value: '1', size: 1, + dataset: { test: 'page-number-input' }, + }); input.setAttribute('aria-label', 'Page Number'); - input.value = '1'; - input.size = 1; - const spanTextOfElm = document.createElement('span'); - spanTextOfElm.className = 'text-of'; - spanTextOfElm.textContent = 'of'; - const spanPageCountElm = document.createElement('span'); - spanPageCountElm.className = 'page-count'; - spanPageCountElm.dataset.test = 'page-count'; + const spanTextOfElm = createDomElement('span', { className: 'text-of', textContent: 'of' }); + const spanPageCountElm = createDomElement('span', { className: 'page-count', dataset: { test: 'page-count' } }); divElm.appendChild(spanTextPageElm); divElm.appendChild(document.createTextNode(' ')); divElm.appendChild(input); @@ -320,38 +304,20 @@ export class SlickPaginationComponent { } protected createPaginationSettingsSection() { - const spanContainerElm = document.createElement('span'); - spanContainerElm.className = 'slick-pagination-settings'; - const selectElm = document.createElement('select'); - selectElm.id = 'items-per-page-label'; - selectElm.className = 'items-per-page'; + const spanContainerElm = createDomElement('span', { className: 'slick-pagination-settings' }); + const selectElm = createDomElement('select', { id: 'items-per-page-label', className: 'items-per-page' }); selectElm.setAttribute('aria-label', 'Items per Page Select'); - const spanItemPerPageElm = document.createElement('span'); - spanItemPerPageElm.className = 'text-item-per-page'; - spanItemPerPageElm.textContent = 'items per page'; - const spanPaginationCount = document.createElement('span'); - spanPaginationCount.className = 'slick-pagination-count'; - const spanInfoFromToElm = document.createElement('span'); - spanInfoFromToElm.className = 'page-info-from-to'; - const spanItemFromElm = document.createElement('span'); - spanItemFromElm.className = 'item-from'; - spanItemFromElm.dataset.test = 'item-from'; + const spanItemPerPageElm = createDomElement('span', { className: 'text-item-per-page', textContent: 'items per page' }); + const spanPaginationCount = createDomElement('span', { className: 'slick-pagination-count' }); + const spanInfoFromToElm = createDomElement('span', { className: 'page-info-from-to' }); + const spanItemFromElm = createDomElement('span', { className: 'item-from', dataset: { test: 'item-from' } }); spanItemFromElm.setAttribute('aria-label', 'Page Item From'); - const spanItemToElm = document.createElement('span'); - spanItemToElm.className = 'item-to'; - spanItemToElm.dataset.test = 'item-to'; + const spanItemToElm = createDomElement('span', { className: 'item-to', dataset: { test: 'item-to' } }); spanItemToElm.setAttribute('aria-label', 'Page Item To'); - const spanOfElm = document.createElement('span'); - spanOfElm.className = 'text-of'; - spanOfElm.textContent = 'of'; - const spanInfoTotalElm = document.createElement('span'); - spanInfoTotalElm.className = 'page-info-total-items'; - const spanTotalItem = document.createElement('span'); - spanTotalItem.className = 'total-items'; - spanTotalItem.dataset.test = 'total-items'; - const spanTextItemsElm = document.createElement('span'); - spanTextItemsElm.className = 'text-items'; - spanTextItemsElm.textContent = 'items'; + const spanOfElm = createDomElement('span', { className: 'text-of', textContent: 'of' }); + const spanInfoTotalElm = createDomElement('span', { className: 'page-info-total-items' }); + const spanTotalItem = createDomElement('span', { className: 'total-items', dataset: { test: 'total-items' } }); + const spanTextItemsElm = createDomElement('span', { className: 'text-items', textContent: 'items' }); spanContainerElm.appendChild(selectElm); spanContainerElm.appendChild(document.createTextNode(' '));