From c813ceac1ed6535963df15e7933a444de3a8790a Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Thu, 19 Aug 2021 19:36:41 -0400 Subject: [PATCH] feat(composite): move SlickGrid Composite Editor factory into universal - instead of using Slick.CompositeEditor from the SlickGrid fork, let's move the code into Slickgrid-Universal --- .../src/examples/example12.ts | 9 +- .../interfaces/editorArguments.interface.ts | 4 +- .../src/compositeEditor.factory.ts | 266 ++++++++++++++++++ .../src/index.spec.ts | 1 + .../composite-editor-component/src/index.ts | 1 + .../src/slick-composite-editor.component.ts | 6 +- 6 files changed, 279 insertions(+), 8 deletions(-) create mode 100644 packages/composite-editor-component/src/compositeEditor.factory.ts diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts index 82fc498a1..2ba5f7dbe 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts @@ -1,4 +1,4 @@ -import { Instance as FlatpickrInstance } from 'flatpickr/dist/types/instance'; +// import { Instance as FlatpickrInstance } from 'flatpickr/dist/types/instance'; import { AutocompleteOption, BindingEventService, @@ -22,7 +22,7 @@ import { } from '@slickgrid-universal/common'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; import { Slicker, SlickerGridInstance, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; -import { SlickCompositeEditorComponent } from '@slickgrid-universal/composite-editor-component'; +import { CompositeEditor, SlickCompositeEditorComponent } from '@slickgrid-universal/composite-editor-component'; import { ExampleGridOptions } from './example-grid-options'; import '../salesforce-styles.scss'; @@ -509,11 +509,11 @@ export class Example12 { } handleValidationError(event) { - console.log('handleValidationError', event.detail); const args = event.detail && event.detail.args; + console.log('handleValidationError', event.detail); if (args.validationResults) { let errorMsg = args.validationResults.msg || ''; - if (args.editor && (args.editor instanceof Slick.CompositeEditor)) { + if (args?.editor instanceof CompositeEditor) { if (args.validationResults.errors) { errorMsg += '\n'; for (const error of args.validationResults.errors) { @@ -521,6 +521,7 @@ export class Example12 { errorMsg += `${columnName.toUpperCase()}: ${error.msg}`; } } + // this.compositeEditorInstance.showValidationSummaryText(true, errorMsg); console.log(errorMsg); } } else { diff --git a/packages/common/src/interfaces/editorArguments.interface.ts b/packages/common/src/interfaces/editorArguments.interface.ts index 4882bf75e..c13790b79 100644 --- a/packages/common/src/interfaces/editorArguments.interface.ts +++ b/packages/common/src/interfaces/editorArguments.interface.ts @@ -1,5 +1,7 @@ import { Column, CompositeEditorOption, ElementPosition, SlickDataView, SlickGrid } from './index'; +export type PositionMethod = () => ElementPosition; + export interface EditorArguments { /** Column Definition */ column: Column; @@ -26,7 +28,7 @@ export interface EditorArguments { item: any; /** Editor Position */ - position: ElementPosition; + position: PositionMethod | ElementPosition; /** When it's a Composite Editor (that is when it's an Editor created by the Composite Editor Modal window) */ compositeEditorOptions?: CompositeEditorOption; diff --git a/packages/composite-editor-component/src/compositeEditor.factory.ts b/packages/composite-editor-component/src/compositeEditor.factory.ts new file mode 100644 index 000000000..687caba62 --- /dev/null +++ b/packages/composite-editor-component/src/compositeEditor.factory.ts @@ -0,0 +1,266 @@ +import { + Column, + CompositeEditorOption, + Editor, + EditorArguments, + EditorValidationResult, + ElementPosition, + HtmlElementPosition, + SlickNamespace +} from '@slickgrid-universal/common'; + +// using external non-typed js libraries +declare const Slick: SlickNamespace; + +export interface CompositeEditorArguments extends EditorArguments { + formValues: any; +} + +/** + * A composite SlickGrid editor factory. + * Generates an editor that is composed of multiple editors for given columns. + * Individual editors are provided given containers instead of the original cell. + * Validation will be performed on all editors individually and the results will be aggregated into one + * validation result. + * + * + * The returned editor will have its prototype set to CompositeEditor, so you can use the "instanceof" check. + * + * NOTE: This doesn't work for detached editors since they will be created and positioned relative to the + * active cell and not the provided container. + * + * @class CompositeEditor + * @constructor + * @param columns {Array} Column definitions from which editors will be pulled. + * @param containers {Array} Container HTMLElements in which editors will be placed. + * @param options {Object} Options hash: + * validationFailedMsg - A generic failed validation message set on the aggregated validation resuls. + * validationMsgPrefix - Add an optional prefix to each validation message (only the ones shown in the modal form, not the ones in the "errors") + * modalType - Defaults to "edit", modal type can 1 of these 3: (create, edit, mass, mass-selection) + * hide - A function to be called when the grid asks the editor to hide itself. + * show - A function to be called when the grid asks the editor to show itself. + * position - A function to be called when the grid asks the editor to reposition itself. + * destroy - A function to be called when the editor is destroyed. + */ +export function CompositeEditor(this: any, columns: Column[], containers: Array, options: CompositeEditorOption) { + const defaultOptions = { + modalType: 'edit', // available type (create, clone, edit, mass) + validationFailedMsg: 'Some of the fields have failed validation', + validationMsgPrefix: null, + show: null, + hide: null, + position: null, + destroy: null, + formValues: {}, + editors: {} + } as unknown as CompositeEditorOption; + options = { ...defaultOptions, ...options }; + let firstInvalidEditor: any; + + const noop = function () { }; + + function getContainerBox(i: number): ElementPosition { + const c = containers[i]; + const offset = $(c).offset(); + const w = $(c).width() || 0; + const h = $(c).height() || 0; + + return { + top: offset?.top ?? 0, + left: offset?.left ?? 0, + bottom: (offset?.top ?? 0) + h, + right: (offset?.left ?? 0) + w, + width: w, + height: h, + visible: true + }; + } + + /* Editor prototype that will get instantiated dynamically by looping through each Editors */ + function editor(this: any, args: EditorArguments) { + let editors: Array = []; + + function init() { + let newArgs: Partial = {}; + let idx = 0; + while (idx < columns.length) { + if (columns[idx].editor) { + const column = columns[idx]; + newArgs = $.extend({}, args as unknown as CompositeEditorArguments); + newArgs.container = containers[idx]; + newArgs.column = column; + newArgs.position = getContainerBox(idx); + newArgs.commitChanges = noop; + newArgs.cancelChanges = noop; + newArgs.compositeEditorOptions = options; + newArgs.formValues = {}; + + // column.editor as < typeof Editor; + const currentEditor = new (column.editor as any)(newArgs); + options.editors[column.id] = currentEditor; // add every Editor instance refs + editors.push(currentEditor); + } + idx++; + } + + // focus on first input + setTimeout(function () { + if (Array.isArray(editors) && editors.length > 0 && editors[0].focus) { + editors[0].focus(); + } + }, 0); + } + + this.destroy = function () { + let idx = 0; + while (idx < editors.length) { + editors[idx].destroy(); + idx++; + } + + options?.destroy?.(); + editors = []; + }; + + + this.focus = function () { + // if validation has failed, set the focus to the first invalid editor + (firstInvalidEditor || editors[0]).focus(); + }; + + + this.isValueChanged = function () { + let idx = 0; + while (idx < editors.length) { + if (editors[idx].isValueChanged()) { + return true; + } + idx++; + } + return false; + }; + + + this.serializeValue = function () { + const serializedValue = []; + let idx = 0; + while (idx < editors.length) { + serializedValue[idx] = editors[idx].serializeValue(); + idx++; + } + return serializedValue; + }; + + + this.applyValue = function (item: any, state: any) { + let idx = 0; + while (idx < editors.length) { + editors[idx].applyValue(item, state[idx]); + idx++; + } + }; + + this.loadValue = function (item: any) { + let idx = 0; + + while (idx < editors.length) { + editors[idx].loadValue(item); + idx++; + } + }; + + + this.validate = function (targetElm: HTMLElement) { + let validationResults: EditorValidationResult; + const errors = []; + let $targetElm = targetElm ? $(targetElm) : null; + + firstInvalidEditor = null; + + let idx = 0; + while (idx < editors.length) { + const columnDef = editors[idx].args?.column ?? {}; + if (columnDef) { + let $validationElm = $(`.item-details-validation.editor-${columnDef.id}`); + let $labelElm = $(`.item-details-label.editor-${columnDef.id}`); + let $editorElm = $(`[data-editorid=${columnDef.id}]`); + const validationMsgPrefix = options?.validationMsgPrefix || ''; + + if (!$targetElm || ($editorElm.has($targetElm as any).length > 0)) { + validationResults = editors[idx].validate(); + + if (!validationResults.valid) { + firstInvalidEditor = editors[idx]; + errors.push({ + index: idx, + editor: editors[idx], + container: containers[idx], + msg: validationResults.msg + }); + + if ($validationElm) { + $validationElm.text(validationMsgPrefix + validationResults.msg); + $labelElm.addClass('invalid'); + $editorElm.addClass('invalid'); + } + } else if ($validationElm) { + $validationElm.text(''); + $editorElm.removeClass('invalid'); + $labelElm.removeClass('invalid'); + } + } + $validationElm = null as any; + $labelElm = null as any; + $editorElm = null as any; + } + idx++; + } + $targetElm = null as any; + + if (errors.length) { + return { + valid: false, + msg: options.validationFailedMsg, + errors + }; + } + return { + valid: true, + msg: '' + }; + }; + + + this.hide = function () { + let idx = 0; + while (idx < editors.length) { + editors[idx]?.hide?.(); + idx++; + } + options?.hide?.(); + }; + + + this.show = function () { + let idx = 0; + while (idx < editors.length) { + editors[idx]?.show?.(); + idx++; + } + options?.show?.(); + }; + + + this.position = function (box: HtmlElementPosition) { + options?.position?.(box); + }; + + // initialize current editor + init(); + } + + // so we can do 'editor instanceof Slick.CompositeEditor OR instanceof CompositeEditor + editor.prototype = this; + Slick.CompositeEditor = editor as any; + return editor; +} \ No newline at end of file diff --git a/packages/composite-editor-component/src/index.spec.ts b/packages/composite-editor-component/src/index.spec.ts index 1532c77ee..ee11a39f5 100644 --- a/packages/composite-editor-component/src/index.spec.ts +++ b/packages/composite-editor-component/src/index.spec.ts @@ -6,6 +6,7 @@ describe('Testing library entry point', () => { }); it('should have all exported object defined', () => { + expect(typeof entry.CompositeEditor).toBe('function'); expect(typeof entry.SlickCompositeEditorComponent).toBe('function'); }); }); diff --git a/packages/composite-editor-component/src/index.ts b/packages/composite-editor-component/src/index.ts index b3e3f922f..058a87ded 100644 --- a/packages/composite-editor-component/src/index.ts +++ b/packages/composite-editor-component/src/index.ts @@ -1 +1,2 @@ +export * from './compositeEditor.factory'; export * from './slick-composite-editor.component'; 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 d9b6ff608..3f944b13b 100644 --- a/packages/composite-editor-component/src/slick-composite-editor.component.ts +++ b/packages/composite-editor-component/src/slick-composite-editor.component.ts @@ -1,4 +1,3 @@ -import 'slickgrid/slick.compositeeditor.js'; import * as assign_ from 'assign-deep'; const assign = (assign_ as any)['default'] || assign_; @@ -37,6 +36,7 @@ import { SortDirectionNumber, TranslaterService, } from '@slickgrid-universal/common'; +import { CompositeEditor } from './compositeEditor.factory'; // using external non-typed js libraries declare const Slick: SlickNamespace; @@ -164,7 +164,7 @@ export class SlickCompositeEditorComponent implements ExternalResource { throw new Error(`Composite Editor with column id "${columnId}" not found.`); } - if (editor && editor.setValue && Array.isArray(this._editorContainers)) { + if (typeof editor.setValue === 'function' && Array.isArray(this._editorContainers)) { editor.setValue(newValue, true, triggerOnCompositeEditorChange); const editorContainerElm = (this._editorContainers as HTMLElement[]).find(editorElm => editorElm!.dataset!.editorid === columnId); const excludeDisabledFieldFormValues = this.gridOptions?.compositeEditorOptions?.excludeDisabledFieldFormValues ?? false; @@ -507,7 +507,7 @@ export class SlickCompositeEditorComponent implements ExternalResource { this._editors = {}; this._editorContainers = modalColumns.map(col => modalBodyElm.querySelector(`[data-editorid=${col.id}]`)) || []; this._compositeOptions = { destroy: this.disposeComponent.bind(this), modalType, validationMsgPrefix: '* ', formValues: {}, editors: this._editors }; - const compositeEditor = new Slick.CompositeEditor(modalColumns, this._editorContainers, this._compositeOptions); + const compositeEditor = new (CompositeEditor as any)(modalColumns, this._editorContainers, this._compositeOptions); this.grid.editActiveCell(compositeEditor); // --