| /* |
| * Copyright (c) 2014-2018 BSI Business Systems Integration AG. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * BSI Business Systems Integration AG - initial API and implementation |
| */ |
| import {arrays, Event, focusUtils, FormField, menus as menus_1, objects, scout, Status, strings, ParsingFailedStatus, ValidationFailedStatus} from '../../index'; |
| import * as $ from 'jquery'; |
| |
| /** |
| * @abstract |
| */ |
| export default class ValueField extends FormField { |
| |
| constructor() { |
| super(); |
| this.clearable = ValueField.Clearable.FOCUSED; |
| this.displayText = null; |
| this.formatter = this._formatValue.bind(this); |
| this.hasText = false; |
| this.initialValue = null; |
| this.invalidValueMessageKey = 'InvalidValueMessageX'; |
| this.parser = this._parseValue.bind(this); |
| this.value = null; |
| this.validators = []; |
| this.validators.push(this._validateValue.bind(this)); |
| |
| this.$clearIcon = null; |
| |
| this._addCloneProperties(['value', 'displayText', 'clearable']); |
| } |
| |
| static Clearable = { |
| /** |
| * The clear icon is showed when the field has text. |
| */ |
| ALWAYS: 'always', |
| /** |
| * The clear icon will be showed when the field is focused and has text. |
| */ |
| FOCUSED: 'focused', |
| /** |
| * Never show the clear icon. |
| */ |
| NEVER: 'never' |
| }; |
| |
| _init(model) { |
| super._init(model); |
| if (this.validator) { |
| // Validators are kept in a list, allow a single validator to be set in the model, similar to parser and formatter. |
| // setValidator will add the new validator to this.validators and remove the other ones. |
| this.setValidator(this.validator); |
| delete this.validator; |
| } |
| this._initValue(this.value); |
| } |
| |
| /** |
| * Override this method if you need to influence the value initialization (e.g. do something before the value is initially set) |
| */ |
| _initValue(value) { |
| // Delete value first, value may be invalid and must not be set |
| this.value = null; |
| this._setValue(value); |
| this._updateEmpty(); |
| } |
| |
| _renderProperties() { |
| super._renderProperties(); |
| this._renderDisplayText(); |
| this._renderClearable(); |
| this._renderHasText(); |
| } |
| |
| _remove() { |
| super._remove(); |
| this.$clearIcon = null; |
| } |
| |
| /** |
| * The default impl. is a NOP, because not every ValueField has a sensible display text. |
| */ |
| _renderDisplayText() { |
| this._updateHasText(); |
| } |
| |
| /** |
| * The default impl. returns an empty string, because not every ValueField has a sensible display text. |
| */ |
| _readDisplayText() { |
| return ''; |
| } |
| |
| _onClearIconMouseDown(event) { |
| this.clear(); |
| event.preventDefault(); |
| } |
| |
| _onFieldBlur() { |
| super._onFieldBlur(); |
| this.acceptInput(false); |
| } |
| |
| /** |
| * Accepts the current input and writes it to the model. |
| * <p> |
| * This method is typically called by the _onBlur() function of the field, but may actually be called from anywhere (e.g. button, actions, cell editor, etc). |
| * It is also called by the _aboutToBlurByMouseDown() function, which is required because our Ok- and Cancel-buttons are not focusable (thus _onBlur() is |
| * never called) but changes in the value-field must be sent to the server anyway when a button is clicked. |
| * <p> |
| * The default reads the display text using this._readDisplayText() and writes it to the model by calling _triggerAcceptInput(). |
| * If subclasses don't have a display-text or want to write another state to the server, they may override this method. |
| */ |
| acceptInput(whileTyping) { |
| whileTyping = !!whileTyping; // cast to boolean |
| var displayText = scout.nvl(this._readDisplayText(), ''); |
| |
| // trigger only if displayText has really changed |
| if (this._checkDisplayTextChanged(displayText, whileTyping)) { |
| // Don't call setDisplayText() to prevent re-rendering of display text (which is unnecessary and |
| // might change the cursor position). Don't call _callSetProperty() as well, as this eventually |
| // executes this._setDisplayText(), which updates the value. |
| this._setProperty('displayText', displayText); |
| if (!whileTyping) { |
| this.parseAndSetValue(displayText); |
| } |
| // Display text may be formatted -> Use this.displayText |
| this._triggerAcceptInput(whileTyping); |
| } |
| } |
| |
| parseAndSetValue(displayText) { |
| this.removeErrorStatus(ParsingFailedStatus); |
| try { |
| var event = new Event({ |
| displayText: displayText |
| }); |
| this.trigger('parse', event); |
| if (!event.defaultPrevented) { |
| var parsedValue = this.parseValue(displayText); |
| this.setValue(parsedValue); |
| } |
| } catch (error) { |
| this._parsingFailed(displayText, error); |
| } |
| } |
| |
| _parsingFailed(displayText, error) { |
| $.log.isDebugEnabled() && $.log.debug('Parsing failed for field with id ' + this.id, error); |
| var event = new Event({ |
| displayText: displayText, |
| error: error |
| }); |
| this.trigger('parseError', event); |
| if (!event.defaultPrevented) { |
| this._addParsingFailedErrorStatus(displayText, error); |
| } |
| } |
| |
| _addParsingFailedErrorStatus(displayText, error) { |
| var status = this._createParsingFailedStatus(displayText, error); |
| this.addErrorStatus(status); |
| } |
| |
| _createParsingFailedStatus(displayText, error) { |
| return this._createInvalidValueStatus('ParsingFailedStatus', displayText, error); |
| } |
| |
| /** |
| * Replaces the existing parser. The parser is called during {@link #parseValue(displayText)}. |
| * <p> |
| * Remember calling the default parser passed as parameter to the parse function, if needed. |
| * @param {function} parser the new parser. If null, the default parser is used. |
| */ |
| setParser(parser) { |
| this.setProperty('parser', parser); |
| if (this.initialized) { |
| this.parseAndSetValue(this.displayText); |
| } |
| } |
| |
| _setParser(parser) { |
| if (!parser) { |
| parser = this._parseValue.bind(this); |
| } |
| this._setProperty('parser', parser); |
| } |
| |
| /** |
| * @returns {*} the parsed value |
| * @throws a message, a Status or an error if the parsing fails |
| */ |
| parseValue(displayText) { |
| var defaultParser = this._parseValue.bind(this); |
| return this.parser(displayText, defaultParser); |
| } |
| |
| /** |
| * @throws a message, a Status or an error if the parsing fails |
| */ |
| _parseValue(displayText) { |
| return displayText; |
| } |
| |
| _checkDisplayTextChanged(displayText, whileTyping) { |
| var oldDisplayText = scout.nvl(this.displayText, ''); |
| return displayText !== oldDisplayText; |
| } |
| |
| /** |
| * Method invoked upon a mousedown click with this field as the currently focused control, and is invoked just before the mousedown click will be interpreted. |
| * However, the mousedown target must not be this control, but any other control instead. |
| * |
| * The default implementation checks, whether the click occurred outside this control, and if so invokes 'ValueField.acceptInput'. |
| * |
| * @param target |
| * the DOM target where the mouse down event occurred. |
| */ |
| aboutToBlurByMouseDown(target) { |
| var eventOnField = this.isFocusOnField(target); |
| if (!eventOnField) { |
| this.acceptInput(); // event outside this value field. |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| isFocused() { |
| return this.rendered && focusUtils.isActiveElement(this.$field); |
| } |
| |
| isFocusOnField(target) { |
| return this.$field.isOrHas(target) || (this.$clearIcon && this.$clearIcon.isOrHas(target)); |
| } |
| |
| _triggerAcceptInput(whileTyping) { |
| var event = { |
| displayText: this.displayText, |
| whileTyping: !!whileTyping |
| }; |
| this.trigger('acceptInput', event); |
| } |
| |
| setDisplayText(displayText) { |
| this.setProperty('displayText', displayText); |
| } |
| |
| _updateHasText() { |
| this.setHasText(strings.hasText(this._readDisplayText())); |
| } |
| |
| setHasText(hasText) { |
| this.setProperty('hasText', hasText); |
| } |
| |
| _renderHasText() { |
| if (this.$field) { |
| this.$field.toggleClass('has-text', this.hasText); |
| } |
| this.$container.toggleClass('has-text', this.hasText); |
| } |
| |
| setClearable(clearableStyle) { |
| this.setProperty('clearable', clearableStyle); |
| } |
| |
| _renderClearable() { |
| if (this.isClearable()) { |
| if (!this.$clearIcon) { |
| this.addClearIcon(); |
| } |
| } else { |
| if (this.$clearIcon) { |
| // Remove $dateField |
| this.$clearIcon.remove(); |
| this.$clearIcon = null; |
| } |
| } |
| this.invalidateLayoutTree(false); |
| this._updateClearableStyles(); |
| } |
| |
| _updateClearableStyles() { |
| this.$container.removeClass('clearable-always clearable-focused'); |
| if (this.isClearable()) { |
| if (this.clearable === ValueField.Clearable.ALWAYS) { |
| this.$container.addClass('clearable-always'); |
| } else if (this.clearable === ValueField.Clearable.FOCUSED) { |
| this.$container.addClass('clearable-focused'); |
| } |
| } |
| } |
| |
| isClearable() { |
| return this.clearable === ValueField.Clearable.ALWAYS || this.clearable === ValueField.Clearable.FOCUSED; |
| } |
| |
| /** |
| * Clears the display text and the value to null. |
| */ |
| clear() { |
| this._clear(); |
| this._updateHasText(); |
| this.acceptInput(); |
| this._triggerClear(); |
| } |
| |
| _clear() { |
| // to be implemented by sublcasses |
| } |
| |
| _triggerClear() { |
| this.trigger('clear'); |
| } |
| |
| setValue(value) { |
| // Same code as in Widget#setProperty expect for the equals check |
| // -> _setValue has to be called even if the value is equal so that update display text will be executed |
| value = this._prepareProperty('value', value); |
| if (this.rendered) { |
| this._callRemoveProperty('value'); |
| } |
| this._callSetProperty('value', value); |
| if (this.rendered) { |
| this._callRenderProperty('value'); |
| } |
| } |
| |
| /** |
| * Resets the value to its initial value. |
| */ |
| resetValue() { |
| this.setValue(this.initialValue); |
| } |
| |
| /** |
| * Default does nothing because the value field does not know which type the concrete field uses. |
| * May be overridden to cast the value to the required type. |
| * @returns {*} the value with the correct type. |
| */ |
| _ensureValue(value) { |
| return value; |
| } |
| |
| _setValue(value) { |
| // When widget is initialized with a given errorStatus and a value -> don't remove the error |
| // status. This is a typical case for Scout Classic: field has a ParsingFailedError and user |
| // hits reload. |
| if (this.initialized) { |
| this.removeErrorStatus(ParsingFailedStatus); |
| this.removeErrorStatus(ValidationFailedStatus); |
| } |
| var oldValue = this.value; |
| var typedValue = null; |
| try { |
| typedValue = this._ensureValue(value); |
| this.value = this.validateValue(typedValue); |
| } catch (error) { |
| typedValue = typedValue || value; |
| this._validationFailed(typedValue, error); |
| return; |
| } |
| |
| this._updateDisplayText(); |
| if (this._valueEquals(oldValue, this.value)) { |
| return; |
| } |
| |
| this._valueChanged(); |
| this._updateTouched(); |
| this._updateEmpty(); |
| this.triggerPropertyChange('value', oldValue, this.value); |
| } |
| |
| _valueEquals(valueA, valueB) { |
| return objects.equals(valueA, valueB); |
| } |
| |
| /** |
| * Is called after a value is changed. May be implemented by subclasses. The default does nothing. |
| */ |
| _valueChanged() { |
| // NOP |
| } |
| |
| /** |
| * Validates the value by executing the validators. If a new value is the result, it will be set. |
| */ |
| validate() { |
| this._setValue(this.value); |
| } |
| |
| /** |
| * @param {function} validator the validator to be added |
| * @param {boolean} [revalidate] True, to revalidate the value, false to just add the validator and do nothing else. Default is true. |
| */ |
| addValidator(validator, revalidate) { |
| var validators = this.validators.slice(); |
| validators.push(validator); |
| this.setValidators(validators, revalidate); |
| } |
| |
| /** |
| * @param {function} validator the validator to be removed |
| * @param {boolean} [revalidate] True, to revalidate the value, false to just remove the validator and do nothing else. Default is true. |
| */ |
| removeValidator(validator, revalidate) { |
| var validators = this.validators.slice(); |
| arrays.remove(validators, validator); |
| this.setValidators(validators, revalidate); |
| } |
| |
| /** |
| * Replaces all existing validators with the given one. If you want to add multiple validators, use {@link #addValidator}. |
| * <p> |
| * Remember calling the default validator which is passed as parameter to the validate function, if needed. |
| * @param {function} validator the new validator which replaces every other. If null, the default validator is used. |
| */ |
| setValidator(validator, revalidate) { |
| if (!validator) { |
| validator = this._validateValue.bind(this); |
| } |
| var validators = []; |
| if (validator) { |
| validators = [validator]; |
| } |
| this.setValidators(validators, revalidate); |
| } |
| |
| setValidators(validators, revalidate) { |
| this.setProperty('validators', validators); |
| if (this.initialized && scout.nvl(revalidate, true)) { |
| this.validate(); |
| } |
| } |
| |
| /** |
| * @param the value to be validated |
| * @returns {*} the validated value |
| * @throws a message, a Status or an error if the validation fails |
| */ |
| validateValue(value) { |
| var defaultValidator = this._validateValue.bind(this); |
| this.validators.forEach(function(validator) { |
| value = validator(value, defaultValidator); |
| }); |
| value = scout.nvl(value, null); // Ensure value is never undefined (necessary for _updateTouched and should make it easier generally) |
| return value; |
| } |
| |
| /** |
| * @returns {*} the validated value |
| * @throws a message, a Status or an error if the validation fails |
| */ |
| _validateValue(value) { |
| if (typeof value === 'string' && value === '') { |
| // Convert empty string to null. |
| // Not using strings.nullIfEmpty is by purpose because it also removes white space characters which may not be desired here |
| value = null; |
| } |
| return value; |
| } |
| |
| _validationFailed(value, error) { |
| $.log.isDebugEnabled() && $.log.debug('Validation failed for field with id ' + this.id, error); |
| var status = this._createValidationFailedStatus(value, error); |
| this.addErrorStatus(status); |
| this._updateDisplayText(value); |
| } |
| |
| _createValidationFailedStatus(value, error) { |
| return this._createInvalidValueStatus('ValidationFailedStatus', value, error); |
| } |
| |
| /** |
| * @param {string} statusType |
| * @returns {Status} |
| */ |
| _createInvalidValueStatus(statusType, value, error) { |
| var statusFunc = Status.classForName(statusType); |
| // type of status is correct |
| if (error instanceof statusFunc) { |
| return error; |
| } |
| var message, severity = Status.Severity.ERROR; |
| if (error instanceof Status) { |
| // its a Status, but it has the wrong specific type |
| message = error.message; |
| severity = error.severity; |
| } else if (typeof error === 'string') { |
| // convert string to status |
| message = error; |
| } else { |
| // create status with default message |
| message = this.session.text(this.invalidValueMessageKey, value); |
| } |
| return scout.create(statusType, { |
| message: message, |
| severity: severity |
| }); |
| } |
| |
| _updateDisplayText(value) { |
| if (!this.initialized && !objects.isNullOrUndefined(this.displayText)) { |
| // If a displayText is provided initially, use that text instead of using formatValue to generate a text based on the value |
| return; |
| } |
| value = scout.nvl(value, this.value); |
| var returned = this.formatValue(value); |
| if (returned && $.isFunction(returned.promise)) { |
| // Promise is returned -> set display text later |
| returned |
| .done(this.setDisplayText.bind(this)) |
| .fail(function() { |
| this.setDisplayText(''); |
| $.log.isInfoEnabled() && $.log.info('Could not resolve display text for value: ' + value); |
| }.bind(this)); |
| } else { |
| this.setDisplayText(returned); |
| } |
| } |
| |
| /** |
| * Replaces the existing formatter. The formatter is called during {@link #formatValue(value)}. |
| * <p> |
| * Remember calling the default formatter which is passed as parameter to the format function, if needed. |
| * @param {function} formatter the new formatter. If null, the default formatter is used. |
| */ |
| setFormatter(formatter) { |
| this.setProperty('formatter', formatter); |
| if (this.initialized) { |
| this.validate(); |
| } |
| } |
| |
| _setFormatter(formatter) { |
| if (!formatter) { |
| formatter = this._formatValue.bind(this); |
| } |
| this._setProperty('formatter', formatter); |
| } |
| |
| /** |
| * @returns {string|Promise} the formatted display text |
| */ |
| formatValue(value) { |
| var defaultFormatter = this._formatValue.bind(this); |
| return this.formatter(value, defaultFormatter); |
| } |
| |
| /** |
| * @returns {string|Promise} the formatted string or a promise |
| */ |
| _formatValue(value) { |
| return scout.nvl(value, '') + ''; |
| } |
| |
| _updateTouched() { |
| this.touched = !this._valueEquals(this.value, this.initialValue); |
| } |
| |
| addClearIcon($parent) { |
| if (!$parent) { |
| $parent = this.$container; |
| } |
| this.$clearIcon = $parent.appendSpan('clear-icon needsclick unfocusable') |
| .on('mousedown', this._onClearIconMouseDown.bind(this)); |
| } |
| |
| addContainer($parent, cssClass, layout) { |
| super.addContainer($parent, cssClass, layout); |
| this.$container.addClass('value-field'); |
| } |
| |
| addField($field) { |
| super.addField($field); |
| this.$field.data('valuefield', this); |
| } |
| |
| setCurrentMenuTypes(currentMenuTypes) { |
| this.setProperty('currentMenuTypes', currentMenuTypes); |
| } |
| |
| _renderCurrentMenuTypes() { |
| // If a tooltip is shown, update it with the new menus |
| this._updateFieldStatus(); |
| } |
| |
| _getCurrentMenus() { |
| if (this.currentMenuTypes) { |
| var menuTypes = this.currentMenuTypes.map(function(elem) { |
| return 'ValueField.' + elem; |
| }); |
| return menus_1.filter(this.menus, menuTypes); |
| } |
| return super._getCurrentMenus(); |
| } |
| |
| markAsSaved() { |
| super.markAsSaved(); |
| this.initialValue = this.value; |
| } |
| |
| /** |
| * @override |
| */ |
| _updateEmpty() { |
| this.empty = this.value === null || this.value === undefined; |
| } |
| |
| // ==== static helper methods ==== // |
| |
| /** |
| * Invokes 'ValueField.aboutToBlurByMouseDown' on the currently active value field. |
| * This method has no effect if another element is the focus owner. |
| */ |
| static invokeValueFieldAboutToBlurByMouseDown(target) { |
| var activeValueField = this._getActiveValueField(target); |
| if (activeValueField) { |
| activeValueField.aboutToBlurByMouseDown(target); |
| } |
| } |
| |
| /** |
| * Invokes 'ValueField.acceptInput' on the currently active value field. |
| * This method has no effect if another element is the focus owner. |
| */ |
| static invokeValueFieldAcceptInput(target) { |
| var activeValueField = this._getActiveValueField(target); |
| if (activeValueField) { |
| activeValueField.acceptInput(); |
| } |
| } |
| |
| /** |
| * Returns the currently active value field, or null if another element is active. |
| * Also, if no value field currently owns the focus, its parent is checked to be a value field and is returned accordingly. |
| * That is used in DateField.js with multiple input elements. |
| */ |
| static _getActiveValueField(target) { |
| var $activeElement = $(target).activeElement(), |
| activeWidget = scout.widget($activeElement); |
| if (activeWidget instanceof ValueField && activeWidget.enabledComputed) { |
| return activeWidget; |
| } |
| return null; |
| } |
| } |