Source: layout/AlfStackContainer.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 layout widget provides the ability to display stacked content a little like the pages (panes) of 
 * a book and where only one pane will be shown at a time. Panes can be dynamically added, selected and 
 * removed as required. Unless explicitly requested, only the content of the intially selected pane will be 
 * rendered - the content of the other panes will be rendered as they are selected. The height of the widget 
 * will grow and shrink based on the content of each pane by default unless the 
 * [height]{@link module:alfresco/layout/AlfStackContainer#height} is explicitly set to a non-percentage 
 * value.</p>
 * <p>If you want the widget to respond to publications to dynamically 
 * [add]{@link module:alfresco/layout/AlfStackContainer#paneAdditionTopic}, 
 * [select]{@link module:alfresco/layout/AlfStackContainer#paneSelectionTopic} or
 * [delete]{@link module:alfresco/layout/AlfStackContainer#paneDeletionTopic} panes then you will need to 
 * configure the topics to subscribe to. Subscriptions will be made at the configured 
 * [pubSubScope]{@link module:alfresco/core/Core#pubSubScope} of the widget.</p>
 * <p>Pane selection can also be made by configuring a 
 * [paneSelectionHashVar]{@link module:alfresco/layout/AlfStackContainer#paneSelectionHashVar}. When a hash 
 * changed event occurs and the hash contains the configured variable, should it match the title of one of 
 * the panes in the AlfStackContainer, that pane will be selected.</p>
 *
 * @example <caption>Basic configuration (first pane will be selected):</caption>
 * {
 *    name: "alfresco/layout/AlfStackContainer",
 *    config: {
 *       widgets: [
 *          {
 *             id: "PANE1",
 *             name: "alfresco/logo/Logo",
 *             title: "Pane with Alfresco Logo",
 *             config: {
 *                logoClasses: "alfresco-logo-only"
 *             }
 *          },
 *          {
 *             id: "PANE2",
 *             name: "alfresco/logo/Logo",
 *             title: "Pane with Surf Logo",
 *             config: {
 *                logoClasses: "surf-logo-large"
 *             }
 *          }
 *       ]
 *    }
 * }
 *
 * @example <caption>Use "delayProcessing" to force second pane to render before selection:</caption>
 * {
 *    name: "alfresco/layout/AlfStackContainer",
 *    config: {
 *       widgets: [
 *          {
 *             id: "PANE1",
 *             name: "alfresco/logo/Logo",
 *             title: "Pane with Alfresco Logo",
 *             selected: true,
 *             config: {
 *                logoClasses: "alfresco-logo-only"
 *             }
 *          },
 *          {
 *             id: "PANE2",
 *             name: "alfresco/logo/Logo",
 *             title: "Pane with Surf Logo",
 *             delayProcessing: false,
 *             config: {
 *                logoClasses: "surf-logo-large"
 *             }
 *          }
 *       ]
 *    }
 * }
 *
 * @example <caption>Use "selected" make the second pane initially selected:</caption>
 * {
 *    name: "alfresco/layout/AlfStackContainer",
 *    config: {
 *       widgets: [
 *          {
 *             id: "PANE1",
 *             name: "alfresco/logo/Logo",
 *             title: "Pane with Alfresco Logo",
 *             config: {
 *                logoClasses: "alfresco-logo-only"
 *             }
 *          },
 *          {
 *             id: "PANE2",
 *             name: "alfresco/logo/Logo",
 *             title: "Pane with Surf Logo",
 *             selected: true,
 *             config: {
 *                logoClasses: "surf-logo-large"
 *             }
 *          }
 *       ]
 *    }
 * }
 * 
 * @example <caption>Define topics to dynamically manipulate panes:</caption>
 * {
 *    name: "alfresco/layout/AlfStackContainer",
 *    config: {
 *       paneSelectionTopic: "SELECT_PANE_TOPIC",
 *       paneAdditionTopic: "ADD_PANE_TOPIC",
 *       paneDeletionTopic: "DELETE_PANE_TOPIC",
 *       widgets: [
 *          {
 *             id: "PANE1",
 *             name: "alfresco/logo/Logo",
 *             title: "Pane with Alfresco Logo",
 *             selected
 *             config: {
 *                logoClasses: "alfresco-logo-only"
 *             }
 *          }
 *       ]
 *    }
 * }
 *
 * @example <caption>Example publication to add a new pane (based on previous example topic):</caption>
 * {
 *    publishTopic: "ADD_PANE_TOPIC",
 *    publishPayload: {
 *       widgets: [
 *          {
 *             name: "alfresco/html/Label",
 *             title: "New",
 *             closable: true,
 *             selected: true,
 *             config: {
 *                label: "This pane was dynamically added"
 *             }
 *          }
 *       ]
 *    }
 * }
 *
 * @example <caption>Define a paneSelectionHashVar:</caption>
 * {
 *    name: "alfresco/layout/AlfStackContainer",
 *    config: {
 *       paneSelectionHashVar: "view",
 *       widgets: [
 *          {
 *             id: "PANE1",
 *             name: "alfresco/logo/Logo",
 *             title: "Pane1",
 *             selected
 *             config: {
 *                logoClasses: "alfresco-logo-only"
 *             }
 *          },
 *          {
 *             id: "PANE2",
 *             name: "alfresco/logo/Logo",
 *             title: "Pane2",
 *             selected: true,
 *             config: {
 *                logoClasses: "surf-logo-large"
 *             }
 *          }
 *       ]
 *    }
 * }
 * <p>If the hash was changed to #view=Pane2, PANE2 will be displayed</p>
 * 
 * @module alfresco/layout/AlfStackContainer
 * @extends external:dijit/_WidgetBase
 * @mixes external:dojo/_TemplatedMixin
 * @mixes module:alfresco/core/Core
 * @mixes module:alfresco/core/CoreWidgetProcessing
 * @author Richard Smith
 */
define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin",
        "dojo/text!./templates/AlfStackContainer.html",
        "alfresco/core/Core",
        "alfresco/core/CoreWidgetProcessing",
        "alfresco/documentlibrary/_AlfHashMixin",
        "dijit/layout/StackContainer",
        "dijit/layout/ContentPane",
        "dojo/dom-construct",
        "dojo/_base/lang",
        "dojo/_base/array",
        "alfresco/util/hashUtils"], 
        function(declare, _WidgetBase, _TemplatedMixin, template, AlfCore, CoreWidgetProcessing, _AlfHashMixin, 
                  StackContainer, ContentPane, domConstruct, lang, array, hashUtils) {
   
   return declare([_WidgetBase, _TemplatedMixin, AlfCore, CoreWidgetProcessing, _AlfHashMixin], {
      
      /**
       * The HTML template to use for the widget
       * 
       * @instance
       * @type {string}
       */
      templateString: template,
      
      /**
       * This will hold a reference to the stack container widget.
       *
       * @instance
       * @type {object}
       * @default
       */
      stackContainerWidget: null,

      /**
       * Height of the finished AlfStackContainer as any recognised css size with units
       * 
       * @instance
       * @type {string}
       * @default
       */
      height: "100%",

      /**
       * Width of the finished AlfStackContainer as any recognised css size with units
       * 
       * @instance
       * @type {string}
       * @default
       */
      width: "100%",

      /**
       * Should StackContainer layout mathematics be invoked?
       * 
       * @instance
       * @type {Boolean}
       * @default
       */
      doLayout: false,

      /**
       * This array is used to store widgets for delayed processing
       * 
       * @type {array}
       */
      _delayedProcessingWidgets: [],

      /**
       * The topic whose publication should trigger the selection of a pane
       * 
       * @instance
       * @type {string}
       * @default
       */
      paneSelectionTopic: null,

      /**
       * The topic whose publication should trigger the addition of a pane
       * 
       * @instance
       * @type {string}
       * @default
       */
      paneAdditionTopic: null,

      /**
       * The topic whose publication should trigger the deletion of a pane
       * 
       * @instance
       * @type {string}
       * @default
       */
      paneDeletionTopic: null,

      /**
       * If this optional value is provided, a change to the hash of this variable will update the pane selection
       *
       * @instance
       * @type {string}
       * @default
       */
      paneSelectionHashVar: null,
      
      /**
       * Variable to record the title of the selected pane at initialisation
       *
       * @instance
       * @type {object}
       * @default
       * @private
       */
      _defaultPaneTitle: null,

      /**
       * @instance
       */
      postCreate: function alfresco_layout_AlfStackContainer__postCreate() {

         // Initialise a StackContainer instance and watch its selectedChildWidget event
         this.stackContainerWidget = new StackContainer({
            style: "height: " + this.height + "; width: " + this.width + ";",
            doLayout: this.doLayout
         }, this.containerNode);
         this.stackContainerWidget.watch("selectedChildWidget", lang.hitch(this, this._paneChanged));

         // Setup child widgets and startup()
         if (this.widgets)
         {
            // By default we want to ensure that we don't unnecessarily process widgets for 
            // panes that are not immediately visible. Therefore unless specifically requested 
            // in the configuration to be the selected pane or to render immediately then
            // we'll mark them all as delayed processing. If no pane is marked as selected then
            // we'll ensure that the first pane is both selected and will be immediately rendered
            var paneSelected = false;
            array.forEach(this.widgets, function(widget) {
               if (widget.delayProcessing !== false && !widget.selected)
               {
                  widget.delayProcessing = true;
               }
               paneSelected = paneSelected || widget.selected;
            });
            if (this.widgets.length && !paneSelected)
            {
               this.widgets[0].selected = true;
               this.widgets[0].delayProcessing = false;
            }

            // Now add panes for each widget...
            array.forEach(this.widgets, lang.hitch(this, this.addWidget));
         }
         this.stackContainerWidget.startup();

         // Subscribe to some optional topics
         if (this.paneSelectionTopic)
         {
            this.alfSubscribe(this.paneSelectionTopic, lang.hitch(this, this.paneSelect));
         }
         if (this.paneAdditionTopic)
         {
            this.alfSubscribe(this.paneAdditionTopic, lang.hitch(this, this.paneAdd));
         }
         if (this.paneDeletionTopic)
         {
            this.alfSubscribe(this.paneDeletionTopic, lang.hitch(this, this.paneDelete));
         }
         if (this.paneSelectionHashVar)
         {
            this.alfSubscribe(this.hashChangeTopic, lang.hitch(this, this.onHashChanged));

            // Initialise the view by invoking onHashChanged, as though the browser had done so
            this.onHashChanged(hashUtils.getHash());
         }
      },

      /**
       * This function adds widgets to the StackContainer widget
       * 
       * @instance
       * @param {object} widget The widget to add
       * @param {integer} [index] The index of the required pane position
       */
      addWidget: function alfresco_layout_AlfStackContainer__addWidget(widget, index) {

         // Create a domNode and ContentPane
         var domNode = domConstruct.create("div", {}),
             cp = new ContentPane();

         // Add content to the ContentPane
         if(widget.content && typeof widget.content === "string")
         {
            cp.set("content", widget.content);
         }

         // Add a title to the ContentPane
         if(widget.title && typeof widget.title === "string")
         {
            cp.set("title", this.message(widget.title));
         }

         // Should the ContentPane be selected?
         if(widget.selected && typeof widget.selected === "boolean")
         {
            cp.set("selected", widget.selected);
            if(cp.get("title") !== null)
            {
               this._defaultPaneTitle = cp.get("title");
            }
         }

         // If not delayed processing, create the widget and add to the panel
         if(!widget.delayProcessing)
         {
            var widgetNode = this.createWidgetDomNode(widget, domNode);
            var w = this.createWidget(widget, widgetNode);

            // Add the widget to the ContentPane
            cp.addChild(w);
         }
         // Otherwise record the widget for processing later on
         else
         {
            this._delayedProcessingWidgets.push(
               {
                  "domNode": domNode,
                  "contentPane": cp,
                  "widget": widget
               }
            );
         }

         // If we have an index add the ContentPane at a particular position otherwise just add it
         this.stackContainerWidget.addChild(cp, (typeof index !== "undefined" ? index : null));
      },

      /**
       * Event triggered when the selected pane changes. Used to load delayed processing widgets.
       *
       * @instance
       * @private
       * @param {string} name
       * @param {object} oldPane
       * @param {object} newPane
       */
      _paneChanged: function alfresco_layout_AlfStackContainer___paneChanged(name, oldPane, newPane) {

         var forDeletion = null;
         for(var i = 0; i < this._delayedProcessingWidgets.length; i++)
         {
            var delayedWidget = this._delayedProcessingWidgets[i];
            if(newPane.containerNode.id === delayedWidget.contentPane.id)
            {
               var widgetNode = this.createWidgetDomNode(delayedWidget.widget, delayedWidget.domNode);
               var w = this.createWidget(delayedWidget.widget, widgetNode);

               // Add the widget to the ContentPane
               newPane.addChild(w);
               forDeletion = i;
               break;
            }
         }
         if(forDeletion !== null)
         {
            this._delayedProcessingWidgets.splice(forDeletion, 1);
         }
      },

      /**
       * This function selects a pane based upon parameter "payload.index" or "payload.id" or "payload.title".
       * 
       * @instance
       * @param {object} payload Details of the pane to select
       */
      paneSelect: function alfresco_layout_AlfStackContainer__paneSelect(payload) {
         var sc = this.stackContainerWidget,
             panes = sc.getChildren();
         if(payload && typeof payload.index === "number" && panes[payload.index])
         {
            sc.selectChild(panes[payload.index]);
         }
         else if(payload && (typeof payload.id === "string" || typeof payload.title === "string"))
         {
            for(var i = 0; i < panes.length; i++) // panes does not support forEach
            {
               if((payload.id && panes[i].id === payload.id) || (payload.title && panes[i].title === payload.title))
               {
                  sc.selectChild(panes[i]);
                  break;
               }
            }
         }
         else
         {
            this.alfLog("warn", "Attempt made to select a StackContainer pane with an inapproriate payload", this);
         }
      },

      /**
       * This function adds a new pane.
       * 
       * @instance
       * @param {object} payload Details of the pane to add
       */
      paneAdd: function alfresco_layout_AlfStackContainer__paneAdd(payload) {
         if (payload && payload.widgets)
         {
            array.forEach(payload.widgets, lang.hitch(this, this.addWidget));
         }
      },

      /**
       * This function deletes a pane based upon parameter "payload.index" or "payload.id" or "payload.title".
       * 
       * @instance
       * @param {object} payload Details of the pane to delete
       */
      paneDelete: function alfresco_layout_AlfStackContainer__paneDelete(payload) {
         var sc = this.stackContainerWidget,
             panes = sc.getChildren();
         if(payload && typeof payload.index === "number" && panes[payload.index])
         {
            sc.removeChild(panes[payload.index]);
         }
         else if(payload && (typeof payload.id === "string" || typeof payload.title === "string"))
         {
            for(var i = 0; i < panes.length; i++) // panes does not support forEach
            {
               if((payload.id && panes[i].id === payload.id) || (payload.title && panes[i].title === payload.title))
               {
                  sc.removeChild(panes[i]);
                  break;
               }
            }
         }
         else
         {
            this.alfLog("warn", "Attempt made to remove a StackContainer pane with an inapproriate payload", this);
         }
      },

      /**
       * This function is called whenever the browser URL hash fragment is changed
       * 
       * @instance
       * @param {object} payload
       */
      onHashChanged: function alfresco_layout_AlfStackContainer__onHashChanged(payload) {
         // If the payload contains a paneSelectionHashVar then we will show that pane. Otherwise we arrive here 
         // only when the hash variable is missing because of browser back or a user edit of the hash string. In 
         // both cases we want to re-show the original default pane if one was originally selected.
         if(payload[this.paneSelectionHashVar])
         {
            this.paneSelect({
               title: payload[this.paneSelectionHashVar]
            });
         }
         else if(this._defaultPaneTitle)
         {
            this.paneSelect({
               title: this._defaultPaneTitle
            });
         }
      }
   });
});