Source: forms/Form.js

/**
 * 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 is the root module for all Aikau forms. It is intended to work with widgets that extend the
 * [BaseFormControl]{@link module:alfresco/forms/controls/BaseFormControl} and handles setting and 
 * getting there values as well as creating and controlling the behaviour of buttons that can be
 * used to publish the overall value of the controls that the form contains.</p>
 *
 * <p>By default forms will show error messages against any fields that contain invalid data when the 
 * form is first displayed. If you'd prefer to give the user a chance to enter or change the data before
 * validation errors are displayed then this can be achieved by configuring the 
 * [showValidationErrorsImmediately]{@link module:alfresco/forms/Form#showValidationErrorsImmediately}
 * to be false.</p>
 * 
 * <p>It is also possible to setup a form to automatically publish itself whenever its values change,
 * and optionally to also do so if any of its values are invalid. If the 
 * [autoSavePublishTopic]{@link module:alfresco/forms/Form#autoSavePublishTopic} is specified, 
 * then the OK and Cancel buttons are automatically hidden.</p>
 *
 * <p>Configuration can be provided to create rules that display one or more [warnings]{@link module:alfresco/forms/Form#warnings}
 * based on the changing values of fields in the forms. The [position of the warnings]{@link module:alfresco/forms/Form#warningsPosition}
 * can be configured to either be at "top" or "bottom" of the form.</p>
 * 
 * <p><b>PLEASE NOTE:</b> If you want to layout your form controls in a specific manner (e.g. in a horizontal 
 * line or within tabs) then you should use dedicated form control layout widgets such as 
 * [ControlRow]{@link module:alfresco/forms/ControlRow} or
 * [TabbedControls]{@link module:alfresco/forms/TabbedControls} as the basic layout widgets 
 * do not provide all the required functionality.</p>
 * 
 * @example <caption>Example configuration for auto-publishing (including invalid) form:</caption>
 * {
 *    name: "alfresco/forms/Form",
 *    config: {
 *       autoSavePublishTopic: "AUTOSAVE_FORM",
 *       autoSavePublishGlobal: true,
 *       autoSaveOnInvalid: true,
 *       widgets: [
 *          {
 *             name: "alfresco/forms/controls/TextBox",
 *             config: {
 *                name: "control",
 *                label: "Autosave form control (even invalid)",
 *                requirementConfig: {
 *                   initialValue: true
 *                }
 *             }
 *          }
 *       ]
 *    }
 * }
 *
 * @example <caption>Example configuration for form that does not show validation errors on initial display:</caption>
 * {
 *    name: "alfresco/forms/Form",
 *    config: {
 *       okButtonPublishTopic: "SAVE_FORM",
 *       okButtonLabel: "Save",
 *       showValidationErrorsImmediately: false,
 *       widgets: [
 *          {
 *             name: "alfresco/forms/controls/TextBox",
 *             config: {
 *                name: "control",
 *                label: "Name",
 *                requirementConfig: {
 *                   initialValue: true
 *                }
 *             }
 *          }
 *       ]
 *    }
 * }
 *
 * @example <caption>Example configuration for form with additional buttons:</caption>
 * {
 *    name: "alfresco/forms/Form",
 *    config: {
 *       okButtonPublishTopic: "SAVE_FORM",
 *       okButtonLabel: "Save",
 *       showValidationErrorsImmediately: false,
 *       widgets: [
 *          {
 *             name: "alfresco/forms/controls/TextBox",
 *             config: {
 *                name: "control",
 *                label: "Name",
 *                requirementConfig: {
 *                   initialValue: true
 *                }
 *             }
 *          }
 *       ],
 *       widgetsAdditionalButtons: [
 *          {
 *             name: "alfresco/buttons/AlfButton",
 *             config: {
 *                label: "Button 1 (includes form values)",
 *                publishTopic: "CUSTOM_TOPIC_1",
 *                publishPayload: {
 *                   additional: "data"
 *                } 
 *             }
 *          },
 *          {
 *             name: "alfresco/buttons/AlfButton",
 *             config: {
 *                updatePayload: false,
 *                label: "Button 1 (does not include form values)",
 *                publishTopic: "CUSTOM_TOPIC_2",
 *                publishPayload: {
 *                   only: "data"
 *                } 
 *             }
 *          }
 *       ]
 *    }
 * }
 * 
 * @module alfresco/forms/Form
 * @extends external:dijit/_WidgetBase
 * @mixes external:dojo/_TemplatedMixin
 * @mixes module:alfresco/core/Core
 * @mixes module:alfresco/core/CoreWidgetProcessing
 * @mixes module:alfresco/documentlibrary/_AlfHashMixin
 * @mixes module:alfresco/forms/controls/utilities/RulesEngineMixin
 * @pageSafe
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin",
        "dijit/form/Form",
        "alfresco/core/Core",
        "alfresco/core/CoreWidgetProcessing",
        "alfresco/core/topics",
        "alfresco/documentlibrary/_AlfHashMixin",
        "alfresco/forms/controls/utilities/RulesEngineMixin",
        "dojo/text!./templates/Form.html",
        "dojo/io-query",
        "alfresco/header/Warning",
        "alfresco/util/hashUtils",
        "dojo/_base/lang",
        "alfresco/buttons/AlfButton",
        "dojo/_base/array",
        "dijit/registry",
        "dojo/Deferred",
        "dojo/dom-construct",
        "dojo/dom-class",
        "dojo/_base/event",
        "dojo/on",
        "jquery"], 
        function(declare, _Widget, _Templated, Form, AlfCore, CoreWidgetProcessing, topics, _AlfHashMixin, RulesEngineMixin, 
                 template, ioQuery, Warning, hashUtils, lang, AlfButton, array, registry, Deferred, domConstruct, domClass, 
                 Event, on, $) {
   
   return declare([_Widget, _Templated, AlfCore, CoreWidgetProcessing, _AlfHashMixin, RulesEngineMixin], {
      
      /**
       * An array of the i18n files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{i18nFile: "./i18n/AlfDialog.properties"}]
       */
      i18nRequirements: [{i18nFile: "./i18n/Form.properties"}],
      
      /**
       * An array of the CSS files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{cssFile:"./css/Form.css"}]
       */
      cssRequirements: [{cssFile:"./css/Form.css"}],

      /**
       * The HTML template to use for the widget.
       * @instance
       * @type {String}
       */
      templateString: template,
      
      /**
       * @instance
       * @type {object}
       * @default
       */
      _form: null,
      
      /**
       * @instance
       * @type {object[]}
       * @default
       */
      widgets: null,
      
      /**
       * The URL that the form will be posted to
       * 
       * @instance
       * @type {string}
       * @default
       */
      postUrl: "",
      
      /**
       * @instance
       * @type {boolean}
       * @default
       */
      convertFormToJsonString: false,

      /**
       * <p>Should the first field in the form be focused when the form is loaded?</p>
       *
       * <p><strong>NOTE:</strong> If more than one form on a page has this setting,
       * then the order in which the fields are focused cannot be guaranteed.</p>
       *
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.49
       */
      firstFieldFocusOnLoad: false,

      /**
       * This will be instantiated as an array and used to keep track of any controls that report themselves as being
       * in an invalid state. The "OK" button for submitting the form should only be enabled when this list is empty.
       * 
       * @instance
       * @type {object[]}
       * @default
       */
      invalidFormControls: null,
      
      /**
       * A reference to the "OK" button for the form.
       * TODO: It should be possible to configure alternative labels for the button
       * 
       * @instance
       * @type {object}
       * @default
       */
      okButton: null,
      
      /**
       * A reference to the "Cancel" button for the form.
       * TODO: It should be possible to configure alternative labels for the button.
       * 
       * @instance
       * @type {object}
       * @default
       */
      cancelButton: null,
      
      /**
       * Indicates that the a new pubSubScope should be generated for this widget so that it's
       * form controls will be scoped to only communicate with this instance and not "pollute"
       * any other forms that may also be on the page.
       * 
       * @instance
       * @type {boolean}
       * @default
       */
      scopeFormControls: true,
      
      /**
       * Indicates whether or not to create buttons for this form.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      displayButtons: true,
      
      /**
       * This should be set to a specific topic that can be published on to set the value of the form.
       * The default value is null (e.g. setting via publication will not be possible unless a topic
       * is explictly configured). The [setValueTopicGlobalScope]{@link module:alfresco/forms/Form#setValueTopicGlobalScope}
       * and [setValueTopicParentScope]{@link module:alfresco/forms/Form#setValueTopicParentScope} should 
       * also be set accordingly.
       * 
       * @instance
       * @type {string}
       * @default
       */
      setValueTopic: null,

      /**
       * This indicates whether any [setValueTopic]{@link module:alfresco/forms/Form#setValueTopic} subscription
       * should be made globally.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      setValueTopicGlobalScope: true,

      /**
       * This indicates whether any [setValueTopic]{@link module:alfresco/forms/Form#setValueTopic} subscription
       * should be made to the parent scope.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      setValueTopicParentScope: false,

      /**
       * Indicates that any validation errors that are present on the fields contained within the form will be 
       * shown immediately when the form is displayed. Validation will still occur if this is configured to be
       * false (so it will not be possible to submit an invalid form).
       * 
       * @instance
       * @type {boolean}
       * @default
       */
      showValidationErrorsImmediately: true,

      /**
       * When using the optional auto-saving capability (configured by setting an [autoSavePublishTopic]{@link module:alfresco/forms/Form#autoSavePublishTopic})
       * the form will typically need to wait until page setup is complete before allowing automatic saving to commence. However,
       * if a form (configured to auto-save) is dynamically created after the page has been loaded (e.g. when created within a
       * dialog for example) then it this should be configured to be false (otherwise auto-save will not occur)
       * 
       * @instance
       * @type {boolean}
       * @default
       */
      waitForPageWidgets: true,

      /**
       * <p>Can be configured to display one or more banners on the form based on the changing value of fields
       * within the form. The configuration is almost identical to that of the 
       * [visibilityConfig]{@link module:alfresco/forms/controls/BaseFormControl#visibilityConfig} in form fields
       * except that a "message" attribute should also be included that will be displayed when the rules 
       * are evaluated to be true.</p>
       * <p>The default position for the warnings is at the top of the form but it is possible to move the
       * warnings to the bottom of the form by configuring
       * [warningsPosition]{@link module:alfresco/forms/Form#warningsPosition} to be "bottom".
       * </p>
       *
       * @example <caption>Example configuration for warning displayed when "FIELD1" is set</caption>
       * warnings: [
       *   {
       *     message: "Warning 1: Field 1 is not blank",
       *     initialValue: true,
       *     rules: [
       *       {
       *         targetId: "FIELD1",
       *         isNot: [""]
       *       }
       *     ]
       *   }
       * ]
       *
       * @instance
       * @type {object[]}
       * @default
       * @since 1.0.32
       */
      warnings: null,

      /**
       * This is the position that any [warnings]{@link module:alfresco/forms/Form#warnings} will be placed. 
       * There are two options available which are "top" (the default) and "bottom". Extending widgets may provide
       * alternative positions.
       *
       * @instance
       * @type {string}
       * @default
       * @since 1.0.32
       */
      warningsPosition: "top",

      /**
       * This is an optional topic that can be provided to allow other widgets to trigger the submission of 
       * the form. This was added to support requirements of the
       * [FormsRuntimeService]{@link module:alfresco/services/FormsRuntimeService} and in particular the
       * [Transitions]{@link module:alfresco/forms/controls/Transitions} control.
       * 
       * @instance
       * @type {string}
       * @default
       * @since 1.0.86
       */
      formSubmissionTriggerTopic: null,

      /**
       * @instance
       * @listens module:alfresco/core/topics#GET_FORM_VALUE_DEPENDENT_OPTIONS
       */
      postCreate: function alfresco_forms_Form__postCreate() {
         // A form is configured to auto-save updates we want to be sure that it doesn't start saving until the 
         // use has actually made any changes. Therefore we want to wait until all the widgets are properly setup
         // before we allow autosaving to commence. However, when the form is included directly on a page (e.g. it is
         // not part of a dialog, etc) then we want to wait until the page has completed being setup, as this publications
         // are queued up until this occurs.
         if (this.waitForPageWidgets === true)
         {
            this.pageWidgetsReadySubcription = this.alfSubscribe(topics.PAGE_WIDGETS_READY, lang.hitch(this, function() {
               this.alfUnsubscribe(this.pageWidgetsReadySubcription);
               this._readyToAutoSave = true;
            }), true);
         }
         else
         {
            this._readyToAutoSave = true;
         }

         // Setup some arrays for recording the valid and invalid widgets...
         this.invalidFormControls = [];
         
         // Generate a new pubSubScope if required...
         if (this.scopeFormControls === true && this.pubSubScope === "")
         {
            this.pubSubScope = this.generateUuid();
         }

         // If requested in the configuration, the value of a form can be set via a publication,
         // however to avoid generating subscriptions unnecessarily the subscription is only
         // set if explicitly requested. Global scope is intentionally used for the subscription
         if (this.setValueTopic)
         {
            this.alfSubscribe(this.setValueTopic, lang.hitch(this, this.setValue), this.setValueTopicGlobalScope, this.setValueTopicParentScope);
         }

         // Create a subscription that allows fields within the form to request options with a payload
         // that is augmented with the value of the form...
         this.alfSubscribe(topics.GET_FORM_VALUE_DEPENDENT_OPTIONS, lang.hitch(this, this.getFormValueDependantOptions));

         // Create any configured warnings...
         this.createWarnings();

         // When any of these topics are published, submit the form
         if (this.publishValueSubscriptions && this.publishValueSubscriptions.length) {
            array.forEach(this.publishValueSubscriptions, function(subscriptionTopic) {
               this.alfSubscribe(subscriptionTopic, lang.hitch(this, this.submitOkButton));
            }, this);
         }
         
         this._form = new Form({
            id: this.generateUuid()
         }, this.formNode);
         
         // Set up the handlers for form controls reporting themselves as valid or invalid
         // following user update...
         this.alfSubscribe("ALF_INVALID_CONTROL", lang.hitch(this, this.onInvalidField));
         this.alfSubscribe("ALF_VALID_CONTROL", lang.hitch(this, this.onValidField));

         if (this.displayButtons === true)
         {
            // Create the buttons for the form...
            this.createButtons();
         }

         // See AKU-1049 - check for any "late" registered fields. This triggers republication
         // of values, but only occurs if the fields are added to the DOM after form setup 
         // has completed. This addresses the case where wrappers - in particular TabbedControls
         // - are used in a form
         on(this.domNode, "ALF_FIELD_ADDED_TO_FORM", lang.hitch(this, function(evt) {
            evt && Event.stop(evt);
            if (this._formSetupComplete)
            {
               array.forEach(this._form.getChildren(), function(entry) {
                  if (typeof entry.publishValue === "function")
                  {
                     entry.publishValue();
                  }
               });
            }
         }));

         // Add the widgets to the form...
         // The widgets should automatically inherit the pubSubScope from the form to scope communication
         // to this widget. However, this widget will need to be assigned with a pubSubScope... 
         if (this.widgets)
         {
            array.forEach(this.widgets, function(widget) {
               if (widget && widget.config)
               {
                  widget.config.showValidationErrorsImmediately = this.showValidationErrorsImmediately;
                  if (widget.config.pubSubScope)
                  {
                     this.alfLog("warn", "It is not recommended to set a pubSubScope attribute on a form control nested within a form, the value of the form control will not be included in the published form value", this, widget);
                  }
               }
            }, this);

            this.processWidgets(this.widgets, this._form.domNode, "FIELDS");
         }

         // Setup subscriptions for the re-enablement of the OK button if necessary
         if(this.okButton && this.okButtonDisableOnPublish && this.okButtonEnablementTopics && this.okButtonEnablementTopics.length) {
            array.forEach(this.okButtonEnablementTopics, function(topic) {
               this.setupOkButtonEnablementSubscription(topic);
            }, this);
         }

         if (this.formSubmissionTriggerTopic && this.okButton)
         {
            this.alfSubscribe(this.formSubmissionTriggerTopic, lang.hitch(this, function() {
               this.okButton.onClick();
            }));
         }
      },

      /**
       * This function is called to process any [warning configuration]{@link module:alfresco/forms/Form#warnings} 
       * and create a [warning widget]{@link module:alfresco/header/Warning} to display those warnings.
       *
       * @instance
       * @since 1.0.32
       */
      createWarnings: function alfresco_forms_Form_createWarnings() {
         var warnings = [];
         array.forEach(this.warnings, lang.hitch(this, this.processWarnings, warnings));
         this.createWidget({
            id: this.id + "_WARNINGS",
            name: "alfresco/header/Warning",
            config: {
               warnings: warnings
            }
         }).placeAt((this.warningsPosition === "top" ? this.warningsTopNode : this.warningsBottomNode));

         if (warnings.length > 0)
         {
            domClass.remove(this.warningsPosition === "top" ? this.warningsTopNode : this.warningsBottomNode, "alfresco-forms-Form__warnings--hidden");
         }
      },

      /**
       * Processes an individal warning configuration for the form from the  
       * [array of warnings]{@link module:alfresco/forms/Form#warnings} that have been configured.
       * 
       * @instance
       * @since 1.0.32
       * @param {object[]} warnings An array of warnings to push the current warning data into
       * @param {object} warningConfig The configuration for the warning
       * @param {number} index The index of the configuration in the complete array of banners
       */
      processWarnings: function alfresco_forms_Form__processWarnings(warnings, warningConfig, index) {
         if (warningConfig.message)
         {
            // Define a unique function name for handling rules evaluation for this banner 
            // (this is going to be passed to the rule config processing as a callback and we're
            // going to create the function in a moment)... prefixing with "alf" should ensure
            // that no-one creates a similarly named function...
            var fName = "alfUpdateBanner" + index;

            // Generate a unique subscription topic, this is then added to the banner configuration
            // and will be used to publish information on whether or not the banner should be displayed
            // as rules are evaluated...
            var subscriptionTopic = this.generateUuid();
            warningConfig.subscriptionTopic = subscriptionTopic;
            this[fName] = lang.hitch(this, this.updateWarnings, warningConfig);
            this.processConfig(fName, warningConfig);

            // Add the specific warning information into the array to be returned (we want to create
            // a single Warning widget, not lots of them)...
            warnings.push({
               subscriptionTopic: subscriptionTopic,
               message: warningConfig.message,
               level: 1
            });
         }
         else
         {
            this.alfLog("warn", "Warning configuration for the form was missing a 'message' to display in the warning", warningConfig, this);
         }
      },

      /**
       * This function is called whenever warning rules have evaluated to indicate a change in visibility
       * for a particular warning.
       * 
       * @instance
       * @since 1.0.32
       * @param {object} warningConfig The configuration for the warning
       * @param {boolean} status Indicates whether or not the warning should be displayed or not
       */
      updateWarnings: function alfresco_forms_Form__updateWarnings(warningConfig, status) {
         this.alfPublish(warningConfig.subscriptionTopic, {
            value: status
         });
      },

      /**
       * Handles the reporting of an invalid field. This will disable the "OK" button if it has
       * been created to prevent users from attempting to submit invalid data.
       * 
       * @instance
       * @param {object} payload The published details of the invalid field.
       */
      onInvalidField: function alfresco_forms_Form__onInvalidField(payload) {
         var alreadyCaptured = array.some(this.invalidFormControls, function(item) {
            return item === payload.fieldId;
         });
         if (!alreadyCaptured)
         {
            this.invalidFormControls.push(payload.fieldId);
         }
         if (this.okButton)
         {
            this.okButton.set("disabled", "true");
         }
         this.publishFormValidity();
      },

      /**
       * Published the current form validity. This is done as a courtesy for other
       * widgets that might be dependant up the current state of the form.
       *
       * @instance
       */
      publishFormValidity: function alfresco_forms_Form__publishFormValidity() {
         var isValid = this.invalidFormControls.length === 0,
             autoSavePayload;

         this.alfPublish("ALF_FORM_VALIDITY", {
            valid: isValid,
            invalidFormControls: this.invalidFormControls
         });

         if(this._formSetupComplete === true && 
            this._readyToAutoSave === true &&
            this.autoSavePublishTopic && 
            typeof this.autoSavePublishTopic === "string" && 
            (isValid || this.autoSaveOnInvalid)) {

            autoSavePayload = lang.mixin(this.autoSavePublishPayload || {}, {
               alfValidForm: isValid
            });

            $.extend(true, autoSavePayload, this.getValue());

            this.alfPublish(this.autoSavePublishTopic, autoSavePayload, this.autoSavePublishGlobal);
         }
      },
      
      /**
       * Handles the reporting of a valid field. If the field was previously recorded as being
       * invalid then it is removed from the [invalidFormControls]{@link module:alfresco/forms/Form#invalidFormControls}
       * attribute and it was the field was the only field in error then the "OK" button is 
       * enabled. 
       * 
       * @instance
       * @param {object} payload The published details of the field that has become valid
       */
      onValidField: function alfresco_forms_Form__onValidField(payload) {
         this.invalidFormControls = array.filter(this.invalidFormControls, function(item) {
            return item !== payload.fieldId;
         });
         if (this.okButton)
         {
            this.okButton.set("disabled", this.invalidFormControls.length > 0);
            // See AKU-275: This is required to ensure that the actual "disabled" attribute is set correctly
            this.okButton.domNode.firstChild.firstChild.setAttribute("disabled", this.invalidFormControls.length > 0);
         }
         this.publishFormValidity();

         // Update the publishPayload of the "OK" button so that when it is clicked
         // it will provide the current form data...
         var formValue = this.getValue();
         array.forEach(this.additionalButtons, function(button) {
            if (!button.updatePayload)
            {
               // No action required, leave the payload as is
            }
            else if (button._alfOriginalButtonPayload)
            {
               var newPayload = {};
               lang.mixin(newPayload, button._alfOriginalButtonPayload, formValue);
               button.publishPayload = newPayload;
            }
            else
            {
               button.publishPayload = formValue;
            }
         });

         if (this.widgetProcessingComplete) {
            this.publishValidValue();
         }
      },

      /**
       * This topic will be published when the form contains valid data
       *
       * @instance
       * @type {string}
       * @default
       */
      validFormValuesPublishTopic: null,

      /**
       * The payload of validFormValuesPublishTopic. If left as null the form's value will be dispatched.
       *
       * @instance
       * @type {object}
       * @default
       */
      validFormValuesPublishPayload: null,

      /**
       * Set to true if a the validFormValuesPublishTopic shall be globally published.
       *
       * @instance
       * @type {string}
       * @default
       */
      validFormValuesPublishGlobal: false,

      /**
       * If set to true the form will fire an event when it has been processed (as long as all values are valid)
       *
       * @instance
       * @type {boolean}
       * @default
       */
      validFormValuesPublishOnInit: false,

      /**
       * Indicates whether or not the "OK" button should be displayed or not.
       * 
       * @instance
       * @type {boolean}
       * @default
       */
      showOkButton: true,
      
      /**
       * Indicates whether or not the "Cancel" button should be displayed or not.
       * 
       * @instance
       * @type {boolean}
       * @default
       */
      showCancelButton: true,
      
      /**
       * The label that will be used for the "OK" button. This value can either be an explicit
       * localised value or an properties key that will be used to retrieve a localised value.
       * 
       * @instance
       * @type {string}
       * @default
       */
      okButtonLabel: "form.button.ok.label",
      
      /**
       * Additional CSS clases to apply to the confirmation button on the form. This defaults to
       * have the "call-to-action" style, but this can be overridden as required.
       * 
       * @instance
       * @type {string}
       * @default
       * @since 1.0.33
       */
      okButtonClass: "call-to-action",

      /**
       * @instance 
       * @type {string}
       * @default
       */
      okButtonPublishTopic: null,
      
      /**
       * @instance
       * @type {object}
       * @default
       */
      okButtonPublishPayload: null,
      
      /**
       * @instance 
       * @type {string}
       * @default
       */
      okButtonPublishGlobal: null,

      /**
       * The label that will be used for the "OK" button after a publish. It will return to the configured label
       * after [okButtonPublishRevertSecs]{@link module:alfresco/forms/Form#okButtonPublishRevertSecs} seconds
       * (but also see [okButtonDisableOnPublish]{@link module:alfresco/forms/Form#okButtonDisableOnPublish}).
       *
       * @instance
       * @type {string}
       * @default
       * @since 1.0.44
       */
      okButtonPublishedLabel: "form.button.ok.label.published",

      /**
       * <p>When the OK button has been pushed, the label will changed to the [published-label]{@link module:alfresco/forms/Form#okButtonPublishedLabel}
       * and will also disable if [okButtonDisableOnPublish]{@link module:alfresco/forms/Form#okButtonDisableOnPublish} is set to true. Unless the
       * [okButtonEnablementTopics property]{@link module:alfresco/alfresco/forms/Form#okButtonEnablementTopics} has been provided, both changes will
       * automatically be reverted after this property's value in seconds has passed.</p>
       *
       * <p>See [this comment from UX]{link https://issues.alfresco.com/jira/browse/AKU-683?focusedCommentId=425326&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-425326} for the reason for this property.</p>
       *
       * @instance
       * @type {number}
       * @default
       * @since 1.0.44
       */
      okButtonPublishRevertSecs: 0,

      /**
       * Whether to disable the OK button after a publish. If this is set to true then the
       * [published-label]{@link module:alfresco/forms/Form#okButtonPublishedLabel} will
       * remain in-place until the button is re-enabled by use of either the
       * [okButtonEnablementTopics]{@link module:alfresco/alfresco/forms/Form#okButtonEnablementTopics}
       * or [okButtonReenableSecs]{@link module:alfresco/alfresco/forms/Form#okButtonReenableSecs}
       * properties.
       *
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.44
       */
      okButtonDisableOnPublish: false,

      /**
       * If [okButtonDisableOnPublish]{@link module:alfresco/forms/Form#okButtonDisableOnPublish} has been set to true
       * and this property is provided then the button will remain disabled until one of these topics has been published
       * (scoped as necessary). The array should contain either strings for unconditional re-enablement if that topic
       * is published or [rule objects]{@link module:alfresco/util/objectUtils#Rules} with an additional topic attribute
       * that defines the topic aname to conditionally subscribe to.
       *
       * @instance
       * @type {string[]|object[]}
       * @default
       * @since 1.0.44
       */
      okButtonEnablementTopics: null,

      /**
       * If any of these topics are published then publish this form using the
       * [okButtonPublishTopic]{@link module:alfresco/forms/Form#okButtonPublishTopic}.
       * One may often wish to use [topics.ENTER_KEY_PRESSED]{@link module:alfresco/core/topics#ENTER_KEY_PRESSED}
       * as a value in this array, to cause the form to submit when ENTER is pressed.
       *
       * @instance
       * @type {string[]}
       * @default
       * @since 1.0.49
       */
      publishValueSubscriptions: null,

      /**
       * The label that will be used for the "Cancel" button. This value can either be an explicit
       * localised value or an properties key that will be used to retrieve a localised value.
       *
       * @instance
       * @type {string}
       * @default
       */
      cancelButtonLabel: "form.button.cancel.label",
      
      /**
       * @instance
       * @type {string}
       * @default
       */
      cancelButtonPublishTopic: null,
      
      /**
       * @instance
       * @type {object}
       * @default
       */
      cancelButtonPublishPayload: null,

      /**
       * @instance
       * @type {object}
       * @default
       */
      cancelButtonPublishGlobal: null,
      
      /**
       * <p>If this is not null, then the form will auto-publish on this topic whenever a form's
       * values change. This setting overrides (and removes) the form buttons so that they are 
       * no longer displayed. Auto-saving will <strong>not</strong> occur if the form is in an
       * invalid state unless [autoSaveOnInvalid]{@link module:alfresco/forms/Form#autoSaveOnInvalid}
       * is configured to be true.</p>
       * 
       * <p><strong>PLEASE NOTE:</strong> If the form is being dynamically created after page load has completed 
       * (e.g. if the form is being displayed in a dialog for example) then in order for auto-saving to occur 
       * it will also be necessary to configure the 
       * [waitForPageWidgets]{@link module:alfresco/forms/Form#waitForPageWidgets} attribute to be false.</p>
       * 
       * @instance 
       * @type {string}
       * @default
       */
      autoSavePublishTopic: null,
      
      /**
       * Additional data to be published on the 
       * [autoSavePublishTopic]{@link module:alfresco/forms/Form#autoSavePublishTopic} 
       * along with the form value.
       * 
       * @instance
       * @type {object}
       * @default
       */
      autoSavePublishPayload: null,
      
      /**
       * Indicates that the [autoSavePublishTopic]{@link module:alfresco/forms/Form#autoSavePublishTopic}
       * should be published on the global scope.
       * 
       * @instance 
       * @type {string}
       * @default
       */
      autoSavePublishGlobal: null,

      /**
       * When [autoSavePublishTopic]{@link module:alfresco/forms/Form#autoSavePublishTopic} is
       * configured this attribute can be set to true to indicate that publishing should also occur
       * when the form contains invalid values. If this is enabled, then the publish payload will 
       * have an additional alfFormInvalid property, which will be set to true.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      autoSaveOnInvalid: false,
      
      /**
       * This can be configured with details of additional buttons to be included with the form.
       * Any button added will have the publishPayload set with the form value. 
       * 
       * @instance
       * @type {object[]}
       * @default
       */
      widgetsAdditionalButtons: null,
      
      /**
       * If more than just "OK" and "Cancel" buttons are required then this can be configured to be 
       * an array of configurations for extra buttons to be displayed. The payload of these buttons will
       * always be augmented with the form values.
       *
       * @instance
       * @type {object[]}
       * @default
       */
      additionalButtons: null,
      
      /**
       * When this is set to true the current URL hash fragment will be used to initialise the form value
       * and when the form is submitted the hash fragment will be updated with the form value. If you intend
       * to use hashing it is recommended that the [okButtonPublishTopic]{@link module:alfresco/forms/Form#okButtonPublishTopic}
       * does not directly result in submission of data, but rather submission of data is triggered by changes
       * to the hash.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      useHash: false,

      /**
       * When this is set to true the URL hash fragment will be updated when the form is published.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      setHash: false,
      
      /**
       * This attribute is used to indicate whether or not the form has completed setting up all of the controls it has been 
       * configured with.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      _formSetupComplete: false,

      /**
       * This function is called when [useHash]{@link module:alfresco/forms/Form#useHash} is set to true
       * and the OK button is clicked to publish the form data. It will take the value of the form and
       * use it to set a hash fragment.
       * 
       * @instance
       * @param {object} payload The form data to set
       */
      setHashFragment: function alfresco_forms_Form__setHashFragment(payload) {
         // Make sure to remove the alfTopic from the payload (this will always be assigned on publications
         // but is not actually part of the form data)...
         this.alfCleanFrameworkAttributes(payload, true);
         
         // Make sure that only the controls with names listed in hashVarsForUpdate are set on the URL hash...
         var updatedHash = {};
         this.payloadContainsUpdateableVar(payload, true, updatedHash);

         var currHash = hashUtils.getHash();
         lang.mixin(currHash, updatedHash);
         var hashString = ioQuery.objectToQuery(currHash);

         // Publish so that the NavigationService can set the hash fragment...
         this.alfPublish("ALF_NAVIGATE_TO_PAGE", {
            url: hashString,
            type: "HASH"
         }, true);
      },

      /**
       * Overrides the implementation from [_AlfHashMixin]{@link module:alfresco/documentlibrary/_AlfHashMixin}
       * which was written to publish Document Library specific topics. This version responds to the hash change
       * by setting the form value.
       * 
       * @instance
       * @param {object} payload The publication topic.
       */
      onHashChange: function alfresco_forms_Form__onHashChange(hashString) {
         if (this.useHash === true)
         {
            var currHash = ioQuery.queryToObject(hashString);
            var updatedFormValue = {};
            this.doHashVarUpdate(currHash, true, updatedFormValue);
            this.setValue(updatedFormValue);
         }
      },

      /**
       * Creates the buttons for the form. This can be overridden to change the buttons that are displayed.
       * 
       * @instance
       */
      createButtons: function alfresco_forms_Form__createButtons() {
         if (this.showOkButton === true && !this.autoSavePublishTopic)
         {
            var onButtonClass = this.okButtonClass ? this.okButtonClass : "";
            this.okButton = new AlfButton({
               pubSubScope: this.pubSubScope,
               label: this.message(this.okButtonLabel),
               additionalCssClasses: "confirmationButton " + onButtonClass,
               publishTopic: this.okButtonPublishTopic,
               publishPayload: this.okButtonPublishPayload,
               publishGlobal: this.okButtonPublishGlobal,
               iconClass: this.okButtonIconClass
            }, this.okButtonNode);

            // Handle any post-publish actions
            if (this.okButtonPublishTopic && lang.trim(this.okButtonPublishTopic))
            {
               this.alfSubscribe(this.okButtonPublishTopic, lang.hitch(this, this.onOkButtonPublish), this.okButtonPublishGlobal);
            }
            else if (this.useHash === true && this.setHash === true)
            {
               this.alfLog("warn", "A form is configured to use the browser hash fragment, but has no okButtonPublishTopic set", this);
            }
         }
         else
         {
            domConstruct.destroy(this.okButtonNode);
         }
         
         if (this.showCancelButton === true && !this.autoSavePublishTopic)
         {
            this.cancelButton = new AlfButton({
               pubSubScope: this.pubSubScope,
               label: this.message(this.cancelButtonLabel),
               additionalCssClasses: "cancelButton",
               publishTopic: this.cancelButtonPublishTopic,
               publishPayload: this.cancelButtonPublishPayload,
               publishGlobal: this.cancelButtonPublishGlobal,
               iconClass: this.cancelButtonIconClass
            }, this.cancelButtonNode);
         }
         else
         {
            domConstruct.destroy(this.cancelButtonNode);
         }
         
         // If there are any other additional buttons to add, then process them here...
         if (this.widgetsAdditionalButtons !== null)
         {
            this.additionalButtons = [];
            this.processWidgets(this.widgetsAdditionalButtons, this.buttonsNode, "BUTTONS");
         }
         else
         {
            this.additionalButtons = registry.findWidgets(this.buttonsNode);
         }

         // Iterate over all the buttons and make a copy of the original button payload (before any value is
         // applied to it). This is required since it is possible for values to be removed from the payload
         // so the original payload needs to be used rather than the payload as it was after the previous update
         // (e.g. a field has become disabled since the last payload update and is configured to not have its value
         // included when it is hidden, therefore we need to ensure its previous value is NOT included in the payload)
         array.forEach(this.additionalButtons, function(button) {
            if (button.publishPayload && button.updatePayload)
            {
               button._alfOriginalButtonPayload = lang.clone(button.publishPayload);
            }
         });
      },

      /**
       * Execute a form publish as defined by the OK button.
       *
       * @instance
       * @since 1.0.49
       */
      submitOkButton: function alfresco_forms_Form__submitOkButton() {
         if(!this.okButton) 
         {
            this.alfLog("warn", "Cannot submit OK button as button not defined");

            // See AKU-1116
            // If no buttons can be found then the likely scenario is that we are within
            // a form dialog. Therefore emit a custom event requesting that the form be
            // submitted...
            on.emit(this.domNode, "onFormSubmit", {
               bubbles: true,
               cancelable: true
            });
         }
         else 
         {
            this.okButton.activate();
         }
      },
      
      /**
       * Makes a call to the [validate]{@link module:alfresco/forms/Form#validate} function to check the initial
       * state of the form.
       * 
       * @instance
       */
      allWidgetsProcessed: function alfresco_forms_Form__allWidgetsProcessed(widgets, processWidgetsId) {
         // If additional button configuration has been processed, then get a reference to ALL the buttons...
         if (processWidgetsId === "BUTTONS")
         {
            this.additionalButtons = registry.findWidgets(this.buttonsNode);
         }
         else
         {
            // Called only when processing the form controls, not when there are additional buttons...
            if (this.useHash)
            {
               var currHash = hashUtils.getHash();
               var updatedFormValue = {};
               this.doHashVarUpdate(currHash, true, updatedFormValue);
               this.setValue(updatedFormValue, true);
            }
            else
            {
               this.setValue(this.value || {}, true);
            }
            
            // Create an object that we're going to use to check off all the form controls as they report their
            // initial value...
            var rollCallObject = {
               count: widgets.length
            };
            
            // Once all the controls have been added to the form we're going to ask them each to
            // publish their initial value. However, in order to ensure that we don't publish before
            // their value has completely initialised (e.g. if a control with options has not been set
            // an initial value it may want to set the first option it has as the current value) which
            // may only complete after the widget has been created but BEFORE it has been placed into
            // the document. Value initialization occurs after being added to the document to avoid
            // potential XSS attacks.
            array.forEach(widgets, function(widget) {
               if (widget.publishValue && typeof widget.publishValue === "function")
               {
                  // Create a Deferred object and pass it to the widget, this *should* (if the function
                  // has not been foolishly overridden!) detect the Deferred object and only resolve it
                  // when value initialization is complete...
                  var deferred = new Deferred();
                  deferred.then(lang.hitch(widget, widget.publishValue)).then(lang.hitch(this, this.onRollCall, rollCallObject));
                  widget.publishValue(deferred);
               }
            }, this);
         }
      },

      /**
       * This function is used to register the successful setup of all the form controls. As each form control is added
       * to the page it will publish its value and after the value is published this function will "check it off" from 
       * an overall count of all the expected controls. This is done as a final check so that we can be absolutely sure
       * that everything is complete before we allow validation or auto-saving to commence.
       *
       * @instance
       * @param  {object} rollCallObject An object containing a simple count of all the form controls yet to register themselves
       */
      onRollCall: function alfresco_forms_Form__onRollCall(rollCallObject) {
         rollCallObject.count--;
         if (rollCallObject.count === 0)
         {
            this.validate();

            // If requested publish a topic now that the form has been initially processed
            if (this.validFormValuesPublishOnInit) {
               this.publishValidValue();
            }
            this._formSetupComplete = true;

            // If configured, focus the first field in the form
            if (this.firstFieldFocusOnLoad) {
               // Use setTimeout to allow other synchronous processes to complete first, as this
               // has a much greater chance of successfully focusing on the field
               setTimeout(lang.hitch(this, this.focus));
            }
         }
      },
      
      /**
       * Update all the button payloads with the supplied values
       * @instance
       */
      updateButtonPayloads: function alfresco_forms_Form__updateButtonPayloads(values) {
         array.forEach(this.additionalButtons, function(button) {
            if (!button.updatePayload)
            {
               // No action required. Leave the payload as is.
            }
            else if (button._alfOriginalButtonPayload)
            {
               var payload = {};
               lang.mixin(payload, button._alfOriginalButtonPayload, values);
               button.publishPayload = payload;
            }
            else
            {
               button.publishPayload = values;
            }
         });
      },

      /**
       * Focuses on the first field in the form.
       *
       * @instance
       */
      focus: function alfresco_forms_Form__focus() {
         if (this._form)
         {
            var children = this._form.getChildren();
            if (children.length > 0 && children[0].wrappedWidget && typeof children[0].focus === "function")
            {
               children[0].focus();
            }
         }
      },

      /**
       * @instance
       * @return {object}
       */
      getValue: function alfresco_forms_Form__getValue() {
         var values = {};
         if (this._form)
         {
            array.forEach(this._form.getChildren(), function(entry) {
               if (typeof entry.addFormControlValue === "function")
               {
                  entry.addFormControlValue(values);
               }
            });
         }
         this.alfLog("log", "Returning form values: ", values);
         this.updateButtonPayloads(values);
         return values;
      },
      
      /**
       * @instance
       * @param {object} values The values to set
       * @param {boolean} initialization Indicates whether this call is part of the initialization of the containing form
       */
      setValue: function alfresco_forms_Form__setValue(values, initialization) {
         this.alfLog("log", "Setting form values: ", values);
         if (values && values instanceof Object)
         {
            if (this._form)
            {
               array.forEach(this._form.getChildren(), function(entry) {
                  if (typeof entry.updateFormControlValue === "function")
                  {
                     entry.updateFormControlValue(values, initialization);
                  }
                  if (typeof entry.publishValue === "function")
                  {
                     entry.publishValue();
                  }
               });
            }
            this.validate();

            this.updateButtonPayloads(this.getValue());
         }
      },
      
      /**
       * @instance
       * @returns {boolean}
       */
      validate: function alfresco_forms_Form__validate() {
         this.alfLog("log", "Validating form", this._form);
         array.forEach(this._form.getChildren(), function(widget) {
            if (typeof widget.validateFormControlValue === "function")
            {
               widget.validateFormControlValue();
            }
         });

         // The form is valid if there are no invalid form controls...
         return this.invalidFormControls.length === 0;
      },

      /**
       * @instance
       */
      publishValidValue: function alfresco_forms_Form__publishValidValue() {
         // The form is valid if there are no invalid form controls...
         if (this.invalidFormControls.length === 0 && this.validFormValuesPublishTopic)
         {
            var payload = this.validFormValuesPublishPayload;
            if (!payload)
            {
               payload = this.getValue();
            }
            this.alfPublish(this.validFormValuesPublishTopic, payload, this.validFormValuesPublishGlobal);
         }
      },

      /**
       * This function is used to forward on options requests from controls contained within the form. It
       * can be used by setting the "publishTopic" in the "optionsConfig" to be
       * [GET_FORM_VALUE_DEPENDENT_OPTIONS]{@link module:alfresco/core/topics#GET_FORM_VALUE_DEPENDENT_OPTIONS}.
       * The "publishPayload" in the "optionsConfig" should contain a "publishTopic" attribute that the 
       * request should ultimately be forwarded on to. The reason to use this topic is that the forwarded
       * payload will be updated to include the current form value. This makes it possible for options to
       * be generated that are dependant upon changing form values.
       * 
       * @instance
       * @param  {object} payload The payload requesting the options.
       * @since 1.0.39
       */
      getFormValueDependantOptions: function alfresco_forms_Form__getFormValueDependantOptions(payload) {
         if (payload.publishTopic && payload.publishTopic !== topics.GET_FORM_VALUE_DEPENDENT_OPTIONS)
         {
            var currentValue = this.getValue();
            
            var clonedPayload = lang.clone(payload);
            if (clonedPayload.publishPayloadModifiers)
            {
               // Set the current value in order to support the "processInstanceTokens" modifier... 
               this.value = currentValue;
               this.processObject(clonedPayload.publishPayloadModifiers, clonedPayload);
               delete clonedPayload.publishPayloadModifiers;
            }

            // Mix the current value into the payload...
            lang.mixin(clonedPayload, currentValue);
            this.alfServicePublish(payload.publishTopic, clonedPayload);
         }
      },

      /**
       * If an [okButtonPublishTopic]{@link module:alfresco/forms/Form#okButtonPublishTopic} has been provided
       * then this function will be run after it has been published.
       *
       * @instance
       * @param {object} payload The published payload
       * @since 1.0.44
       */
      onOkButtonPublish: function alfresco_forms_Form__onOkButtonPublish(payload) {

         // Set the hash fragment with the form contents...
         if (this.useHash === true && this.setHash === true) {
            this.setHashFragment(payload);
         }

         // Change the button label if auto-revert or disable-on-publish are set
         if (this.okButtonPublishRevertSecs > 0 || this.okButtonDisableOnPublish) {
            this.okButton.set("label", this.message(this.okButtonPublishedLabel));
         }

         // If we should disable on publish then do so
         if (this.okButtonDisableOnPublish) {
            this.okButton.set("disabled", true);
         }

         // Unless there are enablement topics, automatically revert changes to the OK button
         if ((!this.okButtonEnablementTopics || !this.okButtonEnablementTopics.length) && this.okButtonPublishRevertSecs > 0) {
            setTimeout(lang.hitch(this, this.reenableOkButton), this.okButtonPublishRevertSecs * 1000);
         }
      },

      /**
       * Setup the subscription(s) to re-enable the OK button after it's been disabled on submission.
       *
       * @instance
       * @param {string|module:alfresco/util/objectUtils#Rules} topic The raw topic name to subscribe to, or a Rules object with an additional
       *                                                              topic attribute that defines the name of the topic to subscribe to
       * @param {string|string[]} [is] The possible value/values that will re-enable the OK button if matching the specified attribute
       * @param {string|string[]} [isNot] The disallowed value/values that will re-enable the OK button if the attribute does not match it/them
       */
      setupOkButtonEnablementSubscription: function alfresco_forms_Form__setupOkButtonEnablementSubscription(topic) {
         var topicName,
            rulesObj;
         if (typeof topic === "string") {
            topicName = topic;
            rulesObj = {};
         } else {
            topicName = topic.topic;
            rulesObj = lang.clone(topic);
            delete rulesObj.topic;
         }
         this.alfConditionalSubscribe(topicName, rulesObj, lang.hitch(this, this.reenableOkButton));
      },

      /**
       * Re-enable the OK button after it's been [disabled by publishing]{@link module:alfresco/forms/Form#okButtonDisableOnPublish}.
       * Calls the [onValidField]{@link module:alfresco/forms/Form#onValidField} method to ensure that validation rules are maintained.
       *
       * @instance
       * @since 1.0.44
       */
      reenableOkButton: function alfresco_forms_Form__reenableOkButton() {
         this.okButton.set("label", this.message(this.okButtonLabel));
         this.onValidField();
      }
   });
});