Source: services/DialogService.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 service subscribes to [ALF_CREATE_FORM_DIALOG_REQUEST]{@link module:alfresco/services/DialogService~event:ALF_CREATE_FORM_DIALOG_REQUEST}
 * and [ALF_CREATE_DIALOG_REQUEST]{@link module:alfresco/services/DialogService~event:ALF_CREATE_DIALOG_REQUEST} topics
 * and should be used to manage [dialogs]{@link module:alfresco/dialogs/AlfDialog} displayed on Aikau pages.</p>
 * <p>When creating a form dialog the value of the form will be published on the topic set by the <b>formSubmissionTopic</b>
 * attribute. You can include additional data in the published payload by using the <b>formSubmissionPayloadMixin</b> attribute.
 * You only need to include the form controls in the <b>widgets</b> attribute - the underlying [form]{@link module:alfresco/forms/Form}
 * will be created automatically.</p>
 * <p>When creating a normal dialog any buttons included in the dialog will have its payload updated to include a
 * <b>dialogContent</b> attribute that will be an array containing all the root widgets that were created from the
 * <b>widgetsContent</b> attribute.</p>
 * <p>It is strongly recommended that you include a <b>dialogId</b> attribute when requesting to create either type of dialog since
 * it is used to help the service manage the dialogs. It is only possible to stack dialogs (e.g. display one dialog on top
 * of another) when the dialogs have different <b>dialogId</b> values. If another dialog with the same <b>dialogId</b> then any existing
 * dialog with that same <b>dialogId</b> will be destroyed. This is done to ensure that the browser DOM does not become
 * over populated with unusable elements</p>
 * <p>It is also possible to create form dialogs that follow the "Create and create again" pattern by configuring the
 * "dialogRepeats" attribute in the [ALF_CREATE_FORM_DIALOG_REQUEST]{@link module:alfresco/services/DialogService~event:ALF_CREATE_FORM_DIALOG_REQUEST}
 * to be true. This will include an additional button in the dialog that when used will publish the value of the current form
 * and then recreate a new dialog with a new copy of the form.</p>
 * <p>In order to be notified when a dialog is cancelled by either clicking on the cross in the top corner, or by pressing escape,
 * a "cancelPublishTopic" can be supplied. This will never be published by someone clicking a "cancel" button - it will only be
 * triggered using one of the two methods above. Scope can be overridden from the default response scope by providing a custom
 * "cancelPublishScope" property. This can be used on normal dialogs and form dialogs.</p>
 *
 * @example <caption>Example button that requests a form dialog</caption>
 * {
 *    name: "alfresco/buttons/AlfButton",
 *    config: {
 *       label: "Display form dialog",
 *       publishTopic: "ALF_CREATE_FORM_DIALOG_REQUEST",
 *       publishPayload: {
 *          cancelPublishScope: "",
 *          cancelPublishTopic: "DIALOG_CANCELLED",
 *          dialogId: "NAME_DIALOG",
 *          dialogTitle: "User name",
 *          dialogConfirmationButtonTitle: "Save",
 *          dialogCancellationButtonTitle: "Quit",
 *          formSubmissionTopic: "MY_CUSTOM_TOPIC",
 *          formSubmissionPayloadMixin: {
 *             extra: "bonus data"
 *          },
 *          formValue: {
 *             name: "Bob"
 *          },
 *          widgets: [
 *             {
 *                name: "alfresco/forms/controls/TextBox",
 *                config: {
 *                   name: "name",
 *                   label: "Name?"
 *                   description: "Please enter your name"
 *                }
 *             }
 *          ]
 *       }
 *    }
 * }
 *
 * @example <caption>Example button that requests basic status dialog</caption>
 * {
 *    name: "alfresco/buttons/AlfButton",
 *    config: {
 *       label: "Alert",
 *       publishTopic: "ALF_CREATE_DIALOG_REQUEST",
 *       publishPayload: {
 *          dialogId: "ALERT_DIALOG",
 *          dialogTitle: "Warning!",
 *          textContent: "You have been warned"
 *       }
 *    }
 * }
 *
 * @example <caption>Example button that requests a dialog with a custom widget model</caption>
 * {
 *    name: "alfresco/buttons/AlfButton",
 *    config: {
 *       label: "Show Logo",
 *       publishTopic: "ALF_CREATE_DIALOG_REQUEST",
 *       publishPayload: {
 *          dialogId: "LOGO_DIALOG",
 *          dialogTitle: "A logo",
 *          widgetsContent: [
 *             {
 *                name: "alfresco/logo/Logo"
 *             }
 *          ],
 *          widgetsButtons: [
 *             {
 *                name: "alfresco/buttons/AlfButton",
 *                config: {
 *                   label: "Close dialog",
 *                   publishTopic: "CUSTOM_TOPIC"
 *                }
 *             }
 *          ]
 *       }
 *    }
 * }
 *
 * @module alfresco/services/DialogService
 * @extends module:alfresco/services/BaseService
 * @pageSafe
 * @author Dave Draper
 * @author David Webster
 */

