Skip to content

Commit

Permalink
feat(composite): move SlickGrid Composite Editor factory into universal
Browse files Browse the repository at this point in the history
- instead of using Slick.CompositeEditor from the SlickGrid fork, let's move the code into Slickgrid-Universal
  • Loading branch information
ghiscoding committed Aug 19, 2021
1 parent 01158bb commit c813cea
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -509,18 +509,19 @@ 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) {
const columnName = error.editor.args.column.name;
errorMsg += `${columnName.toUpperCase()}: ${error.msg}`;
}
}
// this.compositeEditorInstance.showValidationSummaryText(true, errorMsg);
console.log(errorMsg);
}
} else {
Expand Down
4 changes: 3 additions & 1 deletion packages/common/src/interfaces/editorArguments.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Column, CompositeEditorOption, ElementPosition, SlickDataView, SlickGrid } from './index';

export type PositionMethod = () => ElementPosition;

export interface EditorArguments {
/** Column Definition */
column: Column;
Expand All @@ -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;
Expand Down
266 changes: 266 additions & 0 deletions packages/composite-editor-component/src/compositeEditor.factory.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>, 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<Editor & { args: EditorArguments }> = [];

function init() {
let newArgs: Partial<CompositeEditorArguments> = {};
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;
}
1 change: 1 addition & 0 deletions packages/composite-editor-component/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
1 change: 1 addition & 0 deletions packages/composite-editor-component/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './compositeEditor.factory';
export * from './slick-composite-editor.component';
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'slickgrid/slick.compositeeditor.js';
import * as assign_ from 'assign-deep';
const assign = (assign_ as any)['default'] || assign_;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -507,7 +507,7 @@ export class SlickCompositeEditorComponent implements ExternalResource {
this._editors = {};
this._editorContainers = modalColumns.map(col => modalBodyElm.querySelector<HTMLDivElement>(`[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);

// --
Expand Down

0 comments on commit c813cea

Please sign in to comment.