/**
* Copyright (C) 2005-2016 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* <p>This module handles form control validation and was written with the intention of being mixed into the
* [BaseFormControl module]{@link module:alfresco/forms/controls/BaseFormControl}. It provides the ability
* handle more complex validation, including asynchronous validation. This means that it is possible for
* a form to request remote data (e.g. to check whether or not a suggested identifier has already been used).</p>
* <p>The validators that are currently provided include checking that the length of a form field value is
* neither too long or too short, that it matches a specific Regular Expression pattern and whether or not the
* value is unique. Each validator should be configured with an explicit error message (if no error message is
* provided then the invalid indicator will be displayed with no message).</p>
* <p>Multiple validators can be chained together and if more than one validator reports that they are in error
* then their respective error messages will be displayed in sequence.</p>
*
* @example <caption>Example using all validators:</caption>
* validationConfig: [
* {
* validation: "minLength",
* length: 3,
* errorMessage: "Too short"
* },
* {
* validation: "maxLength",
* length: 5,
* errorMessage: "Too long"
* },
* {
* validation: "numericalRange",
* min: 500,
* max: 10000,
* errorMessage: "Must be between 500 and 10000"
* },
* {
* validation: "regex",
* regex: "^[0-9]+$",
* errorMessage: "Numbers only"
* },
* {
* validation: "validateUnique",
* errorMessage: "Already used",
* itemsProperty: "items",
* publishTopic: "GET_VALUES"
* },
* {
* validation: "validationTopic",
* validationTopic: "ALF_VALIDATE_WHITESPACE_TOPIC",
* errorMessage: "No initial or trailing whitespace"
* }
* ]
*
* @module alfresco/forms/controls/FormControlValidationMixin
* @extends module:alfresco/core/Core
* @author Dave Draper
*/
define(["dojo/_base/declare",
"alfresco/core/Core",
"dojo/_base/lang",
"dojo/_base/array",
"dojo/dom-class",
"alfresco/core/ObjectTypeUtils"],
function(declare, AlfCore, lang, array, domClass, ObjectTypeUtils) {
return declare([AlfCore], {
/**
* An array of the i18n files to use with this widget.
*
* @instance
* @type {Array}
*/
i18nRequirements: [{i18nFile: "./i18n/FormControlValidationMixin.properties"}],
/**
* The time in milliseconds to wait before displaying the validation progress indicator.
*
* @instance
* @type {number}
* @default
* @since 1.0.89
*/
validationProgressDisplayTimeout: 1000,
/**
* Indicates whether or not validation is currently in-progress or not
*
* @instance
* @type {boolean}
* @default
*/
_validationInProgress: false,
/**
* Keeps track of all the validators that are currently processing. This array is added to
* by [processValidationArrayElement]{@link module:alfresco/forms/controls/FormControlValidationMixin#processValidationArrayElement}
* and removed from during [validationComplete]{@link module:alfresco/forms/controls/FormControlValidationMixin#validationComplete}
*
* @instance
* @type {object}
* @default
*/
_validatorsInProgress: null,
/**
* Keeps track of the current validation state. Is updated by each call back to
* [validationComplete]{@link module:alfresco/forms/controls/FormControlValidationMixin#validationComplete}
*
* @instance
* @type {boolean}
* @default
*/
_validationInProgressState: true,
/**
* A timeout for showing in-progress validation indicators. Used to debounce the display of the indicator
* to prevent "jumping".
*
* @instance
* @type {object}
* @defaul
* @since 1.0.89
*/
_validationInProgressTimeout: null,
/**
* This is used to build up the overall validation message.
*
* @instance
* @type {string}
* @default
*/
_validationErrorMessage: null,
/**
* This is used to build up the overall validation message for warnings.
*
* @instance
* @type {string}
* @default
* @since 1.0.91
*/
_validationWarningMessage: null,
/**
* Called to start processing validators. If validation is already in progress then a flag will be set
* to queue another validation run once the current processing has completed.
*
* @instance
*/
startValidation: function alfresco_forms_controls_FormControlValidationMixin__startValidation() {
if (this._validationInProgress === true)
{
this._queuedValidationRequest = true;
}
else
{
// Reset the _validatorsInProgress attribute...
this._validationWarnings = false;
this._validationInProgressState = true;
this._validatorsInProgress = {};
this._validationErrorMessage = "";
this._validationMessage.textContent = this._validationErrorMessage;
this._validationWarningMessage = "";
this._warningMessage.textContent = this._validationWarningMessage;
this._validationInProgress = true;
this._queuedValidationRequest = false;
// Put the control into the invalid state (TODO: show a processing icon)
this.alfPublish("ALF_INVALID_CONTROL", {
name: this.name,
fieldId: this.fieldId
});
// Hide any previous errors and reveal the in-progress indicator...
this.hideValidationFailure();
clearTimeout(this._validationInProgressTimeout);
this._validationInProgressTimeout = setTimeout(lang.hitch(this, function() {
domClass.remove(this._validationInProgressIndicator, "hidden");
}), this.validationProgressDisplayTimeout);
// Iterate over each validation configuration, start it and add it to a count...
var validationErrors = [];
array.forEach(this.validationConfig, lang.hitch(this, this.processValidationArrayElement, validationErrors));
var count = this.countValidatorsInProgress();
if (count === 0)
{
// No validation validators were configured so just complete validation...
this.validationComplete();
}
else
{
// Call the relevant validation function for each validation configuration element,
// this is done *after* processing the configuration to ensure that validation isn't
// recorded as having completed *before* all the validators have started...
for (var key in this._validatorsInProgress)
{
if (this._validatorsInProgress.hasOwnProperty(key))
{
var validationConfig = this._validatorsInProgress[key];
this[validationConfig.validation](validationConfig);
}
}
}
}
},
/**
* This function is called from the [processValidationRules]{@link module:alfresco/forms/controls/BaseFormControl#processValidationRules}
* function for each element of the [validationConfig]{@link module:alfresco/forms/controls/BaseFormControl#validationConfig}
* configuration attribute. It checks that the supplied 'validation' attribute maps to a function
* a function (the core validation functions are defined in the [FormControlValidationMixin]{@link module:alfresco/forms/controls/FormControlValidationMixin}
* module).
*
* @instance
* @param {array} validationErrors An array to populate with validation errors.
* @param {object} validationConfig The current element to process
* @param {number} index The index of the element in the array
* @returns {boolean} True if validation is passed and false otherwise
*/
processValidationArrayElement: function alfresco_forms_controls_BaseFormControl__processValidationArrayElement(validationErrors, validationConfig, index) {
var validationType = lang.getObject("validation", false, validationConfig);
if (validationType)
{
if (typeof this[validationType] === "function")
{
// Add the current validator to the those currently in-flight and
// using the index as a key. This index is also added to the
this._validatorsInProgress[index] = validationConfig;
validationConfig.index = index;
}
else
{
this.alfLog("warn", "Validation configuration 'validation' attribute refers to non-existent function", validationType, this);
}
}
else
{
this.alfLog("warn", "Validation configuration missing a 'validation' attribute", validationConfig, this);
}
},
/**
* Counts the validators that are currently in progress.
*
* @instance
* @returns {number} The number of validators still in progress
*/
countValidatorsInProgress: function alfresco_forms_controls_FormControlValidationMixin__countValidatorsInProgress() {
var count = 0;
for (var key in this._validatorsInProgress)
{
if (this._validatorsInProgress.hasOwnProperty(key))
{
count++;
}
}
return count;
},
/**
* This function should be called by all validators when they have finished validating the
* current entry.
*
* @instance
* @param {object} validationConfig The configuration for the validator that has just completed
* @param {boolean} result Whether or not the validation was successful or not
*/
reportValidationResult: function alfresco_forms_controls_FormControlValidationMixin__reportValidationResult(validationConfig, result) {
this.alfLog("log", "Validator complete, result: " + result, validationConfig);
var index = lang.getObject("index", false, validationConfig);
if (!(index || index === 0))
{
this.alfLog("warn", "Validation completion call without index attribute", validationConfig, this);
}
else
{
// Update the error message...
if (result === false)
{
if (validationConfig.errorMessage)
{
if (validationConfig.warnOnly === true)
{
if (this._validationWarningMessage.length !== 0)
{
this._validationWarningMessage += ", ";
}
this._validationWarningMessage += this.message(validationConfig.errorMessage);
}
else
{
if (this._validationErrorMessage.length !== 0)
{
this._validationErrorMessage += ", ";
}
this._validationErrorMessage += this.message(validationConfig.errorMessage);
}
}
if (validationConfig.warnOnly === true)
{
// Show as a warning...
this._validationWarnings = true;
result = true;
this.showValidationWarning();
// Update the validation message...
this._warningMessage.textContent = this._validationWarningMessage;
}
else
{
// Show as a failure...
this.showValidationFailure();
// Update the validation message...
this._validationMessage.textContent = this._validationErrorMessage;
}
}
// Update the overall result...
this._validationInProgressState = this._validationInProgressState && result;
// Remove the completed validator using the configured index...
delete this._validatorsInProgress[index];
// Count the remaining validators...
var count = this.countValidatorsInProgress();
// If all are complete then update validation status
if (count === 0)
{
clearTimeout(this._validationInProgressTimeout);
this.validationComplete();
}
else
{
this.alfLog("log", "Waiting on " + count + " more validators to complete");
}
}
},
/**
* This function is called when all validators have reported their finished state.
*
* @instance
*/
validationComplete: function alfresco_forms_controls_FormControlValidationMixin__validationComplete() {
// Hide the in-progress indicator...
domClass.add(this._validationInProgressIndicator, "hidden");
// Check requirement validation...
var value = this.getValue();
var valueIsEmptyArray = ObjectTypeUtils.isArray(value) && value.length === 0;
var requirementTest = !(this._required && ((!value && value !== 0 && value !== false) || valueIsEmptyArray));
// Publish the results...
if (this._validationInProgressState && requirementTest)
{
this.alfPublish("ALF_VALID_CONTROL", {
name: this.name,
fieldId: this.fieldId
});
this.hideValidationFailure();
if (this._validationWarnings)
{
this.showValidationWarning();
}
}
else
{
this.alfPublish("ALF_INVALID_CONTROL", {
name: this.name,
fieldId: this.fieldId
});
this.showValidationFailure();
}
this._validationInProgress = false;
if (this._queuedValidationRequest === true)
{
this.startValidation();
}
},
/**
* This validator checks the current form field value against the configured regex pattern.
*
* @instance
* @param {object} validationConfig The configuration for this validator
*/
regex: function alfresco_forms_controls_FormControlValidationMixin__regex(validationConfig) {
var isValid = true;
var regexPattern = lang.getObject("regex", false, validationConfig);
if (typeof regexPattern === "string")
{
var value = this.getValue();
var regExObj = new RegExp(regexPattern);
isValid = regExObj.test(value);
if (validationConfig.invertRule === true)
{
isValid = !isValid;
}
}
else
{
this.alfLog("warn", "A regex validation was configured with an invalid 'regex' attribute", validationConfig, this);
}
this.reportValidationResult(validationConfig, isValid);
},
/**
* This validator ensures that the form value is a number that falls within a range. It is acceptable to
* only provide the min or max of the range.
*
* @instance
* @param {object} validationConfig The configuration for this validator
*/
numericalRange: function alfresco_forms_controls_FormControlValidationMixin__numericalRange(validationConfig) {
// PLEASE NOTE: isNaN returns false for null
var isValid = true;
var value = parseFloat(this.getValue());
var minValue = lang.getObject("min", false, validationConfig);
var permitEmpty = lang.getObject("permitEmpty", false, validationConfig);
if (permitEmpty && this.getValue() === null)
{
// If permitEmpty is true, and there is no value then there is no need to check min/max
}
else
{
if (minValue !== null && !isNaN(minValue))
{
isValid = (!isNaN(value) && value >= minValue);
}
else
{
this.alfLog("warn", "A numericalRange validation was configured with an invalid 'min' attribute", validationConfig, this);
}
var maxValue = lang.getObject("max", false, validationConfig);
if (maxValue !== null && !isNaN(maxValue))
{
isValid = isValid && (!isNaN(value) && value <= maxValue);
}
else
{
this.alfLog("warn", "A numericalRange validation was configured with an invalid 'max' attribute", validationConfig, this);
}
}
this.reportValidationResult(validationConfig, isValid);
},
/**
* This validator checks that the current form field value is longer than the configured value.
*
* @instance
* @param {object} validationConfig The configuration for this validator
*/
maxLength: function alfresco_forms_controls_FormControlValidationMixin__maxLength(validationConfig) {
var isValid = true;
var targetLength = lang.getObject("length", false, validationConfig);
if (!isNaN(targetLength))
{
var value = this.getValue();
isValid = value.length <= targetLength;
}
else
{
this.alfLog("warn", "A maxLength validation was configured with an invalid 'length' attribute", validationConfig, this);
}
this.reportValidationResult(validationConfig, isValid);
},
/**
* This validator checks that the current form field value is shorter than the configured value.
*
* @instance
* @param {object} validationConfig The configuration for this validator
*/
minLength: function alfresco_forms_controls_FormControlValidationMixin__minLength(validationConfig) {
var isValid = true;
var targetLength = lang.getObject("length", false, validationConfig);
if (!isNaN(targetLength))
{
var value = this.getValue();
isValid = value.length >= targetLength;
}
else
{
this.alfLog("warn", "A minLength validation was configured with an invalid 'length' attribute", validationConfig, this);
}
this.reportValidationResult(validationConfig, isValid);
},
/**
* This validator checks that the value of the form control with a "fieldId" attribute matching the
* configured "targetId" attribute in the validation configuration matches the current value of this
* form control.
*
* @instance
* @param {object} validationConfig The configuration for this validator
*/
validateMatch: function alfresco_forms_controls_FormControlValidationMixin__validateMatch(validationConfig) {
var targetId = lang.getObject("targetId", false, validationConfig);
if (targetId)
{
// Only subscribe to changes once...
if (!this._validateMatchSubscriptionHandle)
{
this._validateMatchSubscriptionHandle = this.alfSubscribe("_valueChangeOf_" + targetId, lang.hitch(this, this._validateMatchTargetValueChanged, validationConfig));
}
this.reportValidationResult(validationConfig, (this.getValue() === this._validateMatchTargetValue));
}
else
{
this.alfLog("warn", "A validateMatch validation was configured without a 'targetId' attribute", validationConfig, this);
}
},
/**
* Holds the subscription for publication of changes in value of the target form control configured for the
* [validateMatch]{@link module:alfresco/forms/controls/FormControlValidationMixin#validateMatch}
* validator.
*
* @instance
* @type {object}
* @default
*/
_validateMatchSubscriptionHandle: null,
/**
* Used to keep track of changes to the value of the target form control configured for the
* [validateMatch]{@link module:alfresco/forms/controls/FormControlValidationMixin#validateMatch}
* validator.
*
* @instance
* @type {object}
* @default
*/
_validateMatchTargetValue: "",
/**
* When the match target value changes, update a local copy to compare against.
*
* @instance
* @param {object} validationConfig The configuration for this validator
* @param {object} payload A payload containing the latest match target value
*/
_validateMatchTargetValueChanged: function alfresco_forms_controls_FormControlValidationMixin___validateMatchTargetValueChanged(validationConfig, payload) {
this._validateMatchTargetValue = payload.value;
this.validateFormControlValue();
},
/**
* Stores the validator configuration for validating uniqueness so that it doesn't need to be included
* in the payload from the called service. It is set in [validateUnique]{@link module:alfresco/forms/controls/FormControlValidationMixin#validateUnique}
* and reset to null in [onValidationUniqueResponse]{@link module:alfresco/forms/controls/FormControlValidationMixin#onValidationUniqueResponse}.
*
* @instance
* @type {object}
* @default
*/
_validateUniqueConfig: null,
/**
* This validator checks that the current form field value has not already been used on another item.
* It should be configured with a 'publishTopic' attribute to request a list of existing items and
* a 'publishPayload' attribute to go with it. Optionally an 'itemsProperty' attribute can be configured
* that will identify the attribute in the returned payload that will contain the array of items to
* iterate through looking for a value that matches the currently entered value.
*
* @instance
* @param {object} config The configuration for this validation
*/
validateUnique: function alfresco_forms_controls_FormControlValidationMixin__validateUnique(validationConfig) {
var responseTopic = this.generateUuid();
var payload = (validationConfig.publishPayload) ? lang.clone(validationConfig.publishPayload) : {};
payload.alfResponseTopic = responseTopic;
this._validateUniqueConfig = validationConfig;
this._validateUniqueHandle = this.alfSubscribe(responseTopic + "_SUCCESS", lang.hitch(this, this.onValidationUniqueResponse), true);
this.alfPublish(validationConfig.publishTopic, payload, true);
return true;
},
/**
* This is the callback function that is called as a result of using the
* [validateUnique]{@link module:alfresco/forms/controls/FormControlValidationMixin#validateUnique} validator.
*
* @instance
* @param {object} payload The payload containing the items to iterate through looking for an existing use of the form field value
*/
onValidationUniqueResponse: function alfresco_forms_controls_FormControlValidationMixin__onValidationUniqueResponse(payload) {
this.alfUnsubscribeSaveHandles([this._validateUniqueHandle]);
var notUnique = false;
// Grab the previously saved configuration from the instance and then remove it...
var validationConfig = this._validateUniqueConfig;
this._validateUniqueConfig = null;
if (payload)
{
// Get the configured items property (this identifies the attribute in the payload containing
// the array to iterate over)...
var itemsProperty = lang.getObject("itemsProperty", false, validationConfig);
if (!itemsProperty)
{
itemsProperty = "items";
}
// Get the current form field value to compare with each item...
var value = this.getValue();
// Get the array of items and check if the form field 'name' attribute for each item matches the
// currently entered form field value...
var items = lang.getObject(itemsProperty, false, payload);
if (!items)
{
this.alfLog("warn", "Attempting to validate uniqueness but 'itemsProperty' attribute doesn't map to an an array", validationConfig, this);
}
else
{
notUnique = array.some(items, function(item) {
var itemValue = lang.getObject(this.name, false, item);
return itemValue === value;
}, this);
}
}
// Report back with the validation result...
this.reportValidationResult(validationConfig, !notUnique);
},
/**
* Call a topic to validate the value of a form control.
* That topic will receive the value in payload.value & should publish a response on payload.alfResponseTopic
* The response payload should contain an "isValid" boolean property.
*
* @instance
* @param {object} validationConfig
*/
validationTopic: function alfresco_forms_controls_FormControlValidationMixin__validationTopic(validationConfig) {
if (!validationConfig || !validationConfig.validationTopic)
{
this.alfLog("warn", "ValidationTopic missing required fields: " + validationConfig);
// Default to return true
this.reportValidationResult(validationConfig, true);
return;
}
// Save a reference so we can get the config later
this._validationTopicConfig = validationConfig;
if (validationConfig.validateInitialValue === false && this.getValue() === this.initialValue)
{
this.reportValidationResult(validationConfig, true);
}
else
{
var payload;
if (validationConfig.validationPayload)
{
// Use the configured payload if provided (but add in the necessary attributes to ensure
// that the publication/subscription works)...
payload = lang.clone(validationConfig.validationPayload);
payload.alfResponseTopic = validationConfig.alfResponseTopic || this.generateUuid();
payload.validationConfig = validationConfig;
}
else
{
// Create a default payload if none is provided...
payload = {
validationConfig: validationConfig,
value: this.getValue(),
field: this,
alfResponseTopic: validationConfig.alfResponseTopic || this.generateUuid()
};
}
// Duplicate the alfResponseTopic...
payload.alfSuccessTopic = payload.alfResponseTopic;
payload.alfFailureTopic = this.generateUuid();
payload.alfResponseScope = ""; // This effectively defaults to global scope...
if (validationConfig.scopeValidation)
{
// See AKU-1099 - it is better to scope responses to ensure that validation of
// one field does not impact another, however - for backwards compability support
// for RM 2.5 it is necessary to depend on the opt-in "scopeValidation" attribute
// which should be a boolean.
payload.alfResponseScope = this.generateUuid();
}
// Set the validation value as required...
if (validationConfig.validationValueProperty)
{
lang.setObject(validationConfig.validationValueProperty, this.getValue(), payload);
}
var publishGlobal = true;
var publishScope = null;
if (validationConfig.validationTopicScope)
{
publishGlobal = false;
publishScope = validationConfig.validationTopicScope;
}
this._validationTopicHandles = [];
this._validationTopicHandles.push(this.alfSubscribe(payload.alfResponseTopic, lang.hitch(this, this.onValidationTopicResponse), false, false, payload.alfResponseScope));
this._validationTopicHandles.push(this.alfSubscribe(payload.alfFailureTopic, lang.hitch(this, this.onValidationTopicFailure), false, false, payload.alfResponseScope));
this.alfPublish(validationConfig.validationTopic, payload, publishGlobal, false, publishScope);
}
},
/**
* The response called by the topic specified in validationTopic. Receives the isValid state and updates the form.
*
* @instance
* @param {object} payload
*/
onValidationTopicResponse: function alfresco_forms_controls_FormControlValidationMixin__onValidationTopicResponse(payload) {
this.alfUnsubscribeSaveHandles(this._validationTopicHandles);
if (!payload)
{
this.alfLog("warn", "ValidationTopic missing payload");
}
var validationConfig = this._validationTopicConfig;
this._validationTopicConfig = null;
var resultProperty = validationConfig.validationResultProperty || "isValid";
var isValid = lang.getObject(resultProperty, false, payload);
if (validationConfig.negate)
{
isValid = !isValid;
}
// Report back with the validation result...
this.reportValidationResult(validationConfig, isValid);
},
/**
* Handles failed requests to peform validation by publishing a topic. Assumes valid data on failure.
*
* @instance
* @param {object} payload The details of the validation failure
* @since 1.0.89
*/
onValidationTopicFailure: function alfresco_forms_controls_FormControlValidationMixin__onValidationTopicFailure(payload) {
this.alfLog("warn", "It was not possible to validate the field", payload, this);
this.reportValidationResult(this._validationTopicConfig, true);
}
});
});