/**
 * Create a dialog that contains a form
 *
 * @event module:alfresco/services/DialogService~ALF_CREATE_FORM_DIALOG_REQUEST
 * @property {string} dialogTitle - The text to set in the dialog title bar
 * @property {number} [duration=0] - The duration of the fade effect when showing or hiding the dialog
 * @property {string} formSubmissionTopic - The topic to publish when the confirmation button is used (the published payload will be the form value)
 * @property {Object} formSubmissionPayloadMixin - An additional object to "mixin" to the form value before it is published
 * @property {boolean} [formSubmissionGlobal=true] - Whether or not to publish the form submission topic globally
 * @property {string} [formSubmissionScope=null] - A custom scope to be used when publishing the form submission topic
 * @property {Object} [formValue={}] - The initial value to apply to the form when created. This should be an object with attributes mapping to the "name" attribute of each form control.
 * @property {string} [dialogId=null] The ID of the dialog to display. Only one dialog with no dialogId can exist on a page at a time, therefore it is sensible to always include an id for your dialogs to allow stacking.
 * @property {boolean} [dialogRepeats=false] Indicates that an additional button the publishes the current form and then recreates the dialog again
 * @property {string} [dialogConfirmationButtonTitle="OK"] - The label for the dialog confirmation button
 * @property {string} [dialogConfirmAndRepeatButtonTitle="OK (and repeat)"] The label for the button indicating the dialog should be repeated
 * @property {string} [dialogCancellationButtonTitle="Cancel"] - The label for the dialog cancellation button
 * @property {string} [dialogCloseTopic=null] If this is set then the dialog will not automatically be closed when the confirmation button is pressed. Instead the dialog will remain open until this topic is published on.
 * @property {string} [dialogEnableTopic=null] If this is set then the dialog buttons will automatically be disabled when the form is submitted. When this topic is published, the buttons will be re-enabled. This topic should 
 * be published on submission faiure.
 * @property {array} [widgets=null] - An array of form controls to include in the dialog
 * @property {string} [dialogWidth=null] The width to make the dialog panel (needs to include units, e.g. "px")
 * @property {string} [contentWidth=null] - The width to set the dialog body (needs to include units, e.g. "px")
 * @property {string} [contentHeight=null] - The height to set the dialog body (needs to include units, e.g. "px")
 * @property {boolean} [handleOverflow=false] - Should scrollbars be added to if the content is bigger than the dialog
 * @property {boolean} [fixedWidth=false] - If set to true, prevents the dialog resizing to fit the content
 * @property {string} [hideTopic=null] - Topic to subscribe to to trigger a dialog hide. If this is set
 * @property {boolean} [fullScreenMode=false] Whether or not to create the dialog the size of the screen
 * @property {boolean} [fullScreenPadding=10] The padding to leave around the dialog when in full screen mode
 * @property {boolean} [showValidationErrorsImmediately=true] Indicates whether or not to display form errors immediately
 * @property {object} [customFormConfig=null] Any additional configuration that can be applied to a [Form]{@link module:alfresco/forms/Form} (please note that the following form configuration
 * attributes will always be overridden by specific form dialog configuration: "additionalCssClasses", "displayButtons", "widgets", "value", "warnings" and "warningsPosition")
 * @property {boolean} [noMinWidth=false] Indicates whether the minimum width restriction should be lifted
 */

/**
 * Create a dialog containing either widgets or text with and configurable buttons.
 *
 * @event module:alfresco/services/DialogService~ALF_CREATE_DIALOG_REQUEST
 * @property {string} dialogTitle - The text to set in the dialog title bar
 * @property {string} [dialogId=null] The ID of the dialog to display. Only one dialog with no dialogId can exist on a page at a time, therefore it is sensible to always include an id for your dialogs to allow stacking.
 * @property {number} [duration=0] - The duration of the fade effect when showing or hiding the dialog
 * @property {string} [textContent=null] - Text to display in the dialog body
 * @property {array} [widgetsContent=null] - A widget model to display in the dialog body (this supercedes any textContent attribute)
 * @property {Array} [widgetsButtons=null] - A widget model of buttons to display in the dialog footer
 * @property {String} [dialogWidth=null] The width to make the dialog panel (needs to include units, e.g. "px")
 * @property {String} [contentWidth=null] - The width to set the dialog body (needs to include units, e.g. "px")
 * @property {String} [contentHeight=null] - The height to set the dialog body (needs to include units, e.g. "px")
 * @property {Boolean} [handleOverflow=false] - Should scrollbars be added to if the content is bigger than the dialog
 * @property {Boolean} [fixedWidth=false] - If set to true, prevents the dialog resizing to fit the content
 * @property {Array} [publishOnShow=null] - An array of publications objects to make when the dialog is displayed
 * @property {boolean} [fullScreenMode=false] Whether or not to create the dialog the size of the screen
 * @property {boolean} [fullScreenPadding=10] The padding to leave around the dialog when in full screen mode
 * @property {boolean} [noMinWidth=false] Indicates whether the minimum width restriction should be lifted
 */

