Skip to content

Commit

Permalink
feat(editor): fully working dual input editor
Browse files Browse the repository at this point in the history
- though it's still missing Jest unit tests
  • Loading branch information
ghiscoding committed Apr 26, 2020
1 parent 3e193aa commit 773fb49
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 146 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 */
Expand All @@ -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 */
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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 };
Expand Down
8 changes: 4 additions & 4 deletions packages/common/src/editors/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface EditorComboInput extends Partial<ColumnEditor> {
field: string;

/** Editor Type */
type: 'integer' | 'float' | 'password' | 'text';
type: 'integer' | 'float' | 'number' | 'password' | 'text';
}

export interface ColumnEditorComboInput {
Expand Down
11 changes: 6 additions & 5 deletions packages/common/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand All @@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { EditorValidatorOutput } from './editorValidatorOutput.interface';

export interface OnValidationErrorResult {
validationResults: EditorValidatorOutput;
}
Loading

0 comments on commit 773fb49

Please sign in to comment.