/**
* 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/>.
*/
/**
* This mixin provides the ability to evaluate form control rules that define whether or not a given attribute
* is in a positive or negative state based on the changing values of other fields within the same form. The contents
* of this module were originally part of the [BaseFormControl]{@link module:alfresco/forms/controls/BaseFormControl}
* (into which this module is now mixed in) but was abstracted in order for the rules engine to be easily applied to
* other form modules (in particular the ability to support banner display with [Forms]{@link module:alfresco/forms/Form}).
*
* @module alfresco/forms/controls/utilities/RulesEngineMixin
* @since 1.0.32
* @author Dave Draper
*/
define(["dojo/_base/declare",
"dojo/_base/lang",
"dojo/_base/array"],
function(declare, lang, array) {
return declare([], {
/**
* This holds all the data about rules that need to be processed for the various attributes of the widget. By default this
* will handle rules for visibility, requirement and disability.
*
* @instance
* @type {object}
* @default
*/
_rulesEngineData: null,
/**
* <p>This function is reused to process the configuration for the visibility, disablement and requirement attributes of the form
* control. The format for the rules is as follows:</p>
* <p><pre>"visibilityConfig": {
* "initialValue": true,
* "rules": [
* {
* "targetId": "fieldId1",
* "is": ["a", "b", "c"],
* "isNot": ["d", "e", "f"]
* }
* ],
* "callbacks": {
* "id": "functionA"
* }
* }</pre></p>
* <p>This structure applied to the following attributes:<ul>
* <li>[visibilityConfig]{@link module:alfresco/forms/controls/BaseFormControl#visibilityConfig}</li>
* <li>[requirementConfig]{@link module:alfresco/forms/controls/BaseFormControl#requirementConfig}</li>
* <li>[disablementConfig]{@link module:alfresco/forms/controls/BaseFormControl#disablementConfig}</li></ul></p>
*
* @mmethod processConfig
* @param {string} attribute
* @param {object} config
* @param {object} [widget=this] The widget to apply rules changes to (defaults to the calling object, i.e. this)
*/
processConfig: function alfresco_forms_controls_utilities_RulesEngineMixin__processConfig(attribute, config, widget) {
if (config)
{
// If no widget has been provided then assume the current widget is the subject of the rules. This
// change is required following AKU-974 where this module was mixed into CoreWidgetProcessing to
// support form style visibility processing on any widget
if (!widget)
{
widget = this;
}
// Set the initial value...
if (typeof config.initialValue !== "undefined")
{
this[attribute](config.initialValue, widget);
}
// Process the rule subscriptions...
if (typeof config.rules !== "undefined")
{
this.processRulesConfig(attribute, config, widget);
}
else
{
// Debug output when instantiation data is incorrect. Only log when some data is defined but isn't an object.
// There's no point in logging messages for unsupplied data - just incorrectly supplied data.
this.alfLog("log", "The rules configuration for attribute '" + attribute + "' for property '" + this.fieldId + "' was not an Object");
}
// Process the callback subscriptions...
if (typeof config.callbacks === "object")
{
this._processCallbacksConfig(attribute, config.callbacks);
}
else if (typeof config.callbacks !== "undefined")
{
// Debug output when instantiation data is incorrect. Only log when some data is defined but isn't an object.
// There's no point in logging messages for unsupplied data - just incorrectly supplied data.
this.alfLog("log", "The callback configuration for attribute '" + attribute + "' for property '" + this.fieldId + "' was not an Object");
}
}
},
/**
* This function sets up the subscriptions for processing rules relating to attributes.
*
* @instance
* @param {string} attribute E.g. visibility, editability, requirement
* @param {object} config The full configuration for rules processing
* @param {object} [widget=this] The widget to apply rules changes to (defaults to the calling object, i.e. this)
*/
processRulesConfig: function alfresco_forms_controls_utilities_RulesEngineMixin__processRulesConfig(attribute, config, widget) {
// TODO: Implement rules for handling changes in validity (each type could have rule type of "isValid"
// and should subscribe to changes in validity. The reason for this would be to allow changes
// on validity. Validity may change asynchronously from value as it could be performed via a
// remote request.
// Set up the data structure that will be required for processing the rules for the target property changes...
if (!widget._rulesEngineData)
{
// Ensure that the rulesEngineData object has been created
widget._rulesEngineData = {};
}
if (typeof widget._rulesEngineData[attribute] === "undefined")
{
// Ensure that the rulesEngineData object has specific information about the form control attribute...
widget._rulesEngineData[attribute] = {};
}
array.forEach(config.rules, lang.hitch(this, this.processRule, attribute, config, widget));
},
/**
* This function processes an individual attribute rule (e.g. to change the visibility, disablement or
* requirement status).
*
* @instance
* @param {string} attribute The attribute that the rule effects (e.g. visibility)
* @param {object} config The full configuration for rules processing
* @param {object} [widget=this] The widget to apply rules changes to (defaults to the calling object, i.e. this)
* @param {object} rule The rule to process.
* @param {number} index The index of the rule.
*/
processRule: function alfresco_forms_controls_utilities_RulesEngineMixin__processRule(attribute, config, widget, rule, /*jshint unused:false*/ index) {
if (rule.targetId || rule.topic)
{
// As of AKU-974 the RulesEngine support non form controls, so we need to determine whether or not
// to use the targetId or topic for persisting data. At this stage we also need to update the config
// with a flag to assist processing of the data. This is done because there we want there is not
// an either/or test possible on topic or field id when evaluating the rules later (because alfTopic
// is always available!)...
var topic = "_valueChangeOf_" + rule.targetId;
var dataKey = rule.targetId;
if (!dataKey)
{
topic = rule.topic;
dataKey = widget.pubSubScope + rule.topic;
config._useTopic = true;
}
// Create an attribute to capure the value changes...
if (typeof widget._rulesEngineData[attribute][dataKey] === "undefined")
{
widget._rulesEngineData[attribute][dataKey] = {};
}
// Set the rules to be processed for the current rule...
// NOTE: Previous rules can be potentically overridden here...
widget._rulesEngineData[attribute][dataKey].rules = rule;
// Subscribe to changes in the relevant property...
this.alfSubscribe(topic, lang.hitch(this, this.evaluateRules, attribute, config, widget));
}
else
{
this.alfLog("warn", "The following rule is missing a 'name' attribute", rule, this);
}
},
/**
* This function evaluates all the rules configured for a particular attribute (e.g. "visibility") for the
* current form control. It is triggered whenever one of the other fields configured as part of a rule changes,
* but ALL the rules are evaluated for that attribute.
*
* @instance
* @param {string} attribute
* @param {object} config The full configuration for rules processing
* @param {object} [widget=this] The widget to apply rules changes to (defaults to the calling object, i.e. this)
* @param {object} payload The publication posted on the topic that triggered the rule
*/
evaluateRules: function alfresco_forms_controls_utilities_RulesEngineMixin__evaluateRules(attribute, config, widget, payload) {
this.alfLog("log", "RULES EVALUATION('" + attribute + "'): Field '" + (widget.fieldId || widget.id) + "'");
// Set the current value that triggered the evaluation of rules...
var dataKey = config._useTopic ? payload.alfTopic : payload.fieldId;
if (typeof widget._rulesEngineData[attribute][dataKey] !== "undefined")
{
var data = widget._rulesEngineData[attribute][dataKey];
var attributeKey = lang.getObject("rules.attribute", false, data);
if (!attributeKey)
{
attributeKey = "value";
}
widget._rulesEngineData[attribute][dataKey].currentValue = payload[attributeKey];
var rulesEngineKeys = Object.keys(widget._rulesEngineData[attribute]);
var status;
if (config.rulesMethod === "ANY")
{
// NOTE: Array.every returns true for empty arrays, but Array.some returns false. We want to work on the
// assumption that rules evaluate to true if there is no data to look at which is why we check the
// length of the keys array...
status = rulesEngineKeys.length === 0 || array.some(rulesEngineKeys, lang.hitch(this, this.evaluateRule, widget._rulesEngineData[attribute]));
}
else
{
status = array.every(rulesEngineKeys, lang.hitch(this, this.evaluateRule, widget._rulesEngineData[attribute]));
}
this[attribute](status, widget);
}
return status;
},
/**
*
* @instance
* @since 1.0.34
* @param {object} rulesEngineData The data required to evaluate the rule
* @param {string} key The current key to use in the rulesEngineData
* @return {boolean} Whether or not the rule evaluated successfully
*/
evaluateRule: function alfresco_forms_controls_utilities_RulesEngineMixin__evaluateRule(rulesEngineData, key) {
var currentValue = rulesEngineData[key].currentValue;
var validValues = rulesEngineData[key].rules.is;
var invalidValues = rulesEngineData[key].rules.isNot;
// Assume that its NOT valid value (we'll only do the actual test if its not set to an INVALID value)...
// UNLESS there are no valid values specified (in which case any value is valid apart form those in the invalid list)
var isValidValue = typeof validValues === "undefined" || validValues.length === 0;
// Initialise the invalid value to be false if no invalid values have been declared (and only check values if defined)...
var isInvalidValue = typeof invalidValues !== "undefined" && invalidValues.length > 0;
if (isInvalidValue)
{
// Check to see if the current value is set to an invalid value (i.e. a value that negates the rule)
isInvalidValue = array.some(invalidValues, lang.hitch(this, this.ruleValueComparator, currentValue));
}
// Check to see if the current value is set to a valid value...
if (!isInvalidValue && typeof validValues !== "undefined" && validValues.length > 0)
{
isValidValue = array.some(validValues, lang.hitch(this, this.ruleValueComparator, currentValue));
}
// The overall status is true (i.e. the rule is still passing) if the current status is true and the
// current value IS set to a valid value and NOT set to an invalid value
return isValidValue && !isInvalidValue;
},
/**
* The default comparator function used for comparing a rule value against the actual value of a field.
* Note that the target value is expected to be an object from the arrays (assigned to the "is" or "isNot"
* attribute) and by default the "value" attribute of those objects are compared with the current value
* of the field. It is possible to override this comparator to allow a more complex comparison operation.
*
* It's important to note that values are compared as strings. This is done to ensure that booleans can
* be compared. This is important as it should be possible to construct rules dynamically and values
* should be entered as text.
*
* @instance
* @param {object} currentValue The value currently
* @param {object} targetValue The value to compare against
*/
ruleValueComparator: function alfresco_forms_controls_utilities_RulesEngineMixin__ruleValueComparator(currentValue, targetValue) {
this.alfLog("log", "Comparing", currentValue, targetValue);
// If both values aren't null then compare the .toString() output, if one of them is null
// then it doesn't really matter whether or not we get the string output for the value or not
if (currentValue && targetValue && targetValue.value)
{
return currentValue.toString() === targetValue.value.toString();
}
else
{
// return currentValue == targetValue.value; // Commented out because I think this is wrong (shouldn't have .value)
return currentValue === targetValue;
}
},
/**
* The payload of property value changing publications should have the following attributes...
* 1) The name of the property that has changed ("name")
* 2) The old value of the property that has changed ("oldValue")
* 3) The new value of the property that has changed ("value")
* Callbacks should take the following arguments (nameOfChangedProperty, oldValue, newValue, callingObject, attribute)
*
* @instance
* @param {string} attribute
* @param {object} callbacks
*/
_processCallbacksConfig: function alfresco_forms_controls_utilities_RulesEngineMixin___processCallbacksConfig(attribute, callbacks) {
/*jshint loopfunc:true*/
// TODO Should refactor this to both avoid the loop functions and also reduce duplication
var _this = this;
for (var key in callbacks) {
if (typeof callbacks[key] === "function")
{
// Subscribe using the supplied function (this will only be possible when form controls are created
// dynamically from widgets (rather than in configuration)...
_this.alfSubscribe("_valueChangeOf_" + key, function(payload) {
var status = callbacks[payload.name](payload.name, payload.oldValue, payload.value, _this, attribute);
_this[attribute](status);
});
}
else if (typeof callbacks[key] === "string" &&
typeof _this[callbacks[key]] === "function")
{
// Subscribe using a String reference to a function defined in this widget...
_this.alfSubscribe(_this.pubSubScope + "_valueChangeOf_" + key, function(payload) {
var status = _this[callbacks[payload.name]](payload.name, payload.oldValue, payload.value, _this, attribute);
_this[attribute](status);
});
}
else
{
// Log a message if the callback supplied isn't actually a function...
this.alfLog("log", "The callback for property '" + _this.name + "' for handling changes to property '" + key + "' was not a function or was not a String that references a local function");
}
}
}
});
});