diff --git a/packages/common/src/editors/comboInputEditor.ts b/packages/common/src/editors/dualInputEditor.ts similarity index 59% rename from packages/common/src/editors/comboInputEditor.ts rename to packages/common/src/editors/dualInputEditor.ts index 1b24253b2..b85f38983 100644 --- a/packages/common/src/editors/comboInputEditor.ts +++ b/packages/common/src/editors/dualInputEditor.ts @@ -1,17 +1,27 @@ import { KeyCode } from '../enums/keyCode.enum'; -import { Column, ColumnEditor, ColumnEditorComboInput, Editor, EditorArguments, EditorValidator, EditorValidatorOutput } from '../interfaces/index'; import { getDescendantProperty, setDeepValue } from '../services/utilities'; -import { floatValidator } from '../editorValidators/floatValidator'; -import { textValidator, integerValidator } from '../editorValidators'; +import { floatValidator, integerValidator, textValidator } from '../editorValidators'; +import { + Column, + ColumnEditor, + ColumnEditorComboInput, + Editor, + EditorArguments, + EditorValidator, + EditorValidatorOutput, + SlickEventHandler +} from '../interfaces/index'; + +// using external non-typed js libraries +declare const Slick: any; /* * An example of a 'detached' editor. * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. */ -export class ComboInputEditor implements Editor { - protected _inputType = 'number'; - private _cellContainerClassName: string; - private _previousColumnItemIds: string; +export class DualInputEditor implements Editor { + private _eventHandler: SlickEventHandler; + private _isValueSaveCalled = false; private _lastEventType: string | undefined; private _lastInputKeyEvent: KeyboardEvent; private _leftInput: HTMLInputElement; @@ -30,6 +40,8 @@ export class ComboInputEditor implements Editor { } this.grid = args.grid; this.init(); + this._eventHandler = new Slick.EventHandler(); + this._eventHandler.subscribe(this.grid.onValidationError, () => this._isValueSaveCalled = true); } /** Get Column Definition object */ @@ -51,19 +63,12 @@ export class ComboInputEditor implements Editor { return this.columnEditor.params || {}; } - get hasAutoCommitEdit() { - return this.grid.getOptions().autoCommitEdit; - } - - - /** Getter of input type (text, number, password) */ - get inputType() { - return this._inputType; + get eventHandler(): SlickEventHandler { + return this._eventHandler; } - /** Setter of input type (text, number, password) */ - set inputType(type: string) { - this._inputType = type; + get hasAutoCommitEdit() { + return this.grid.getOptions().autoCommitEdit; } /** Get the Validator function, can be passed in Editor property or Column Definition */ @@ -79,101 +84,48 @@ export class ComboInputEditor implements Editor { this._rightFieldName = this.editorParams.rightInput?.field; this._leftInput = this.createInput('leftInput'); this._rightInput = this.createInput('rightInput'); - const columnId = this.columnDef && this.columnDef.id; - const itemId = this.args?.item?.id || 0; - this._previousColumnItemIds = columnId + itemId; const containerElm = this.args?.container; if (containerElm && typeof containerElm.appendChild === 'function') { - this._cellContainerClassName = containerElm.className; containerElm.appendChild(this._leftInput); containerElm.appendChild(this._rightInput); } this._leftInput.onkeydown = this.handleKeyDown; this._rightInput.onkeydown = this.handleKeyDown; - // this._leftInput.oninput = this.restrictDecimalWhenProvided.bind(this, 'leftInput'); - // this._rightInput.oninput = this.restrictDecimalWhenProvided.bind(this, 'rightInput'); // the lib does not get the focus out event for some reason, so register it here if (this.hasAutoCommitEdit) { - // this._leftInput.addEventListener('focusout', this.handleFocusOut.bind(this)); - // this._rightInput.addEventListener('focusout', this.handleFocusOut.bind(this)); - // for the left input, we'll save if next element isn't a combo editor - this._leftInput.addEventListener('focusout', (event: any) => { - console.log('left focusout') - const nextTargetClass = event.relatedTarget?.className || ''; - // const parentClass = this._cellContainerClassName.className || ''; - const columnId = this.columnDef && this.columnDef.id; - const itemId = this.args?.item?.id || 0; - console.log(nextTargetClass, columnId, itemId) - const targetClassNames = event.relatedTarget?.className || ''; - // if (this._previousColumnItemIds !== (columnId + itemId)) { - if (targetClassNames.indexOf('compound-editor') === -1 && this._lastEventType !== 'focusout') { - // if (nextTargetClass !== parentClass) { - console.log('calls save') - this.save(); - } - this._lastEventType = event.type; - this._previousColumnItemIds = columnId + itemId; - }); - this._rightInput.addEventListener('focusout', (event: any) => { - console.log('right focusout') - const nextTargetClass = event.relatedTarget?.parentNode?.className || ''; - // const parentClass = this._cellContainerClassName.className || ''; - console.log(nextTargetClass, columnId, itemId) - if (nextTargetClass !== this._cellContainerClassName) { - this.save(); - } - this._lastEventType = event && event.type; - }); + this._leftInput.addEventListener('focusout', (event: any) => this.handleFocusOut(event, 'leftInput')); + this._rightInput.addEventListener('focusout', (event: any) => this.handleFocusOut(event, 'rightInput')); } - setTimeout(() => this.focus(), 50); + setTimeout(() => this._leftInput.select(), 50); } - handleKeyDown(event: KeyboardEvent) { - this._lastInputKeyEvent = event; - if (event.keyCode === KeyCode.LEFT || event.keyCode === KeyCode.RIGHT || event.keyCode === KeyCode.TAB) { - event.stopImmediatePropagation(); - } - } - - handleFocusOut(event: any) { - const nextTargetClass = event.relatedTarget?.className || ''; - // const parentClass = this._cellContainerClassName.className || ''; - const columnId = this.columnDef && this.columnDef.id; - const itemId = this.args?.item?.id || 0; - console.log(this._previousColumnItemIds, columnId, itemId, this.args, 'nextTargetClass::', nextTargetClass) + handleFocusOut(event: any, position: 'leftInput' | 'rightInput') { + // when clicking outside the editable cell OR when focusing out of it const targetClassNames = event.relatedTarget?.className || ''; - if (this._previousColumnItemIds !== (columnId + itemId)) { - // if (this._previousColumnItemIds !== (columnId + itemId) || nextTargetClass.indexOf(`compound-editor-text editor-${columnId}`) === -1) { - // if (targetClassNames.indexOf('compound-editor') === -1 && this._lastEventType !== 'focusout') { - // if (nextTargetClass !== parentClass) { - this.save(); + if (targetClassNames.indexOf('compound-editor') === -1 && this._lastEventType !== 'focusout-right') { + if (position === 'rightInput' || (position === 'leftInput' && this._lastEventType !== 'focusout-left')) { + this.save(); + } } - this._lastEventType = event.type; - this._previousColumnItemIds = columnId + itemId; + const side = (position === 'leftInput') ? 'left' : 'right'; + this._lastEventType = `${event?.type}-${side}`; } - restrictDecimalWhenProvided(position: 'leftInput' | 'rightInput', event: KeyboardEvent & { target: HTMLInputElement }) { - const maxDecimal = this.getDecimalPlaces(position); - console.log(event.target.value) - if (maxDecimal >= 0 && event && event.target) { - const currentVal = event.target.value; - // const pattern = maxDecimal === 0 ? '^-?[0-9]+' : `^-?\\d+\\.?\\d{0,${maxDecimal}}`; - const pattern = maxDecimal === 0 ? '^-?\\d*$' : `^[1-9]\\d*(?:\\.\\d{0,${maxDecimal}})?$`; - const regex = new RegExp(pattern); - if (!regex.test(currentVal)) { - console.log('invalid', currentVal, currentVal.substring(0, currentVal.length - 1)) - event.target.value = currentVal.substring(0, currentVal.length - 1); - } else { - console.log(currentVal, 'valid', maxDecimal, pattern, regex.test(currentVal)) - } + handleKeyDown(event: KeyboardEvent) { + this._lastInputKeyEvent = event; + if (event.keyCode === KeyCode.LEFT || event.keyCode === KeyCode.RIGHT || event.keyCode === KeyCode.TAB) { + event.stopImmediatePropagation(); } } destroy() { + // unsubscribe all SlickGrid events + this._eventHandler.unsubscribeAll(); + const columnId = this.columnDef && this.columnDef.id; const elm = document.querySelector(`.compound-editor-text.editor-${columnId}`); if (elm) { @@ -187,22 +139,27 @@ export class ComboInputEditor implements Editor { const columnId = this.columnDef && this.columnDef.id; const itemId = this.args?.item?.id || 0; + let fieldType = editorSideParams.type || 'text'; + if (fieldType === 'float' || fieldType === 'integer') { + fieldType = 'number'; + } + const input = document.createElement('input') as HTMLInputElement; input.id = `item-${itemId}`; input.className = `compound-editor-text editor-${columnId} ${position.replace(/input/gi, '')}`; - input.type = editorSideParams?.type || 'text'; + input.type = fieldType || 'text'; input.setAttribute('role', 'presentation'); input.autocomplete = 'off'; - input.placeholder = editorSideParams?.placeholder || ''; - input.title = editorSideParams?.title || ''; - input.step = this.getInputDecimalSteps(position); - + input.placeholder = editorSideParams.placeholder || ''; + input.title = editorSideParams.title || ''; + if (fieldType === 'number') { + input.step = this.getInputDecimalSteps(position); + } return input; } - focus(): void { - this._leftInput.focus(); - this._leftInput.select(); + focus() { + // do nothing since we have 2 inputs and we might focus on left/right depending on which is invalid or new } getValue(): string { @@ -249,39 +206,43 @@ export class ComboInputEditor implements Editor { } loadValue(item: any) { - // is the field a complex object, "address.streetNumber" - const isComplexObject = this._leftFieldName && this._leftFieldName.indexOf('.') > 0; - - if (item && this._leftFieldName !== undefined && this.columnDef && (item.hasOwnProperty(this._leftFieldName) || isComplexObject)) { - const leftValue = (isComplexObject) ? getDescendantProperty(item, this._leftFieldName) : (item.hasOwnProperty(this._leftFieldName) && item[this._leftFieldName] || ''); - this.originalLeftValue = leftValue; - const leftDecimal = this.getDecimalPlaces('leftInput'); - if (leftDecimal !== null && (this.originalLeftValue || this.originalLeftValue === 0) && (+this.originalLeftValue).toFixed) { - this.originalLeftValue = (+this.originalLeftValue).toFixed(leftDecimal); - } - this._leftInput.value = `${this.originalLeftValue}`; - this._leftInput.select(); - } + this.loadValueByPosition(item, 'leftInput'); + this.loadValueByPosition(item, 'rightInput'); + this._leftInput.select(); + } - if (item && this._rightFieldName !== undefined && this.columnDef && (item.hasOwnProperty(this._rightFieldName) || isComplexObject)) { - const rightValue = (isComplexObject) ? getDescendantProperty(item, this._rightFieldName) : (item.hasOwnProperty(this._rightFieldName) && item[this._rightFieldName] || ''); - this.originalRightValue = rightValue; - const rightDecimal = this.getDecimalPlaces('rightInput'); - if (rightDecimal !== null && (this.originalRightValue || this.originalRightValue === 0) && (+this.originalRightValue).toFixed) { - this.originalRightValue = (+this.originalRightValue).toFixed(rightDecimal); + loadValueByPosition(item: any, position: 'leftInput' | 'rightInput') { + // is the field a complex object, "address.streetNumber" + const fieldName = (position === 'leftInput') ? this._leftFieldName : this._rightFieldName; + const originalValuePosition = (position === 'leftInput') ? 'originalLeftValue' : 'originalRightValue'; + const inputVarPosition = (position === 'leftInput') ? '_leftInput' : '_rightInput'; + const isComplexObject = fieldName && fieldName.indexOf('.') > 0; + + if (item && fieldName !== undefined && this.columnDef && (item.hasOwnProperty(fieldName) || isComplexObject)) { + const itemValue = (isComplexObject) ? getDescendantProperty(item, fieldName) : (item.hasOwnProperty(fieldName) && item[fieldName] || ''); + this[originalValuePosition] = itemValue; + if (this.editorParams[position].type === 'float') { + const decimalPlaces = this.getDecimalPlaces(position); + if (decimalPlaces !== null && (this[originalValuePosition] || this[originalValuePosition] === 0) && (+this[originalValuePosition]).toFixed) { + this[originalValuePosition] = (+this[originalValuePosition]).toFixed(decimalPlaces); + } } - this._rightInput.value = `${this.originalRightValue}`; + this[inputVarPosition].value = `${this[originalValuePosition]}`; } } save() { const validation = this.validate(); const isValid = (validation && validation.valid) || false; + const isChanged = this.isValueChanged(); - if (this.hasAutoCommitEdit && isValid) { - this.grid.getEditorLock().commitCurrentEdit(); - } else { - this.args.commitChanges(); + if (!this._isValueSaveCalled) { + if (this.hasAutoCommitEdit && isValid) { + this.grid.getEditorLock().commitCurrentEdit(); + } else { + this.args.commitChanges(); + } + this._isValueSaveCalled = true; } } @@ -339,9 +300,11 @@ export class ComboInputEditor implements Editor { const rightValidation = this.validateByPosition('rightInput'); if (!leftValidation.valid) { + this._leftInput.select(); return leftValidation; } if (!rightValidation.valid) { + this._rightInput.select(); return rightValidation; } return { valid: true, msg: null }; diff --git a/packages/common/src/editors/index.ts b/packages/common/src/editors/index.ts index 5b07671f8..d74c0899d 100644 --- a/packages/common/src/editors/index.ts +++ b/packages/common/src/editors/index.ts @@ -1,7 +1,7 @@ import { AutoCompleteEditor } from './autoCompleteEditor'; import { CheckboxEditor } from './checkboxEditor'; -import { ComboInputEditor } from './comboInputEditor'; import { DateEditor } from './dateEditor'; +import { DualInputEditor } from './dualInputEditor'; import { FloatEditor } from './floatEditor'; import { IntegerEditor } from './integerEditor'; import { LongTextEditor } from './longTextEditor'; @@ -17,12 +17,12 @@ export const Editors = { /** Checkbox Editor (uses native checkbox DOM element) */ checkbox: CheckboxEditor, - /** Dual Input Float Editor */ - comboInput: ComboInputEditor, - /** Date Picker Editor (which uses 3rd party lib "flatpickr") */ date: DateEditor, + /** Dual Input Editor, default input type is text but it could be (integer/float/number/password/text) */ + dualInput: DualInputEditor, + /** Float Number Editor */ float: FloatEditor, diff --git a/packages/common/src/interfaces/columnEditorComboInput.interface.ts b/packages/common/src/interfaces/columnEditorComboInput.interface.ts index 1af72f9b3..2b96a1a8a 100644 --- a/packages/common/src/interfaces/columnEditorComboInput.interface.ts +++ b/packages/common/src/interfaces/columnEditorComboInput.interface.ts @@ -5,7 +5,7 @@ interface EditorComboInput extends Partial { field: string; /** Editor Type */ - type: 'integer' | 'float' | 'password' | 'text'; + type: 'integer' | 'float' | 'number' | 'password' | 'text'; } export interface ColumnEditorComboInput { diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 197ae168d..59aff2ec8 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -18,12 +18,12 @@ export * from './columnFilter.interface'; export * from './columnFilters.interface'; export * from './columnPicker.interface'; export * from './columnSort.interface'; -export * from './contextMenu.interface'; export * from './currentColumn.interface'; export * from './currentFilter.interface'; export * from './currentPagination.interface'; export * from './currentRowSelection.interface'; export * from './currentSorter.interface'; +export * from './contextMenu.interface'; export * from './customFooterOption.interface'; export * from './draggableGrouping.interface'; export * from './editCommand.interface'; @@ -68,13 +68,13 @@ export * from './groupingFormatterItem.interface'; export * from './groupTotalsFormatter.interface'; export * from './headerButton.interface'; export * from './headerButtonItem.interface'; +export * from './headerButtonOnCommandArgs.interface'; export * from './headerMenu.interface'; export * from './htmlElementPosition.interface'; -export * from './headerButtonOnCommandArgs.interface'; export * from './jQueryUiSliderOption.interface'; export * from './jQueryUiSliderResponse.interface'; export * from './keyTitlePair.interface'; -export * from './menuCallbackArgs.interface'; +export * from './locale.interface'; export * from './menuCommandItem.interface'; export * from './menuCommandItemCallbackArgs.interface'; export * from './menuItem.interface'; @@ -84,16 +84,17 @@ export * from './metrics.interface'; export * from './multiColumnSort.interface'; export * from './multipleSelectOption.interface'; export * from './onEventArgs.interface'; +export * from './onValidationErrorResult.interface'; export * from './pagination.interface'; export * from './paginationChangedArgs.interface'; +export * from './menuCallbackArgs.interface'; export * from './rowMoveManager.interface'; -export * from './locale.interface'; export * from './selectedRange.interface'; export * from './servicePagination.interface'; export * from './slickEvent.interface'; export * from './slickEventData.interface'; +export * from './slickEventHandler.interface'; export * from './selectOption.interface'; export * from './sorter.interface'; export * from './subscription.interface'; export * from './treeDataOption.interface'; -export * from './slickEventHandler.interface'; diff --git a/packages/common/src/interfaces/onValidationErrorResult.interface.ts b/packages/common/src/interfaces/onValidationErrorResult.interface.ts new file mode 100644 index 000000000..18d8a6c11 --- /dev/null +++ b/packages/common/src/interfaces/onValidationErrorResult.interface.ts @@ -0,0 +1,5 @@ +import { EditorValidatorOutput } from './editorValidatorOutput.interface'; + +export interface OnValidationErrorResult { + validationResults: EditorValidatorOutput; +} diff --git a/packages/web-demo-vanilla-bundle/src/examples/example04.ts b/packages/web-demo-vanilla-bundle/src/examples/example04.ts index 81036b9a5..3c583d7e7 100644 --- a/packages/web-demo-vanilla-bundle/src/examples/example04.ts +++ b/packages/web-demo-vanilla-bundle/src/examples/example04.ts @@ -1,4 +1,4 @@ -import { AutocompleteOption, Column, ColumnEditorComboInput, Editors, FieldType, Filters, Formatters, OperatorType, GridOption, ColumnEditor, Editor } from '@slickgrid-universal/common'; +import { AutocompleteOption, Column, ColumnEditorComboInput, Editors, FieldType, Filters, Formatters, OperatorType, GridOption } from '@slickgrid-universal/common'; import { Slicker } from '@slickgrid-universal/vanilla-bundle'; import { ExampleGridOptions } from './example-grid-options'; @@ -31,8 +31,8 @@ export class Example4 { const gridElm = document.querySelector(`.slickgrid-container`); // gridContainerElm.addEventListener('onclick', handleOnClick); - gridContainerElm.addEventListener('onvalidationerror', this.handleValidationError.bind(this)); - gridContainerElm.addEventListener('onitemdeleted', this.handleItemDeleted.bind(this)); + gridContainerElm.addEventListener('onvalidationerror', this.handleOnValidationError.bind(this)); + gridContainerElm.addEventListener('onitemdeleted', this.handleOnItemDeleted.bind(this)); gridContainerElm.addEventListener('onslickergridcreated', this.handleOnSlickerGridCreated.bind(this)); this.slickgridLwc = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, dataset); } @@ -116,8 +116,8 @@ export class Example4 { model: Filters.compoundSlider, }, editor: { - model: Editors.comboInput, - // the ComboInputEditor MUST include the params object with (leftInput/rightInput) + model: Editors.dualInput, + // the DualInputEditor is of Type ColumnEditorComboInput and MUST include (leftInput/rightInput) in its params object // in each of these 2 properties, you can pass any regular properties of a column editor // and they will be executed following the options defined in each params: { @@ -126,18 +126,19 @@ export class Example4 { type: 'float', decimal: 2, minValue: 0, - maxValue: 9999, - placeholder: '<100K', - errorMessage: 'Cost must be positive and below $100K.', + maxValue: 50000, + placeholder: '< 50K', + errorMessage: 'Cost must be positive and below $50K.', }, rightInput: { field: 'duration', - type: 'float', - // decimal: 0, + type: 'float', // you could have 2 different input type as well minValue: 0, maxValue: 100, + title: 'make sure Duration is withing its range of 0 to 100', errorMessage: 'Duration must be between 0 and 100.', - // you can also optionally define a validator in 1 or both input + + // You could also optionally define a custom validator in 1 or both inputs // validator: (value, args) => { // let isValid = true; // let errorMsg = ''; @@ -331,7 +332,7 @@ export class Example4 { percentComplete: Math.round(Math.random() * 100), start: new Date(randomYear, randomMonth, randomDay), finish: new Date(randomYear, (randomMonth + 1), randomDay), - cost: (i % 33 === 0) ? null : Math.random() * 1000, + cost: (i % 33 === 0) ? null : Math.random() * 10000, completed: (i % 5 === 0), cityOfOrigin: (i % 2) ? 'Vancouver, BC, Canada' : 'Boston, MA, United States', }; @@ -353,8 +354,8 @@ export class Example4 { console.log('onClick', event.detail); } - handleValidationError(event) { - console.log('handleValidationError', event.detail); + handleOnValidationError(event) { + console.log('handleOnValidationError', event.detail); const args = event.detail && event.detail.args; if (args.validationResults) { alert(args.validationResults.msg); @@ -362,7 +363,7 @@ export class Example4 { } } - handleItemDeleted(event) { + handleOnItemDeleted(event) { const itemId = event && event.detail; console.log('item deleted with id:', itemId); }