define(["dojo/_base/declare",
        "alfresco/services/BaseService",
        "alfresco/core/topics",
        "alfresco/dialogs/AlfDialog",
        "alfresco/forms/Form",
        "dojo/_base/lang",
        "dojo/_base/array",
        "dojo/aspect",
        "dojo/dom-class",
        "dojo/dom-style",
        "dojo/on",
        "dojo/keys",
        "dojo/when",
        "jquery"],
        function(declare, BaseService, topics, AlfDialog, AlfForm, lang, array, aspect, domClass, domStyle, on, keys, when, $) {

   return declare([BaseService], {

      /**
       * An array of the i18n files to use with this widget.
       *
       * @instance
       * @type {object[]}
       * @default [{i18nFile: "./i18n/DialogService.properties"}]
       */
      i18nRequirements: [{i18nFile: "./i18n/DialogService.properties"}],

      /**
       * The topic published when the confirmation button is used.
       * 
       * @instance
       * @type {string}
       * @default
       */
      _formConfirmationTopic: "ALF_CREATE_FORM_DIALOG_MIXIN_CONFIRMATION_TOPIC",

      /**
       * The topic published when the button confirming the current dialog, but requesting to
       * repeat the dialog is used.
       * 
       * @instance
       * @type {string}
       * @default 
       */
      _formConfirmationRepeatTopic: "ALF_CREATE_REPEATING_FORM_DIALOG_MIXIN_CONFIRMATION_TOPIC",

      /**
       * The topic published when the cancellation button is used.
       * 
       * @instance
       * @type {string}
       * @default 
       */
      _formCancellationTopic: "ALF_CLOSE_DIALOG",

      /**
       * The default configuration for form dialogs. This is used as a base when requests are received.
       *
       * @instance
       * @type {object}
       */
      defaultFormDialogConfig: {
         dialogTitle: "",
         dialogConfirmationButtonTitle: "dialogService.formDialog.ok.button.label",
         dialogCancellationButtonTitle: "dialogService.formDialog.cancel.button.label",
         dialogConfirmAndRepeatButtonTitle: "dialogService.formDialog.repeat.button.label"
      },

      /**
       * This is the topic that will be published when the dialog is "confirmed" (e.g. the "OK" button is clicked)
       *
       * @instance
       * @type {string}
       * @default
       */
      formSubmissionTopic: null,
      
      /**
       * This is used to map the dialog created on for each request against the "dialogId" associated with that request.
       * Only one instance of a dialog with the same "dialogId" can exist at one time. If a new request is made with
       * the same "dialogId" as has been used for an existing dialog, then that existing dialog will be destroyed.
       *
       * @instance
       * @type {object}
       * @default
       */
      idToDialogMap: null,

      /**
       * This us used to map all the subscription and aspect handles created for each requested dialog against the
       * "dialogId" associated with the request to create that dialog. This is done so that all handles can be removed
       * when the dialog is destroyed. Each "dialogId" is associated with an object into which handles can be mapped
       * against a specific key. This allows the service to check for an existing handle before creating duplicates
       * (e.g. when handling repeating dialog requests).
       *
       * @instance
       * @type {object}
       * @default
       */
      idToHandleMap: null,

      /**
       * The configuration for the contents of the dialog to be displayed. This should be provided either on instantiation
       * or by the widget that mixes this module in
       *
       * @instance
       * @type {object}
       * @default
       */
      widgets: null,

      /**
       * The stack of active dialogs
       *
       * @instance
       * @static
       * @type {Object[]}
       */
      _activeDialogs: [],

      /**
       * The scrollbar width for this browser-environment
       *
       * @instance
       * @type {number}
       * @default
       * @since 1.0.53
       */
      _scrollbarWidth: null,

      /**
       * @instance
       * @listens module:alfresco/services/DialogService~event:ALF_CREATE_FORM_DIALOG_REQUEST
       * @listens module:alfresco/services/DialogService~event:ALF_CREATE_DIALOG_REQUEST
       * @listens module:alfresco/core/topics#DIALOG_CHANGE_TITLE
       * @since 1.0.32
       */
      registerSubscriptions: function alfresco_services_DialogService__registerSubscriptions() {
         // Generate a new pub/sub scope for the widget (this will intentionally override any other settings
         // to contrain communication...
         this.publishTopic = topics.CREATE_FORM_DIALOG;
         this.alfSubscribe(this.publishTopic, lang.hitch(this, this.onCreateFormDialogRequest));
         this.alfSubscribe(topics.CREATE_DIALOG, lang.hitch(this, this.onCreateDialogRequest));
         this.alfSubscribe(topics.DIALOG_CHANGE_TITLE, lang.hitch(this, this.changeDialogTitle));

         // Create a reference of IDs to dialogs...
         // The idea is that we shouldn't have multiple instances of a dialog with the same ID, but we
         // can have multiple dialogs with different IDs...
         this.idToDialogMap = {};
         this.idToHandleMap = {};

         // We need to make sure the escape key closes our last-opened dialog, so we must listen
         // at the body level
         on(document.body, "keydown", lang.hitch(this, function(evt) {
            if (evt && evt.keyCode === keys.ESCAPE) {
               this._handleEscape();
            }
         }));
      },

      /**
       * Change the title of the current (i.e. top-most) dialog.
       *
       * @instance
       * @param {object} payload The payload
       */
      changeDialogTitle: function alfresco_services_DialogService__changeDialogTitle(payload) {
         var newTitle = payload.title,
            dialogs = this._activeDialogs,
            topDialog = dialogs.length && dialogs[dialogs.length - 1];
         if (newTitle && topDialog) {
            topDialog.setTitle(newTitle);
         }
      },

      /**
       * This deletes any previously created dialog that was requested for the same id.
       *
       * @instance
       * @param {object} payload The payload for the new dialog request.
       */
      cleanUpAnyPreviousDialog: function alfresco_services_DialogService__cleanUpPreviousDialog(payload) {
         if (this.idToDialogMap[payload.dialogId] &&
             typeof this.idToDialogMap[payload.dialogId].destroyRecursive === "function")
         {
            // We have a reference to an existing dialog, so we'll destroy it
            this.idToDialogMap[payload.dialogId].destroyRecursive();
            delete this.idToDialogMap[payload.dialogId];

            this.cleanUpDialogHandles(this.idToHandleMap[payload.dialogId]);
            delete this.idToHandleMap[payload.dialogId];
         }
         else if (this.idToDialogMap[null] &&
                  typeof this.idToDialogMap[null].destroyRecursive === "function")
         {
            this.idToDialogMap[null].destroyRecursive();
            delete this.idToDialogMap[null];

            this.cleanUpDialogHandles(this.idToHandleMap[null]);
            delete this.idToHandleMap[null];
         }
      },

      /**
       * Cleans up any subscription or aspect handles created for a dialog that has been destroyed.
       *
       * @instance
       * @param  {object} map A map of handles to remove
       */
      cleanUpDialogHandles: function alfresco_services_DialogService__cleanUpDialogHandles(map) {
         for (var key in map)
         {
            if (map.hasOwnProperty(key))
            {
               if (typeof map[key].remove === "function")
               {
                  // NOTE: In some respects we shouldn't do this, we should probably go via alfUnsubscribe
                  //       but in this specific case it is better to explicitly deal with the handle
                  //       Should we every swap out Dojo subscription handling then we will need to deal 
                  //       with aspects and subscription handles separately
                  map[key].remove();
               }
            }
         }
      },

      /**
       * Maps the id requested for the dialog to the dialog created so that it can be destroyed when a
       * request is made to create a dialog with the same id. If an id has not been requested then the
       * dialog will be mapped to null. Only one dialog is no requested id can exist at any one time.
       *
       * @instance
       * @param {object} payload The payload passed when requesting to create the dialog
       * @param {object} dialog The dialog created
       */
      mapRequestedIdToDialog: function alfresco_services_DialogService__mapRequestedIdToDialog(payload, dialog) {
         if (payload.dialogId)
         {
            // Map the dialog id to the dialog (so that it can be destroyed if another is requested)...
            this.idToDialogMap[payload.dialogId] = dialog;
         }
         else
         {
            // If no id was provided for the dialog we'll store it against null...
            this.idToDialogMap[null] = dialog;
         }
      },

      /**
       * Maps a handle against a specific handle id within the map of dialogId to handles.
       * 
       * @param  {object} payload The create dialog request payload
       * @param  {string} id The id to map the handle against
       * @param  {object} handle A subscription or aspect handle
       */
      mapRequestedIdToHandle: function alfresco_services_DialogService__mapRequestedIdToHandles(payload, id, handle) {
         var dialogId = payload.dialogId || null;
         var map = this.idToHandleMap[dialogId];
         if (!map)
         {
            map = {};
            this.idToHandleMap[dialogId] = map;
         }
         map[id] = handle;
      },

      /**
       * Handles requests to create basic dialogs.
       *
       * @instance
       * @param {module:alfresco/services/DialogService~event:ALF_CREATE_DIALOG_REQUEST} payload The details of the widgets and buttons for the dialog
       */
      onCreateDialogRequest: function alfresco_services_DialogService__onCreateDialogRequest(payload) {
         // jshint maxcomplexity:false
         this.cleanUpAnyPreviousDialog(payload);

         var handleOverflow = true;
         if (payload.handleOverflow === false)
         {
            handleOverflow = false;
         }
         var fixedWidth = false;
         if (payload.fixedWidth === true)
         {
            fixedWidth = true;
         }

         // In general we want to clone the models in payloads to ensure that model data cannot
         // be updated and then re-used, however we need to support a specific instance for the
         // UploadService where non-cloning is relied upon...
         var widgetsContent = payload.widgetsContent;
         var widgetsButtons = payload.widgetsButtons;
         if (typeof payload.cloneModels === "undefined" || payload.cloneModels)
         {
            widgetsContent = lang.clone(payload.widgetsContent);
            widgetsButtons = lang.clone(payload.widgetsButtons);
         }

         // TODO: Update this and other function with scroll setting...
         var dialogConfig = {
            id: payload.dialogId ? payload.dialogId : this.generateUuid(),
            title: this.encodeHTML(this.message(payload.dialogTitle || "")),
            content: payload.textContent,
            duration: payload.duration || 0,
            fullScreenMode: payload.fullScreenMode || false,
            fullScreenPadding: !isNaN(payload.fullScreenPadding) ? payload.fullScreenPadding : 10,
            widgetsContent: widgetsContent,
            widgetsButtons: widgetsButtons,
            additionalCssClasses: payload.additionalCssClasses ? payload.additionalCssClasses : "",
            dialogWidth: payload.dialogWidth || null,
            contentWidth: payload.contentWidth ? payload.contentWidth : null,
            contentHeight: payload.contentHeight ? payload.contentHeight : null,
            handleOverflow: handleOverflow,
            fixedWidth: fixedWidth,
            noMinWidth: !!payload.noMinWidth
         };

         // Ensure that text content is center aligned (see AKU-368)...
         if (dialogConfig.content)
         {
            dialogConfig.additionalCssClasses += " alfresco-dialogs-AlfDialog--textContent";
         }

         var dialog = this.createDialog(dialogConfig);

         if (payload.publishOnShow)
         {
            array.forEach(payload.publishOnShow, lang.hitch(this, this.publishOnShow));
         }
         this.mapRequestedIdToDialog(payload, dialog);
         this._showDialog(payload, dialog);

         if (payload.hideTopic)
         {
            var handle = this.alfSubscribe(payload.hideTopic, lang.hitch(dialog, dialog.hide));
            this.mapRequestedIdToHandle(payload, "dialog.hide", handle);
         }
      },

      /**
       * Creates and returns a [dialog]{@link module:alfresco/dialogs/AlfDialog} using
       * the configuration provided.
       * 
       * @instance
       * @parameter {object} config The configuration to use to create the dialog
       * @since 1.0.96
       */
      createDialog: function alfresco_services_DialogService__createDialog(config) {
         return new AlfDialog(config);
      },

      /**
       * This function is called when the request to create a dialog includes publication data
       * to be performed when the dialog is displayed.
       *
       * @instance
       * @param {object} publication The publication configuration
       */
      publishOnShow: function alfresco_services_DialogService__publishOnShow(publication) {
         // TODO: Defensive coding, global/parent scope arg handling...
         if (publication.publishTopic)
         {
            this.alfPublish(publication.publishTopic, publication.publishPayload, !!publication.publishGlobal);
         }
         else
         {
            this.alfLog("warn", "A request was made to publish data when a dialog is loaded, but the topic was missing", publication, this);
         }
      },

      /**
       * Handles requests to create the [dialog]{@link module:alfresco/dialogs/AlfDialog} containining a
       * [form]{@link module:alfresco/forms/Form}. It will delete any previously created dialog (to ensure
       * no stale data is displayed) and create a new dialog containing the form defined.
       *
       * @instance
       * @param {module:alfresco/services/DialogService~event:ALF_CREATE_FORM_DIALOG_REQUEST} payload The payload published on the request topic.
       */
      onCreateFormDialogRequest: function alfresco_services_DialogService__onCreateFormDialogRequest(payload) {
         // jshint maxstatements:false, maxcomplexity:false
         this.cleanUpAnyPreviousDialog(payload);
         if (!payload.widgets)
         {
            this.alfLog("warn", "A request was made to display a dialog but no 'widgets' attribute has been defined", payload, this);
         }
         else if (!payload.formSubmissionTopic)
         {
            this.alfLog("warn", "A request was made to display a dialog but no 'formSubmissionTopic' attribute has been defined", payload, this);
         }
         else
         {
            try
            {
               // Create a new pubSubScope just for this request (to allow multiple dialogs to behave independently)...
               var pubSubScope = payload.pubSubScope || this.generateUuid();
               var subcriptionTopic =  pubSubScope + this._formConfirmationTopic;
               var confirmationHandle = this.alfSubscribe(subcriptionTopic, lang.hitch(this, this.onFormDialogConfirmation));
               this.mapRequestedIdToHandle(payload, "dialog.confirmation", confirmationHandle);

               if (payload.dialogRepeats)
               {
                  // Create a copy of the original dialog request payload, we'll need it later...
                  var repeatPayload = lang.clone(payload);
               }
               
               // Take a copy of the default configuration and mixin in the supplied config to override defaults
               // as appropriate...
               var config = lang.clone(this.defaultFormDialogConfig);
               // NOTE: Ideally we'd like to avoid cloning the payload here in case it contains native code, however
               //       we need to ensure that pubSubScopes do not get baked into the payload object that will be re-used
               //       the next time the dialog is opened. We will need to explore alternative solutions in the 5.1 timeframe
               var clonedPayload = lang.clone(payload);
               lang.mixin(config, clonedPayload);
               config.pubSubScope = pubSubScope;
               config.parentPubSubScope = this.parentPubSubScope;
               config.subcriptionTopic = subcriptionTopic; // Include the subscriptionTopic in the configuration the subscription can be cleaned up

               // Construct the form widgets and then construct the dialog using that configuration...
               var formValue = config.formValue ? config.formValue: {};
               var formConfig = this.createFormConfig(config, formValue);
               if (config.showValidationErrorsImmediately === false)
               {
                  formConfig.config.showValidationErrorsImmediately = false;
               }
               var dialogConfig = this.createDialogConfig(config, formConfig);
               var dialog = this.createDialog(dialogConfig);
               this.mapRequestedIdToDialog(payload, dialog);
               this._showDialog(payload, dialog);

               if (config.dialogCloseTopic)
               {
                  var closeHandle = this.alfSubscribe(config.dialogCloseTopic, lang.hitch(this, this.onCloseDialog, dialog));
                  this.mapRequestedIdToHandle(payload, "dialog.close", closeHandle);
               }
               if (config.dialogEnableTopic)
               {
                  var enableHandle = this.alfSubscribe(config.dialogEnableTopic, lang.hitch(this, this.onFailedSubmission, dialog));
                  this.mapRequestedIdToHandle(payload, "dialog.enable", enableHandle);
               }
               if (payload.formSubmissionTriggerTopic)
               {
                  var triggerSubmissionHandle = this.alfSubscribe(pubSubScope + payload.formSubmissionTriggerTopic, lang.hitch(this, this.onCloseDialog, dialog));
                  this.mapRequestedIdToHandle(payload, "dialog.trigger", triggerSubmissionHandle);
               }

               if (payload.dialogRepeats)
               {
                  var repeatSubscriptionTopic = pubSubScope + this._formConfirmationRepeatTopic;
                  var repeatHandle = this.alfSubscribe(repeatSubscriptionTopic, lang.hitch(this, this.repeatFormDialogRequest, repeatPayload, dialog));
                  this.mapRequestedIdToHandle(payload, "dialog.repeat", repeatHandle);
               }
            }
            catch (e)
            {
               this.alfLog("error", "The following error occurred creating a dialog for defined configuration", e, this.dialogConfig, this);
            }
         }
      },

      /**
       * 
       * @instance
       * @param  {object} payload The original payload request
       */
      repeatFormDialogRequest: function alfresco_services_DialogService__repeatFormDialogRequest(repeatPayload, dialog, confirmationPayload) {
         if (repeatPayload.dialogCloseTopic)
         {
            // For the "error path" we want to set up a new subscription to handle the expected success publication
            // (if not previously created) and then forward the confirmationPayload on so that it can be processed...
            if (!this.idToHandleMap[repeatPayload.dialogId || null]["dialog.repeat.close"])
            {
               var handle = this.alfSubscribe(repeatPayload.dialogCloseTopic, lang.hitch(this, this._repeatFormDialogRequestAfterHide, repeatPayload, dialog));
               this.mapRequestedIdToHandle(repeatPayload, "dialog.repeat.close", handle);
            }
            this.onFormDialogConfirmation(confirmationPayload);
         }
         else
         {
            // For the "golden path" we can just publish the confirmation payload and then immediately
            // request a repeat of the dialog. It is up to the developer to ensure that they are validating
            // their form configuration to guarantee a successful publication everytime...
            this.onFormDialogConfirmation(confirmationPayload);
            this._repeatFormDialogRequestAfterHide(repeatPayload, dialog);
         }
      },

      /**
       * When repeating a request for a dialog (the "Create and then create another" paradigm) it is necessary to wait
       * until the previous dialog has been completely hidden before attempting to create another one if there is a
       * fade out animation configured on the dialog.
       *
       * @instance
       * @param {object} payload A copy of the original form dialog creation request
       * @param {object} dialog The dialog that must be fully hidden before the request for a new dialog is made.
       */
      _repeatFormDialogRequestAfterHide: function alfresco_services_DialogService___repeatFormDialogRequestAfterHide(payload, dialog) {
         if (dialog._isShown())
         {
            // If the dialog is still shown then we need to set up an aspect to wait for the dialog to be hidden
            // before we attempt to recreate a dialog with the original configuration...
            var handle = aspect.after(dialog, "onHide", lang.hitch(this, this.onCreateFormDialogRequest, payload));
            this.mapRequestedIdToHandle(payload, "dialog.repeat.hide", handle);
         }
         else
         {
            // ...but if the dialog is already hidden we can just go ahead and recreate it
            this.onCreateFormDialogRequest(payload);
         }
      },

      /**
       * Closes the supplied dialog.
       *
       * @instance
       * @param {object} dialog The dialog to close.
       */
      onCloseDialog: function alfresco_services_DialogService__onCloseDialog(dialog) {
         if (typeof dialog.hide === "function")
         {
            dialog.hide();
         }
      },

      /**
       * Handles failed form submission by re-enabling the dialog buttons so that the user to submit
       * alternative data.
       * 
       * @instance
       * @param {object} dialog The dialog to enable the buttons for
       * @since 1.0.58
       */
      onFailedSubmission: function alfresco_services_DialogService___onFailedSubmission(dialog) {
         if (dialog)
         {
            when(dialog.getButtons(), lang.hitch(this, function(buttons) {
               array.forEach(buttons, function(button) {
                  button.set("disabled", false);
               });
            }));
         }
      },

      /**
       * Creates the configuration object to pass to the dialog.
       *
       * @instance
       * @param {object} config
       * @param {object} formConfig
       * @returns {object} The dialog configuration.
       */
      createDialogConfig: function alfresco_services_DialogService__createDialogConfig(config, formConfig) {
         // jshint maxcomplexity:false
         var handleOverflow = true;
         if (config.handleOverflow === false)
         {
            handleOverflow = false;
         }
         var fixedWidth = false;
         if (config.fixedWidth === true)
         {
            fixedWidth = true;
         }

         // If a specific dialogCloseTopic has been requeste then add the "confirmationButton" CSS class as
         // a value that will suppress dialog closure.
         var suppressCloseClasses = config.dialogCloseTopic ? ["confirmationButton","confirmAndRepeatButton"]: null,
            dialogId = config.dialogId ? config.dialogId : this.generateUuid();

         var dialogConfig = {
            id: dialogId,
            title: this.message(config.dialogTitle || ""),
            pubSubScope: config.pubSubScope, // Scope the dialog content so that it doesn't pollute any other widgets,,
            duration: config.duration || 0,
            handleOverflow: handleOverflow,
            fixedWidth: fixedWidth,
            noMinWidth:  !!config.noMinWidth,
            fullScreenMode: config.fullScreenMode || false,
            fullScreenPadding: !isNaN(config.fullScreenPadding) ? config.fullScreenPadding : 10,
            parentPubSubScope: config.parentPubSubScope,
            additionalCssClasses: config.additionalCssClasses ? config.additionalCssClasses : "",
            suppressCloseClasses: suppressCloseClasses,
            dialogWidth: config.dialogWidth || null,
            contentWidth: config.contentWidth ? config.contentWidth : null,
            contentHeight: config.contentHeight ? config.contentHeight : null,
            widgetsContent: [formConfig],
            widgetsButtons: [
                  {
                     name: "alfresco/buttons/AlfButton",
                     config: {
                        id: (config.dialogConfirmationButtonId) ? config.dialogConfirmationButtonId : dialogId + "_OK",
                        label: config.dialogConfirmationButtonTitle,
                        disableOnInvalidControls: true,
                        additionalCssClasses: "confirmationButton call-to-action",
                        publishTopic: this._formConfirmationTopic,
                        publishPayload: {
                           formSubmissionTopic: config.formSubmissionTopic,
                           formSubmissionPayloadMixin: config.formSubmissionPayloadMixin,
                           formSubmissionGlobal: config.formSubmissionGlobal,
                           formSubmissionScope: config.formSubmissionScope,
                           responseScope: config.alfResponseScope,
                           dialogEnableTopic: config.dialogEnableTopic
                        },
                        triggerTopic: config.formSubmissionTriggerTopic
                     }
                  },
                  {
                     name: "alfresco/buttons/AlfButton",
                     config: {
                        id: (config.dialogCancellationButtonId) ? config.dialogCancellationButtonId : dialogId + "_CANCEL",
                        label: config.dialogCancellationButtonTitle,
                        additionalCssClasses: "cancellationButton",
                        publishTopic: this._formCancellationTopic
                     }
                  }
               ]
         };

         if (config.dialogRepeats)
         {
            dialogConfig.widgetsButtons.splice(1,0,{
               name: "alfresco/buttons/AlfButton",
               config: {
                  id: config.dialogConfirmAndRepeatButtonId || (dialogId + "_OK_AND_REPEAT"),
                  label: config.dialogConfirmAndRepeatButtonTitle,
                  disableOnInvalidControls: true,
                  additionalCssClasses: "confirmAndRepeatButton",
                  publishTopic: this._formConfirmationRepeatTopic,
                  publishPayload: {
                     formSubmissionTopic: config.formSubmissionTopic,
                     formSubmissionPayloadMixin: config.formSubmissionPayloadMixin,
                     formSubmissionGlobal: config.formSubmissionGlobal,
                     formSubmissionScope: config.formSubmissionScope,
                     responseScope: config.alfResponseScope,
                     dialogEnableTopic: config.dialogEnableTopic
                  }
               }
            });
         }

         return dialogConfig;
      },

      /**
       * Creates and returns the [form]{@link module:alfresco/forms/Form} configuration to be added to the 
       * [dialog]{@link module:alfresco/dialog/AlfDialog}
       *
       * @instance
       * @param {object} widgets This is the configuration of the fields to be included in the form.
       * @param {object} formValue The initial value to set in the form.
       * @returns {object} The configuration for the form to add to the dialog
       */
      createFormConfig: function alfresco_services_DialogService__createFormConfig(config, formValue) {
         // See AKU-904...
         // The "customFormConfig" attribute is a catch-all for supporting any additional options that may
         // be added to a form without requiring the same attributes to be added to this service. We
         // use the custom config as the initial base and then mixin in the defaults as we want the 
         // specific properties to "win"...
         var formConfig = config.customFormConfig || {};
         lang.mixin(formConfig, {
            additionalCssClasses: "root-dialog-form",
            displayButtons: false,
            widgets: config.widgets,
            value: formValue,
            warnings: config.warnings,
            warningsPosition: config.warningsPosition
         });

         var form = {
            name: "alfresco/forms/Form",
            config: formConfig
         };
         return form;
      },

      /**
       * This handles the user clicking the confirmation button on the dialog (typically, and by default the "OK" button). This has a special
       * handler to process the  payload and construct a simple object reqpresenting the
       * content of the inner [form]{@link module:alfresco/forms/Form}.
       *
       * @instance
       * @param {object} payload The dialog content
       */
      onFormDialogConfirmation: function alfresco_services_DialogService__onFormDialogConfirmation(payload) {
         if (payload &&
             payload.dialogContent)
         {
            when(payload.dialogContent, lang.hitch(this, function(dialogContent) {
               if (dialogContent && dialogContent.length)
               {
                  var data = {};
                  var formData = dialogContent[0].getValue();

                  // See AKU-846 - If the dialog has been configured with an enablement topic then we can disable
                  // all the buttons in the dialog, in the knowledge that they will be re-enabled if form submission
                  // fails.
                  if (payload.dialogEnableTopic && payload.dialogRef)
                  {
                     when(payload.dialogRef.getButtons(), lang.hitch(this, function(buttons) {
                        array.forEach(buttons, function(button) {
                           button.set("disabled", true);
                        });
                     }));
                  }

                  // Destroy the dialog if a reference is provided...
                  if (payload.dialogReference && typeof payload.dialogReference.destroyRecursive === "function")
                  {
                     payload.dialogReference.destroyRecursive();
                  }

                  // Mixin in any additional payload information...
                  // An alfResponseScope should always have been set on a payload so it can be set as the
                  // responseScope, but a responseScope in the formSubmissionPayloadMixin will override it
                  lang.mixin(data, {
                     responseScope: payload.alfResponseScope
                  });
                  payload.formSubmissionPayloadMixin && lang.mixin(data, payload.formSubmissionPayloadMixin);

                  // Using JQuery here in order to support deep merging of dot-notation properties...
                  $.extend(true, data, formData);

                  // Publish the topic requested for complete...
                  var customScope;
                  if (payload.formSubmissionScope || payload.formSubmissionScope === "") 
                  {
                     customScope = payload.formSubmissionScope;
                  }
                  
                  var topic = payload.formSubmissionTopic,
                     globalScope = payload.hasOwnProperty("formSubmissionGlobal") ? !!payload.formSubmissionGlobal : true,
                     toParent = false;
                  this.alfPublish(topic, data, globalScope, toParent, customScope);
               }
               else
               {
                  this.alfLog("error", "The format of the dialog content was not as expected, the 'formSubmissionTopic' will not be published", payload, this);
               }
            }));
         }
      },

      /**
       * Show the supplied dialog (also used for adding hooks to the show/hide methods)
       *
       * @instance
       * @param {Object} payload The original request payload
       * @param {Object} dialog The dialog to show
       */
      _showDialog: function alfresco_services_DialogService___showDialog(payload, dialog) {

         // Add to the stack of active dialogs
         this._activeDialogs.push(dialog);

         // Ensure the dialogs-showing CSS state is "true"
         // NOTE: This selector is used in AlfDialog.css as this service has no CSS file itself
         domStyle.set(document.body, "margin-right", this._getScrollbarWidth() + "px");
         domClass.add(document.documentElement, "alfresco-dialog-AlfDialog--dialogs-visible");

         // Handle cancelling
         this._handleCancellation(payload, dialog);

         // Show the dialog
         dialog.show();

         // Hook up to the dialog hide event
         aspect.after(dialog, "onHide", lang.hitch(this, function() {
            
            // Remove any cancellation topic values
            dialog.cancelPublishTopic = dialog.cancelPublishScope = null;

            // Remove this dialog from the active dialogs stack
            this._activeDialogs = array.filter(this._activeDialogs, function(activeDialog) {
               return activeDialog !== dialog;
            });

            // If there are no dialogs "left" then set the dialogs-showing CSS state to "false"
            if(this._activeDialogs.length === 0) {
               domClass.remove(document.documentElement, "alfresco-dialog-AlfDialog--dialogs-visible");
               domStyle.set(document.body, "margin-right", "0");
            }
         }));
      },

      /**
       * Get the scrollbar width for the current browser environment. This is cached
       * after first retrieval for faster access.
       *
       * @instance
       * @returns {number} The scrollbar width
       * @since 1.0.53
       */
      _getScrollbarWidth: function alfresco_services_DialogService___getScrollbarWidth() {
         if (!this._scrollbarWidth) {
            this._scrollbarWidth = window.innerWidth - document.documentElement.offsetWidth;
         }
         return this._scrollbarWidth;
      },

       /**
       * When a cancelTopic has been defined, make sure it's published when the dialog
       * is cancelled (via escape keypress or cross-button click).
       *
       * @instance
       * @param {object} payload The original payload
       * @param {object} dialog The dialog widget
       */
      _handleCancellation: function alfresco_services_DialogService___handleCancellation(payload, dialog) {
         if (payload.cancelPublishTopic) {
            dialog.cancelPublishTopic = payload.cancelPublishTopic;
            dialog.cancelPublishScope = payload.cancelPublishScope || payload.alfResponseScope;
         } else {
            dialog.cancelPublishTopic = dialog.cancelPublishScope = null;
         }
      },

      /**
       * Handle escape key being pressed at the body level
       *
       * @instance
       */
      _handleEscape: function alfresco_services_DialogService___handleEscape() {
         if (this._activeDialogs.length) {
            var lastOpenedDialog = this._activeDialogs[this._activeDialogs.length - 1];
            lastOpenedDialog.onCancel();
         }
      }
   });
});