From 8c82e2ae03e5009895cbb33f6819afaa23106c1c Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Tue, 26 Feb 2019 17:08:27 -0500 Subject: [PATCH] feat(editors): add "required" and "alwaysSaveOnEnterKey" options - these 2 new options might not be supported by all Editors (e.g. checkbox doesn't use ENTER key) - the "alwaysSaveOnEnterKey" can be helpful if user want to save value regardless if it's null or not - also fixed integer editor validation, a few validations were missing like minValue/maxValue/... --- .../examples/custom-angularComponentEditor.ts | 19 ++--- src/app/examples/custom-inputEditor.ts | 22 ++--- src/app/examples/grid-editor.component.ts | 5 +- .../modules/angular-slickgrid/constants.ts | 4 + .../editors/autoCompleteEditor.ts | 48 ++++++++--- .../editors/checkboxEditor.ts | 25 ++++-- .../angular-slickgrid/editors/dateEditor.ts | 37 ++++---- .../angular-slickgrid/editors/floatEditor.ts | 82 +++++++++--------- .../editors/integerEditor.ts | 84 +++++++++++++------ .../editors/longTextEditor.ts | 68 +++++++++------ .../angular-slickgrid/editors/selectEditor.ts | 31 +++++-- .../angular-slickgrid/editors/sliderEditor.ts | 27 ++++-- .../angular-slickgrid/editors/textEditor.ts | 47 +++++++---- .../models/columnEditor.interface.ts | 12 +++ 14 files changed, 325 insertions(+), 186 deletions(-) diff --git a/src/app/examples/custom-angularComponentEditor.ts b/src/app/examples/custom-angularComponentEditor.ts index 4909d1207..c0132290c 100644 --- a/src/app/examples/custom-angularComponentEditor.ts +++ b/src/app/examples/custom-angularComponentEditor.ts @@ -3,6 +3,7 @@ import { Subscription } from 'rxjs'; import { AngularUtilService, Column, + ColumnEditor, Editor, EditorValidator, EditorValidatorOutput, @@ -53,7 +54,7 @@ export class CustomAngularComponentEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor || {}; } @@ -94,13 +95,10 @@ export class CustomAngularComponentEditor implements Editor { } save() { - const validation = this.validate(); - if (validation && validation.valid) { - if (this.hasAutoCommitEdit) { - this.args.grid.getEditorLock().commitCurrentEdit(); - } else { - this.args.commitChanges(); - } + if (this.hasAutoCommitEdit) { + this.args.grid.getEditorLock().commitCurrentEdit(); + } else { + this.args.commitChanges(); } } @@ -166,10 +164,7 @@ export class CustomAngularComponentEditor implements Editor { validate(): EditorValidatorOutput { if (this.validator) { const value = this.componentRef.instance.selectedId; - const validationResults = this.validator(value, this.args); - if (!validationResults.valid) { - return validationResults; - } + return this.validator(value, this.args); } // by default the editor is always valid diff --git a/src/app/examples/custom-inputEditor.ts b/src/app/examples/custom-inputEditor.ts index 7422b84b3..237133624 100644 --- a/src/app/examples/custom-inputEditor.ts +++ b/src/app/examples/custom-inputEditor.ts @@ -1,4 +1,4 @@ -import { Column, Editor, EditorValidator, EditorValidatorOutput, KeyCode } from './../modules/angular-slickgrid'; +import { Column, ColumnEditor, Editor, EditorValidator, EditorValidatorOutput, KeyCode } from './../modules/angular-slickgrid'; // using external non-typed js libraries declare var $: any; @@ -8,6 +8,7 @@ declare var $: any; * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. */ export class CustomInputEditor implements Editor { + private _lastInputEvent: KeyboardEvent; $input: any; defaultValue: any; @@ -21,7 +22,7 @@ export class CustomInputEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor || {}; } @@ -39,9 +40,10 @@ export class CustomInputEditor implements Editor { this.$input = $(``) .appendTo(this.args.container) - .on('keydown.nav', (e) => { - if (e.keyCode === KeyCode.LEFT || e.keyCode === KeyCode.RIGHT) { - e.stopImmediatePropagation(); + .on('keydown.nav', (event: KeyboardEvent) => { + this._lastInputEvent = event; + if (event.keyCode === KeyCode.LEFT || event.keyCode === KeyCode.RIGHT) { + event.stopImmediatePropagation(); } }); @@ -88,6 +90,10 @@ export class CustomInputEditor implements Editor { } isValueChanged() { + const lastEvent = this._lastInputEvent && this._lastInputEvent.keyCode; + if (this.columnEditor && this.columnEditor.alwaysSaveOnEnterKey && lastEvent === KeyCode.ENTER) { + return true; + } return (!(this.$input.val() === '' && this.defaultValue === null)) && (this.$input.val() !== this.defaultValue); } @@ -102,11 +108,7 @@ export class CustomInputEditor implements Editor { validate(): EditorValidatorOutput { if (this.validator) { const value = this.$input && this.$input.val && this.$input.val(); - const validationResults = this.validator(value, this.args); - - if (!validationResults.valid) { - return validationResults; - } + return this.validator(value, this.args); } return { diff --git a/src/app/examples/grid-editor.component.ts b/src/app/examples/grid-editor.component.ts index 07e23c136..fb75ac85b 100644 --- a/src/app/examples/grid-editor.component.ts +++ b/src/app/examples/grid-editor.component.ts @@ -145,6 +145,7 @@ export class GridEditorComponent implements OnInit { type: FieldType.string, editor: { model: Editors.longText, + required: true, validator: myCustomTitleValidator, // use a custom validator }, onCellChange: (e: Event, args: OnEventArgs) => { @@ -187,6 +188,7 @@ export class GridEditorComponent implements OnInit { editor: { // default is 0 decimals, if no decimals is passed it will accept 0 or more decimals // however if you pass the "decimalPlaces", it will validate with that maximum + alwaysSaveOnEnterKey: true, // defaults to False, when set to true and user presses ENTER it will always call a Save even if value is empty model: Editors.float, minValue: 0, maxValue: 365, @@ -383,6 +385,7 @@ export class GridEditorComponent implements OnInit { separatorBetweenTextLabels: ' ' }, model: Editors.multipleSelect, + required: true }, filter: { collectionAsync: this.http.get<{ value: string; label: string; }[]>(URL_SAMPLE_COLLECTION_DATA), @@ -511,7 +514,7 @@ export class GridEditorComponent implements OnInit { tempDataset.push({ id: i, title: 'Task ' + i, - duration: Math.round(Math.random() * 100) + '', + duration: (i % 33 === 0) ? null : Math.round(Math.random() * 100) + '', percentComplete: randomPercent, percentCompleteNumber: randomPercent, start: new Date(randomYear, randomMonth, randomDay), diff --git a/src/app/modules/angular-slickgrid/constants.ts b/src/app/modules/angular-slickgrid/constants.ts index 5fd344537..f548a54cd 100644 --- a/src/app/modules/angular-slickgrid/constants.ts +++ b/src/app/modules/angular-slickgrid/constants.ts @@ -17,8 +17,12 @@ export class Constants { static TEXT_SORT_DESCENDING = 'Sort Descending'; static TEXT_TOGGLE_FILTER_ROW = 'Toggle Filter Row'; static TEXT_TOGGLE_PRE_HEADER_ROW = 'Toggle Pre-Header Row'; + static VALIDATION_REQUIRED_FIELD = 'Field is required'; static VALIDATION_EDITOR_VALID_NUMBER = 'Please enter a valid number'; static VALIDATION_EDITOR_VALID_INTEGER = 'Please enter a valid integer number'; + static VALIDATION_EDITOR_INTEGER_BETWEEN = 'Please enter a valid integer number between {{minValue}} and {{maxValue}}'; + static VALIDATION_EDITOR_INTEGER_MAX = 'Please enter a valid integer number that is lower than {{maxValue}}'; + static VALIDATION_EDITOR_INTEGER_MIN = 'Please enter a valid integer number that is greater than {{minValue}}'; static VALIDATION_EDITOR_NUMBER_BETWEEN = 'Please enter a valid number between {{minValue}} and {{maxValue}}'; static VALIDATION_EDITOR_NUMBER_MAX = 'Please enter a valid number that is lower than {{maxValue}}'; static VALIDATION_EDITOR_NUMBER_MIN = 'Please enter a valid number that is greater than {{minValue}}'; diff --git a/src/app/modules/angular-slickgrid/editors/autoCompleteEditor.ts b/src/app/modules/angular-slickgrid/editors/autoCompleteEditor.ts index a95f64e33..5092472cb 100644 --- a/src/app/modules/angular-slickgrid/editors/autoCompleteEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/autoCompleteEditor.ts @@ -1,5 +1,6 @@ import { Column, + ColumnEditor, Editor, EditorValidator, EditorValidatorOutput, @@ -7,6 +8,7 @@ import { CollectionCustomStructure, FieldType } from './../models/index'; +import { Constants } from './../constants'; // using external non-typed js libraries declare var $: any; @@ -18,6 +20,7 @@ declare var $: any; export class AutoCompleteEditor implements Editor { private _currentValue: any; private _defaultTextValue: string; + private _lastInputEvent: KeyboardEvent; $input: any; /** The property name for labels in the collection */ @@ -41,7 +44,7 @@ export class AutoCompleteEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor || {}; } @@ -50,6 +53,10 @@ export class AutoCompleteEditor implements Editor { return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor.customStructure; } + get hasAutoCommitEdit() { + return this.args.grid.getOptions().autoCommitEdit; + } + /** Get the Validator function, can be passed in Editor property or Column Definition */ get validator(): EditorValidator { return this.columnEditor.validator || this.columnDef.validator; @@ -63,9 +70,10 @@ export class AutoCompleteEditor implements Editor { this.$input = $(``) .appendTo(this.args.container) - .on('keydown.nav', (e) => { - if (e.keyCode === KeyCode.LEFT || e.keyCode === KeyCode.RIGHT) { - e.stopImmediatePropagation(); + .on('keydown.nav', (event: KeyboardEvent) => { + this._lastInputEvent = event; + if (event.keyCode === KeyCode.LEFT || event.keyCode === KeyCode.RIGHT) { + event.stopImmediatePropagation(); } }); @@ -124,6 +132,14 @@ export class AutoCompleteEditor implements Editor { this.$input.select(); } + save() { + if (this.hasAutoCommitEdit) { + this.args.grid.getEditorLock().commitCurrentEdit(); + } else { + this.args.commitChanges(); + } + } + serializeValue() { // if user provided a custom structure, we need to reswap the properties // we do this because autocomplete needed label/value pair which might not be what the user provided @@ -141,20 +157,30 @@ export class AutoCompleteEditor implements Editor { } isValueChanged() { + const lastEvent = this._lastInputEvent && this._lastInputEvent.keyCode; + if (this.columnEditor && this.columnEditor.alwaysSaveOnEnterKey && lastEvent === KeyCode.ENTER) { + return true; + } return (!(this.$input.val() === '' && this._defaultTextValue === null)) && (this.$input.val() !== this._defaultTextValue); } validate(): EditorValidatorOutput { + const isRequired = this.columnEditor.required; + const elmValue = this.$input && this.$input.val && this.$input.val(); + const errorMsg = this.columnEditor.errorMessage; + if (this.validator) { - const value = this.$input && this.$input.val && this.$input.val(); - const validationResults = this.validator(value, this.args); - if (!validationResults.valid) { - return validationResults; - } + return this.validator(elmValue, this.args); + } + + // by default the editor is almost always valid (except when it's required but not provided) + if (isRequired && elmValue === '') { + return { + valid: false, + msg: errorMsg || Constants.VALIDATION_REQUIRED_FIELD + }; } - // by default the editor is always valid - // if user want it to be a required checkbox, he would have to provide his own validator return { valid: true, msg: null diff --git a/src/app/modules/angular-slickgrid/editors/checkboxEditor.ts b/src/app/modules/angular-slickgrid/editors/checkboxEditor.ts index c3d21b5cf..094c78800 100644 --- a/src/app/modules/angular-slickgrid/editors/checkboxEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/checkboxEditor.ts @@ -1,4 +1,5 @@ -import { Column, Editor, EditorValidator, EditorValidatorOutput } from './../models/index'; +import { Constants } from './../constants'; +import { Column, ColumnEditor, Editor, EditorValidator, EditorValidatorOutput } from './../models/index'; // using external non-typed js libraries declare var $: any; @@ -21,7 +22,7 @@ export class CheckboxEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor || {}; } @@ -84,16 +85,22 @@ export class CheckboxEditor implements Editor { } validate(): EditorValidatorOutput { + const isRequired = this.columnEditor.required; + const isChecked = this.$input && this.$input.prop && this.$input.prop('checked'); + const errorMsg = this.columnEditor.errorMessage; + if (this.validator) { - const value = this.$input && this.$input.val && this.$input.val(); - const validationResults = this.validator(value, this.args); - if (!validationResults.valid) { - return validationResults; - } + return this.validator(isChecked, this.args); + } + + // by default the editor is almost always valid (except when it's required but not provided) + if (isRequired && !isChecked) { + return { + valid: false, + msg: errorMsg || Constants.VALIDATION_REQUIRED_FIELD + }; } - // by default the editor is always valid - // if user want it to be a required checkbox, he would have to provide his own validator return { valid: true, msg: null diff --git a/src/app/modules/angular-slickgrid/editors/dateEditor.ts b/src/app/modules/angular-slickgrid/editors/dateEditor.ts index 33c89c49e..7ae56a2ce 100644 --- a/src/app/modules/angular-slickgrid/editors/dateEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/dateEditor.ts @@ -1,6 +1,7 @@ -import { mapFlatpickrDateFormatWithFieldType, mapMomentDateFormatWithFieldType } from './../services/utilities'; -import { Column, Editor, EditorValidator, EditorValidatorOutput, FieldType, GridOption } from './../models/index'; import { TranslateService } from '@ngx-translate/core'; +import { Constants } from './../constants'; +import { mapFlatpickrDateFormatWithFieldType, mapMomentDateFormatWithFieldType } from './../services/utilities'; +import { Column, ColumnEditor, Editor, EditorValidator, EditorValidatorOutput, FieldType, GridOption } from './../models/index'; import * as moment_ from 'moment-mini'; const moment = moment_; // patch to fix rollup "moment has no default export" issue, document here https://github.com/rollup/rollup/issues/670 @@ -29,7 +30,7 @@ export class DateEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor || {}; } @@ -115,13 +116,10 @@ export class DateEditor implements Editor { save() { // autocommit will not focus the next editor - const validation = this.validate(); - if (validation && validation.valid) { - if (this.args.grid.getOptions().autoCommitEdit) { - this.args.grid.getEditorLock().commitCurrentEdit(); - } else { - this.args.commitChanges(); - } + if (this.args.grid.getOptions().autoCommitEdit) { + this.args.grid.getEditorLock().commitCurrentEdit(); + } else { + this.args.commitChanges(); } } @@ -161,16 +159,23 @@ export class DateEditor implements Editor { } validate(): EditorValidatorOutput { + const isRequired = this.columnEditor.required; + const elmValue = this.$input && this.$input.val && this.$input.val(); + const errorMsg = this.columnEditor.errorMessage; + if (this.validator) { const value = this.$input && this.$input.val && this.$input.val(); - const validationResults = this.validator(value, this.args); - if (!validationResults.valid) { - return validationResults; - } + return this.validator(value, this.args); + } + + // by default the editor is almost always valid (except when it's required but not provided) + if (isRequired && elmValue === '') { + return { + valid: false, + msg: errorMsg || Constants.VALIDATION_REQUIRED_FIELD + }; } - // by default the editor is always valid - // if user want it to be a required checkbox, he would have to provide his own validator return { valid: true, msg: null diff --git a/src/app/modules/angular-slickgrid/editors/floatEditor.ts b/src/app/modules/angular-slickgrid/editors/floatEditor.ts index 57a656a88..01953e654 100644 --- a/src/app/modules/angular-slickgrid/editors/floatEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/floatEditor.ts @@ -1,5 +1,5 @@ import { Constants } from '../constants'; -import { Column, Editor, EditorValidator, EditorValidatorOutput, KeyCode } from './../models/index'; +import { Column, ColumnEditor, Editor, EditorValidator, EditorValidatorOutput, KeyCode } from './../models/index'; // using external non-typed js libraries declare var $: any; @@ -11,6 +11,7 @@ const defaultDecimalPlaces = 0; * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. */ export class FloatEditor implements Editor { + private _lastInputEvent: KeyboardEvent; $input: any; defaultValue: any; @@ -24,7 +25,7 @@ export class FloatEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor || {}; } @@ -43,9 +44,13 @@ export class FloatEditor implements Editor { this.$input = $(``) .appendTo(this.args.container) - .on('keydown.nav', (e) => { - if (e.keyCode === KeyCode.LEFT || e.keyCode === KeyCode.RIGHT) { - e.stopImmediatePropagation(); + .on('keydown.nav', (event: KeyboardEvent) => { + this._lastInputEvent = event; + if (event.keyCode === KeyCode.LEFT || event.keyCode === KeyCode.RIGHT) { + event.stopImmediatePropagation(); + } else if (event.keyCode === KeyCode.ENTER) { + event.stopImmediatePropagation(); + this.save(); } }); @@ -111,7 +116,12 @@ export class FloatEditor implements Editor { } serializeValue() { - let rtn = parseFloat(this.$input.val()) || 0; + const elmValue = this.$input.val(); + if (elmValue === '' || isNaN(elmValue)) { + return elmValue; + } + + let rtn = parseFloat(elmValue); const decPlaces = this.getDecimalPlaces(); if (decPlaces !== null && (rtn || rtn === 0) @@ -128,17 +138,18 @@ export class FloatEditor implements Editor { isValueChanged() { const elmValue = this.$input.val(); + const lastEvent = this._lastInputEvent && this._lastInputEvent.keyCode; + if (this.columnEditor && this.columnEditor.alwaysSaveOnEnterKey && lastEvent === KeyCode.ENTER) { + return true; + } return (!(elmValue === '' && this.defaultValue === null)) && (elmValue !== this.defaultValue); } save() { - const validation = this.validate(); - if (validation && validation.valid) { - if (this.hasAutoCommitEdit) { - this.args.grid.getEditorLock().commitCurrentEdit(); - } else { - this.args.commitChanges(); - } + if (this.hasAutoCommitEdit) { + this.args.grid.getEditorLock().commitCurrentEdit(); + } else { + this.args.commitChanges(); } } @@ -146,6 +157,7 @@ export class FloatEditor implements Editor { const elmValue = this.$input.val(); const floatNumber = !isNaN(elmValue as number) ? parseFloat(elmValue) : null; const decPlaces = this.getDecimalPlaces(); + const isRequired = this.columnEditor.required; const minValue = this.columnEditor.minValue; const maxValue = this.columnEditor.maxValue; const errorMsg = this.columnEditor.errorMessage; @@ -155,54 +167,46 @@ export class FloatEditor implements Editor { '{{minDecimal}}': 0, '{{maxDecimal}}': decPlaces }; + let isValid = true; + let outputMsg = ''; if (this.validator) { - const validationResults = this.validator(elmValue, this.args); - if (!validationResults.valid) { - return validationResults; - } + return this.validator(elmValue, this.args); + } else if (isRequired && elmValue === '') { + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_REQUIRED_FIELD; } else if (isNaN(elmValue as number) || (decPlaces === 0 && !/^[-+]?(\d+(\.)?(\d)*)$/.test(elmValue))) { // when decimal value is 0 (which is the default), we accept 0 or more decimal values - return { - valid: false, - msg: errorMsg || Constants.VALIDATION_EDITOR_VALID_NUMBER - }; + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_EDITOR_VALID_NUMBER; } else if (minValue !== undefined && maxValue !== undefined && floatNumber !== null && (floatNumber < minValue || floatNumber > maxValue)) { // MIN & MAX Values provided // when decimal value is bigger than 0, we only accept the decimal values as that value set // for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals - return { - valid: false, - msg: errorMsg || Constants.VALIDATION_EDITOR_NUMBER_BETWEEN.replace(/{{minValue}}|{{maxValue}}/gi, (matched) => mapValidation[matched]) - }; + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_EDITOR_NUMBER_BETWEEN.replace(/{{minValue}}|{{maxValue}}/gi, (matched) => mapValidation[matched]); } else if (minValue !== undefined && floatNumber !== null && floatNumber <= minValue) { // MIN VALUE ONLY // when decimal value is bigger than 0, we only accept the decimal values as that value set // for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals - return { - valid: false, - msg: errorMsg || Constants.VALIDATION_EDITOR_NUMBER_MIN.replace(/{{minValue}}/gi, (matched) => mapValidation[matched]) - }; + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_EDITOR_NUMBER_MIN.replace(/{{minValue}}/gi, (matched) => mapValidation[matched]); } else if (maxValue !== undefined && floatNumber !== null && floatNumber >= maxValue) { // MAX VALUE ONLY // when decimal value is bigger than 0, we only accept the decimal values as that value set // for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals - return { - valid: false, - msg: errorMsg || Constants.VALIDATION_EDITOR_NUMBER_MAX.replace(/{{maxValue}}/gi, (matched) => mapValidation[matched]) - }; + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_EDITOR_NUMBER_MAX.replace(/{{maxValue}}/gi, (matched) => mapValidation[matched]); } else if ((decPlaces > 0 && !new RegExp(`^(\\d*(\\.)?(\\d){0,${decPlaces}})$`).test(elmValue))) { // when decimal value is bigger than 0, we only accept the decimal values as that value set // for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals - return { - valid: false, - msg: errorMsg || Constants.VALIDATION_EDITOR_DECIMAL_BETWEEN.replace(/{{minDecimal}}|{{maxDecimal}}/gi, (matched) => mapValidation[matched]) - }; + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_EDITOR_DECIMAL_BETWEEN.replace(/{{minDecimal}}|{{maxDecimal}}/gi, (matched) => mapValidation[matched]); } return { - valid: true, - msg: null + valid: isValid, + msg: outputMsg }; } } diff --git a/src/app/modules/angular-slickgrid/editors/integerEditor.ts b/src/app/modules/angular-slickgrid/editors/integerEditor.ts index d5511963f..d4e66fdd3 100644 --- a/src/app/modules/angular-slickgrid/editors/integerEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/integerEditor.ts @@ -1,5 +1,5 @@ import { Constants } from './../constants'; -import { Column, Editor, EditorValidator, EditorValidatorOutput, KeyCode } from './../models/index'; +import { Column, ColumnEditor, Editor, EditorValidator, EditorValidatorOutput, KeyCode } from './../models/index'; // using external non-typed js libraries declare var $: any; @@ -9,6 +9,7 @@ declare var $: any; * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. */ export class IntegerEditor implements Editor { + private _lastInputEvent: KeyboardEvent; $input: any; defaultValue: any; @@ -22,7 +23,7 @@ export class IntegerEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor || {}; } @@ -41,9 +42,10 @@ export class IntegerEditor implements Editor { this.$input = $(``) .appendTo(this.args.container) - .on('keydown.nav', (e) => { - if (e.keyCode === KeyCode.LEFT || e.keyCode === KeyCode.RIGHT) { - e.stopImmediatePropagation(); + .on('keydown.nav', (event: KeyboardEvent) => { + this._lastInputEvent = event; + if (event.keyCode === KeyCode.LEFT || event.keyCode === KeyCode.RIGHT) { + event.stopImmediatePropagation(); } }); @@ -78,7 +80,11 @@ export class IntegerEditor implements Editor { } serializeValue() { - return parseInt(this.$input.val() as string, 10) || 0; + const elmValue = this.$input.val(); + if (elmValue === '' || isNaN(elmValue)) { + return elmValue; + } + return parseInt(elmValue, 10) || 0; } applyValue(item: any, state: any) { @@ -88,39 +94,67 @@ export class IntegerEditor implements Editor { isValueChanged() { const elmValue = this.$input.val(); const value = isNaN(elmValue) ? elmValue : parseInt(elmValue, 10); - return (!(value === '' && this.defaultValue === null)) && (value !== this.defaultValue); + const lastEvent = this._lastInputEvent && this._lastInputEvent.keyCode; + + if (this.columnEditor && this.columnEditor.alwaysSaveOnEnterKey && lastEvent === KeyCode.ENTER) { + return true; + } + return (!(value === '' && this.defaultValue === null && lastEvent !== KeyCode.ENTER)) && (value !== this.defaultValue); } save() { - const validation = this.validate(); - if (validation && validation.valid) { - if (this.hasAutoCommitEdit) { - this.args.grid.getEditorLock().commitCurrentEdit(); - } else { - this.args.commitChanges(); - } + if (this.hasAutoCommitEdit) { + this.args.grid.getEditorLock().commitCurrentEdit(); + } else { + this.args.commitChanges(); } } validate(): EditorValidatorOutput { const elmValue = this.$input.val(); - const errorMsg = this.columnEditor.params && this.columnEditor.errorMessage; + const intNumber = !isNaN(elmValue as number) ? parseInt(elmValue, 10) : null; + const errorMsg = this.columnEditor.errorMessage; + const isRequired = this.columnEditor.required; + const minValue = this.columnEditor.minValue; + const maxValue = this.columnEditor.maxValue; + const mapValidation = { + '{{minValue}}': minValue, + '{{maxValue}}': maxValue + }; + let isValid = true; + let outputMsg = ''; if (this.validator) { - const validationResults = this.validator(elmValue, this.args); - if (!validationResults.valid) { - return validationResults; - } + return this.validator(elmValue, this.args); + } else if (isRequired && elmValue === '') { + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_REQUIRED_FIELD; } else if (isNaN(elmValue as number) || !/^[+-]?\d+$/.test(elmValue)) { - return { - valid: false, - msg: errorMsg || Constants.VALIDATION_EDITOR_VALID_INTEGER - }; + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_EDITOR_VALID_INTEGER; + } else if (minValue !== undefined && maxValue !== undefined && intNumber !== null && (intNumber < minValue || intNumber > maxValue)) { + // MIN & MAX Values provided + // when decimal value is bigger than 0, we only accept the decimal values as that value set + // for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_EDITOR_INTEGER_BETWEEN.replace(/{{minValue}}|{{maxValue}}/gi, (matched) => mapValidation[matched]); + } else if (minValue !== undefined && intNumber !== null && intNumber <= minValue) { + // MIN VALUE ONLY + // when decimal value is bigger than 0, we only accept the decimal values as that value set + // for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_EDITOR_INTEGER_MIN.replace(/{{minValue}}/gi, (matched) => mapValidation[matched]); + } else if (maxValue !== undefined && intNumber !== null && intNumber >= maxValue) { + // MAX VALUE ONLY + // when decimal value is bigger than 0, we only accept the decimal values as that value set + // for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals + isValid = false; + outputMsg = errorMsg || Constants.VALIDATION_EDITOR_INTEGER_MAX.replace(/{{maxValue}}/gi, (matched) => mapValidation[matched]); } return { - valid: true, - msg: null + valid: isValid, + msg: outputMsg }; } } diff --git a/src/app/modules/angular-slickgrid/editors/longTextEditor.ts b/src/app/modules/angular-slickgrid/editors/longTextEditor.ts index d7a59c8bc..b63222eb4 100644 --- a/src/app/modules/angular-slickgrid/editors/longTextEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/longTextEditor.ts @@ -2,6 +2,7 @@ import { TranslateService } from '@ngx-translate/core'; import { Constants } from './../constants'; import { Column, + ColumnEditor, Editor, EditorValidator, EditorValidatorOutput, @@ -43,7 +44,7 @@ export class LongTextEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor || {}; } @@ -85,36 +86,25 @@ export class LongTextEditor implements Editor { this.$textarea.focus().select(); } - handleKeyDown(e: any) { - if (e.which === KeyCode.ENTER && e.ctrlKey) { + handleKeyDown(event: KeyboardEvent) { + if (event.which === KeyCode.ENTER && event.ctrlKey) { this.save(); - } else if (e.which === KeyCode.ESCAPE) { - e.preventDefault(); + } else if (event.which === KeyCode.ESCAPE) { + event.preventDefault(); this.cancel(); - } else if (e.which === KeyCode.TAB && e.shiftKey) { - e.preventDefault(); + } else if (event.which === KeyCode.TAB && event.shiftKey) { + event.preventDefault(); if (this.args && this.args.grid) { this.args.grid.navigatePrev(); } - } else if (e.which === KeyCode.TAB) { - e.preventDefault(); + } else if (event.which === KeyCode.TAB) { + event.preventDefault(); if (this.args && this.args.grid) { this.args.grid.navigateNext(); } } } - save() { - const validation = this.validate(); - if (validation && validation.valid) { - if (this.hasAutoCommitEdit) { - this.args.grid.getEditorLock().commitCurrentEdit(); - } else { - this.args.commitChanges(); - } - } - } - cancel() { this.$textarea.val(this.defaultValue); if (this.args && this.args.cancelChanges) { @@ -144,6 +134,14 @@ export class LongTextEditor implements Editor { this.$textarea.focus(); } + getValue() { + return this.$textarea.val(); + } + + setValue(val: string) { + this.$textarea.val(val); + } + getColumnEditor() { return this.args && this.args.column && this.args.column.internalColumnEditor && this.args.column.internalColumnEditor; } @@ -161,21 +159,37 @@ export class LongTextEditor implements Editor { item[this.columnDef.field] = state; } + isValueChanged() { - return (!(this.$textarea.val() === '' && this.defaultValue == null)) && (this.$textarea.val() !== this.defaultValue); + return (!(this.$textarea.val() === '' && this.defaultValue === null)) && (this.$textarea.val() !== this.defaultValue); + } + + save() { + if (this.hasAutoCommitEdit) { + this.args.grid.getEditorLock().commitCurrentEdit(); + } else { + this.args.commitChanges(); + } } validate(): EditorValidatorOutput { + const isRequired = this.columnEditor.required; + const elmValue = this.$textarea && this.$textarea.val && this.$textarea.val(); + const errorMsg = this.columnEditor.errorMessage; + if (this.validator) { const value = this.$textarea && this.$textarea.val && this.$textarea.val(); - const validationResults = this.validator(value, this.args); - if (!validationResults.valid) { - return validationResults; - } + return this.validator(value, this.args); + } + + // by default the editor is almost always valid (except when it's required but not provided) + if (isRequired && elmValue === '') { + return { + valid: false, + msg: errorMsg || Constants.VALIDATION_REQUIRED_FIELD + }; } - // by default the editor is always valid - // if user want it to be a required checkbox, he would have to provide his own validator return { valid: true, msg: null diff --git a/src/app/modules/angular-slickgrid/editors/selectEditor.ts b/src/app/modules/angular-slickgrid/editors/selectEditor.ts index d0c5dee56..4edd23b55 100644 --- a/src/app/modules/angular-slickgrid/editors/selectEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/selectEditor.ts @@ -1,8 +1,10 @@ import { TranslateService } from '@ngx-translate/core'; +import { Constants } from '../constants'; import { CollectionCustomStructure, CollectionOption, Column, + ColumnEditor, Editor, EditorValidator, EditorValidatorOutput, @@ -97,7 +99,7 @@ export class SelectEditor implements Editor { }, onBlur: () => this.destroy(), onClose: () => { - if (!this._destroying && args.grid.getOptions().autoCommitEdit) { + if (!this._destroying && this.hasAutoCommitEdit) { // do not use args.commitChanges() as this sets the focus to the next // row. Also the select list will stay shown when clicking off the grid args.grid.getEditorLock().commitCurrentEdit(); @@ -140,7 +142,7 @@ export class SelectEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor || {}; } @@ -149,6 +151,10 @@ export class SelectEditor implements Editor { return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor.customStructure; } + get hasAutoCommitEdit() { + return this.args.grid.getOptions().autoCommitEdit; + } + /** * The current selected values (multiple select) from the collection */ @@ -313,16 +319,23 @@ export class SelectEditor implements Editor { } validate(): EditorValidatorOutput { + const isRequired = this.columnEditor.required; + const elmValue = this.$editorElm && this.$editorElm.val && this.$editorElm.val(); + const errorMsg = this.columnEditor.errorMessage; + if (this.validator) { const value = this.isMultipleSelect ? this.currentValues : this.currentValue; - const validationResults = this.validator(value, this.args); - if (!validationResults.valid) { - return validationResults; - } + return this.validator(value, this.args); + } + + // by default the editor is almost always valid (except when it's required but not provided) + if (isRequired && (elmValue === '' || (Array.isArray(elmValue) && elmValue.length === 0))) { + return { + valid: false, + msg: errorMsg || Constants.VALIDATION_REQUIRED_FIELD + }; } - // by default the editor is always valid - // if user want it to be a required checkbox, he would have to provide his own validator return { valid: true, msg: null @@ -344,7 +357,7 @@ export class SelectEditor implements Editor { // user might want to filter certain items of the collection if (this.columnEditor && this.columnEditor.collectionFilterBy) { const filterBy = this.columnEditor.collectionFilterBy; - const filterCollectionBy = this.columnEditor.collectionOptions && this.columnEditor.collectionOptions.filterAfterEachPass || null; + const filterCollectionBy = this.columnEditor.collectionOptions && this.columnEditor.collectionOptions.filterResultAfterEachPass || null; outputCollection = this._collectionService.filterCollection(outputCollection, filterBy, filterCollectionBy); } diff --git a/src/app/modules/angular-slickgrid/editors/sliderEditor.ts b/src/app/modules/angular-slickgrid/editors/sliderEditor.ts index 7aaa5b70e..4932d2e10 100644 --- a/src/app/modules/angular-slickgrid/editors/sliderEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/sliderEditor.ts @@ -1,5 +1,5 @@ import { Constants } from '../constants'; -import { Column, Editor, EditorValidator, EditorValidatorOutput } from './../models/index'; +import { Column, Editor, EditorValidator, EditorValidatorOutput, KeyCode, ColumnEditor } from './../models/index'; // using external non-typed js libraries declare var $: any; @@ -9,6 +9,7 @@ const DEFAULT_MAX_VALUE = 100; const DEFAULT_STEP = 1; export class SliderEditor implements Editor { + private _lastInputEvent: KeyboardEvent; private _elementRangeInputId: string; private _elementRangeOutputId: string; $editorElm: any; @@ -26,7 +27,7 @@ export class SliderEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor || {}; } @@ -62,10 +63,11 @@ export class SliderEditor implements Editor { // if user chose to display the slider number on the right side, then update it every time it changes // we need to use both "input" and "change" event to be all cross-browser if (!this.editorParams.hideSliderNumber) { - this.$editorElm.on('input change', (e: { target: HTMLInputElement }) => { - const value = e && e.target && e.target.value || ''; + this.$editorElm.on('input change', (event: KeyboardEvent & { target: HTMLInputElement }) => { + this._lastInputEvent = event; + const value = event && event.target && event.target.value || ''; if (value) { - document.getElementById(this._elementRangeOutputId).innerHTML = e.target.value; + document.getElementById(this._elementRangeOutputId).innerHTML = event.target.value; } }); } @@ -110,11 +112,16 @@ export class SliderEditor implements Editor { isValueChanged() { const elmValue = this.$input.val(); + const lastEvent = this._lastInputEvent && this._lastInputEvent.keyCode; + if (this.columnEditor && this.columnEditor.alwaysSaveOnEnterKey && lastEvent === KeyCode.ENTER) { + return true; + } return (!(elmValue === '' && this.defaultValue === null)) && (elmValue !== this.defaultValue); } validate(): EditorValidatorOutput { const elmValue = this.$input.val(); + const isRequired = this.columnEditor.required; const minValue = this.columnEditor.minValue; const maxValue = this.columnEditor.maxValue; const errorMsg = this.columnEditor.errorMessage; @@ -124,10 +131,12 @@ export class SliderEditor implements Editor { }; if (this.validator) { - const validationResults = this.validator(elmValue, this.args); - if (!validationResults.valid) { - return validationResults; - } + return this.validator(elmValue, this.args); + } else if (isRequired && elmValue === '') { + return { + valid: false, + msg: errorMsg || Constants.VALIDATION_REQUIRED_FIELD + }; } else if (minValue !== undefined && (elmValue < minValue || elmValue > maxValue)) { // when decimal value is bigger than 0, we only accept the decimal values as that value set // for example if we set decimalPlaces to 2, we will only accept numbers between 0 and 2 decimals diff --git a/src/app/modules/angular-slickgrid/editors/textEditor.ts b/src/app/modules/angular-slickgrid/editors/textEditor.ts index 9ba245b8a..9d2959042 100644 --- a/src/app/modules/angular-slickgrid/editors/textEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/textEditor.ts @@ -1,4 +1,5 @@ -import { Column, Editor, EditorValidator, EditorValidatorOutput, KeyCode } from './../models/index'; +import { Constants } from '../constants'; +import { Column, ColumnEditor, Editor, EditorValidator, EditorValidatorOutput, KeyCode } from './../models/index'; // using external non-typed js libraries declare var $: any; @@ -8,6 +9,7 @@ declare var $: any; * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. */ export class TextEditor implements Editor { + private _lastInputEvent: KeyboardEvent; $input: any; defaultValue: any; @@ -21,7 +23,7 @@ export class TextEditor implements Editor { } /** Get Column Editor object */ - get columnEditor(): any { + get columnEditor(): ColumnEditor { return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor || {}; } @@ -40,9 +42,10 @@ export class TextEditor implements Editor { this.$input = $(``) .appendTo(this.args.container) - .on('keydown.nav', (e) => { - if (e.keyCode === KeyCode.LEFT || e.keyCode === KeyCode.RIGHT) { - e.stopImmediatePropagation(); + .on('keydown.nav', (event: KeyboardEvent) => { + this._lastInputEvent = event; + if (event.keyCode === KeyCode.LEFT || event.keyCode === KeyCode.RIGHT) { + event.stopImmediatePropagation(); } }); @@ -89,31 +92,39 @@ export class TextEditor implements Editor { } isValueChanged() { + const lastEvent = this._lastInputEvent && this._lastInputEvent.keyCode; + if (this.columnEditor && this.columnEditor.alwaysSaveOnEnterKey && lastEvent === KeyCode.ENTER) { + return true; + } return (!(this.$input.val() === '' && this.defaultValue === null)) && (this.$input.val() !== this.defaultValue); } save() { - const validation = this.validate(); - if (validation && validation.valid) { - if (this.hasAutoCommitEdit) { - this.args.grid.getEditorLock().commitCurrentEdit(); - } else { - this.args.commitChanges(); - } + if (this.hasAutoCommitEdit) { + this.args.grid.getEditorLock().commitCurrentEdit(); + } else { + this.args.commitChanges(); } } validate(): EditorValidatorOutput { + const isRequired = this.columnEditor.required; + const elmValue = this.$input && this.$input.val && this.$input.val(); + const errorMsg = this.columnEditor.errorMessage; + if (this.validator) { const value = this.$input && this.$input.val && this.$input.val(); - const validationResults = this.validator(value, this.args); - if (!validationResults.valid) { - return validationResults; - } + return this.validator(value, this.args); + } + + // by default the editor is almost always valid (except when it's required but not provided) + if (isRequired && elmValue === '') { + return { + valid: false, + msg: errorMsg || Constants.VALIDATION_REQUIRED_FIELD + }; } - // by default the editor is always valid - // if user want it to be a required checkbox, he would have to provide his own validator return { valid: true, msg: null diff --git a/src/app/modules/angular-slickgrid/models/columnEditor.interface.ts b/src/app/modules/angular-slickgrid/models/columnEditor.interface.ts index eeaf7eb7c..dbc4b9575 100644 --- a/src/app/modules/angular-slickgrid/models/columnEditor.interface.ts +++ b/src/app/modules/angular-slickgrid/models/columnEditor.interface.ts @@ -9,6 +9,12 @@ import { import { Observable } from 'rxjs'; export interface ColumnEditor { + /** + * Defaults to false, when set to True and user presses the ENTER key (on Editors that supports it), + * it will always call a Save regardless if the current value is null and/or previous value was null + */ + alwaysSaveOnEnterKey?: boolean; + /** A collection of items/options that will be loaded asynchronously (commonly used with a Select/Multi-Select Editor) */ collectionAsync?: Promise | Observable; @@ -60,6 +66,12 @@ export interface ColumnEditor { */ placeholder?: string; + /** + * Defaults to false, is the field required to be valid? + * Only on Editors that supports it. + */ + required?: boolean; + /** Editor Validator */ validator?: EditorValidator;