Source: layout/AlfTabContainer.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 tabbed content where tabs can be dynamically
 * added, selected and removed as required. Unless explicitly requested, only the content of the intially
 * selected tab will be rendered - the content of the other tabs will be rendered as they are selected. The 
 * height of the widget will grow and shrink based on the content of each tab by default unless
 * the [height]{@link module:alfresco/layout/AlfTabContainer#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/AlfTabContainer#tabAdditionTopic}, 
 * [select]{@link module:alfresco/layout/AlfTabContainer#tabSelectionTopic}, 
 * [disable]{@link module:alfresco/layout/AlfTabContainer#tabDisablementTopic} or
 * [delete]{@link module:alfresco/layout/AlfTabContainer#tabDeletionTopic} tabs 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>You can also request an additional publication occur when requesting to add a new tab by including a
 * "publishOnAdd" attribute in the payload that is an object containing a "publishTopic" attribute (with optional
 * "publishPayload", "publishGlobal", "publishToParent" and "publishScope" attributes).</p>
 * 
 * <p><b>PLEASE NOTE:</b> It is not possible to use this module to control the layout of controls within a form. If you wish
 * to create a form containing tabbed controls then you should use the 
 * [TabbedControls]{@link module:alfresco/forms/TabbedControls} widget</p>
 *
 * @example <caption>Basic configuration (first tab will be selected):</caption>
 * {
 *    name: "alfresco/layout/AlfTabContainer",
 *    config: {
 *       widgets: [
 *          {
 *             id: "TAB1",
 *             name: "alfresco/logo/Logo",
 *             title: "Tab with Alfresco Logo",
 *             config: {
 *                logoClasses: "alfresco-logo-only"
 *             }
 *          },
 *          {
 *             id: "TAB2",
 *             name: "alfresco/logo/Logo",
 *             title: "Tab with Surf Logo",
 *             config: {
 *                logoClasses: "surf-logo-large"
 *             }
 *          }
 *       ]
 *    }
 * }
 *
 * @example <caption>Use "delayProcessing" to force second tab to render before selection:</caption>
 * {
 *    name: "alfresco/layout/AlfTabContainer",
 *    config: {
 *       widgets: [
 *          {
 *             id: "TAB1",
 *             name: "alfresco/logo/Logo",
 *             title: "Tab with Alfresco Logo",
 *             selected: true,
 *             config: {
 *                logoClasses: "alfresco-logo-only"
 *             }
 *          },
 *          {
 *             id: "TAB2",
 *             name: "alfresco/logo/Logo",
 *             title: "Tab with Surf Logo",
 *             delayProcessing: false,
 *             config: {
 *                logoClasses: "surf-logo-large"
 *             }
 *          }
 *       ]
 *    }
 * }
 *
 * @example <caption>Use "selected" make the second tab initially selected:</caption>
 * {
 *    name: "alfresco/layout/AlfTabContainer",
 *    config: {
 *       widgets: [
 *          {
 *             id: "TAB1",
 *             name: "alfresco/logo/Logo",
 *             title: "Tab with Alfresco Logo",
 *             config: {
 *                logoClasses: "alfresco-logo-only"
 *             }
 *          },
 *          {
 *             id: "TAB2",
 *             name: "alfresco/logo/Logo",
 *             title: "Tab with Surf Logo",
 *             selected: true,
 *             config: {
 *                logoClasses: "surf-logo-large"
 *             }
 *          }
 *       ]
 *    }
 * }
 * 
 * @example <caption>Define topics to dynamically manipulate tabs:</caption>
 * {
 *    name: "alfresco/layout/AlfTabContainer",
 *    config: {
 *       tabSelectionTopic: "SELECT_TAB_TOPIC",
 *       tabDisablementTopic: "DISABLE_TAB_TOPIC",
 *       tabAdditionTopic: "ADD_TAB_TOPIC",
 *       tabDeletionTopic: "DELETE_TAB_TOPIC",
 *       widgets: [
 *          {
 *             id: "TAB1",
 *             name: "alfresco/logo/Logo",
 *             title: "Tab with Alfresco Logo",
 *             selected
 *             config: {
 *                logoClasses: "alfresco-logo-only"
 *             }
 *          }
 *       ]
 *    }
 * }
 *
 * @example <caption>Example publication to add a new tab (based on previous example topic):</caption>
 * {
 *    publishTopic: "ADD_TAB_TOPIC",
 *    publishPayload: {
 *       widgets: [
 *          {
 *             name: "alfresco/html/Label",
 *             title: "New",
 *             closable: true,
 *             selected: true,
 *             config: {
 *                label: "This tab was dynamically added"
 *             }
 *          }
 *       ]
 *    }
 * }
 * 
 * @module alfresco/layout/AlfTabContainer
 * @extends external:dijit/_WidgetBase
 * @mixes external:dojo/_TemplatedMixin
 * @mixes module:alfresco/core/Core
 * @mixes module:alfresco/core/CoreWidgetProcessing
 * @mixes module:alfresco/core/ResizeMixin
 * @author Richard Smith
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin",
        "dojo/text!./templates/AlfTabContainer.html",
        "alfresco/core/Core",
        "alfresco/core/CoreWidgetProcessing",
        "alfresco/core/ResizeMixin",
        "alfresco/core/topics",
        "dijit/layout/TabContainer",
        "dijit/layout/ContentPane",
        "dojo/Deferred",
        "dojo/dom-construct",
        "dojo/dom-class",
        "dojo/_base/lang",
        "dojo/_base/array",
        "jquery"], 
        function(declare, _WidgetBase, _TemplatedMixin, template, AlfCore, CoreWidgetProcessing, ResizeMixin, 
                 topics, TabContainer, ContentPane, Deferred, domConstruct, domClass, lang, array, $) {
   
   return declare([_WidgetBase, _TemplatedMixin, AlfCore, CoreWidgetProcessing, ResizeMixin], {
      
      /**
       * An array of the CSS files to use with this widget
       * 
       * @instance
       * @type {object[]}
       * @default [{cssFile:"./css/AlfTabContainer.css"}]
       */
      cssRequirements: [{cssFile:"./css/AlfTabContainer.css"}],
      
      /**
       * The HTML template to use for the widget
       * 
       * @instance
       * @type {string}
       */
      templateString: template,
      
      /**
       * This will hold a reference to the tab container widget.
       *
       * @instance
       * @type {object}
       * @default
       */
      tabContainerWidget: null,

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

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

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

      /**
       * This can be configured to give all tabs some padding around their content. The amount of
       * padding is controlled by the LESS variable "tab-padding"
       * 
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.79
       */
      padded: false,

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

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

      /**
       * The topic whose publication should trigger the disablement/enablement of a tab
       * 
       * @instance
       * @type {string}
       * @default
       */
      tabDisablementTopic: null,

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

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

      /**
       * This is the default delayed processing behaviour of tabs. By default a tabs content
       * will not be created until it is displayed (unless the tab is configured to request
       * otherwise). However, this can be overridden to switch the default so that all tab
       * content will be created as soon as the container is created.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      delayProcessingDefault: true,

      /**
       * A promise of the children of the [tabContainerWidget]{@link module:alfresco/layout/AlfTabContainer#tabContainerWidget}
       * that is resolved when it is created.
       * 
       * @instance
       * @type {object}
       * @since 1.0.79
       */
      _tabContainerChildrenPromise: null,

      /**
       * Creates the "dijit/layout/TabContainer" wrapped by this widget and sets up associated
       * subscriptions, etc.
       * 
       * @instance
       * @since 1.0.46
       */
      createTabContainer: function alfresco_layout_AlfTabContainer__createTabContainer() {
         var overallwidth = $(this.domNode).width();
         if (overallwidth)
         {
            try
            {
               // Initialise a TabContainer instance and watch its selectedChildWidget event
               this.tabContainerWidget = new TabContainer({
                  id: this.id + "_TABCONTAINER",
                  style: "height: " + this.height + "; width: " + this.width + ";",
                  doLayout: this.doLayout
               }, this.tabNode);
               this.tabContainerWidget.watch("selectedChildWidget", lang.hitch(this, this._tabChanged));

               // Setup child widgets and startup()
               if (this.widgets)
               {
                  this.widgets = array.filter(this.widgets, function(widget) {
                     return this.processAllFilters(widget.config);
                  }, this);

                  // By default we want to ensure that we don't unnecessarily process widgets for 
                  // tabs that are not immediately visible. Therefore unless specifically requested 
                  // in the configuration to be the selected tab or the to render immediately then
                  // we'll mark them all as delayed processing. If no tab is marked as selected then
                  // we'll ensure that the first tab is both selected and will be immediately rendered
                  var tabSelected = false;
                  array.forEach(this.widgets, function(widget) {
                     if ((widget.delayProcessing === true && !widget.selected) || 
                         widget.delayProcessing === false)
                     {
                        // No action, use the explicit configuration
                     }
                     else if (widget.delayProcessing === true && widget.selected)
                     {
                        // Silly configuration, if the tab is to be initially selected, processing can't be delayed...
                        widget.delayProcessingDefault = false;
                     }
                     else
                     {
                        // Use the default if no configuration is provided...
                        widget.delayProcessing = this.delayProcessingDefault;
                     }
                     tabSelected = tabSelected || widget.selected;
                  }, this);

                  if (this.widgets.length && !tabSelected)
                  {
                     this.widgets[0].selected = true;
                     this.widgets[0].delayProcessing = false;
                  }

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

               // Subscribe to some optional topics
               if (this.tabSelectionTopic)
               {
                  this.alfSubscribe(this.tabSelectionTopic, lang.hitch(this, this.onTabSelect));
               }
               if (this.tabDisablementTopic)
               {
                  this.alfSubscribe(this.tabDisablementTopic, lang.hitch(this, this.onTabDisable));
               }
               if (this.tabAdditionTopic)
               {
                  this.alfSubscribe(this.tabAdditionTopic, lang.hitch(this, this.onTabAdd));
               }
               if (this.tabDeletionTopic)
               {
                  this.alfSubscribe(this.tabDeletionTopic, lang.hitch(this, this.onTabDelete));
               }
               
               this.alfPublishResizeEvent(this.domNode);
               domClass.add(this.domNode, "alfresco-layout-AlfTabContainer--tabsDisplayed");

               if (this._tabContainerChildrenPromise)
               {
                  this._tabContainerChildrenPromise.resolve(this.tabContainerWidget.getChildren());
               }
            }
            catch(e)
            {
               this.alfLog("error", "It was not possible to create a TabContainer", e, this);
            }
         }
      },

      /**
       * Returns a promise of the children of the 
       * [tabContainerWidget]{@link module:alfresco/layout/AlfTabContainer#tabContainerWidget}
       * that will be resolved when it is created.
       * 
       * @instance
       * @return {promise} A promise of the children of the tab container
       * @since 1.0.79
       */
      getTabContainerChildren: function alfresco_layout_AlfTabContainer__getTabContainerChildren() {
         return this._tabContainerChildrenPromise;
      },

      /**
       * Calls [createTabContainer]{@link module:alfresco/layout/AlfTabContainer#createTabContainer}
       * to create the "dijit/layout/TabContainer" that is wrapped by this widget.
       * 
       * @instance
       */
      postCreate: function alfresco_layout_AlfTabContainer__postCreate() {
         this._tabContainerChildrenPromise = new Deferred();
         this.createTabContainer();
         this.alfSetupResizeSubscriptions(this.onResize, this);
         this.addResizeListener(this.domNode);
         if (this.padded)
         {
            domClass.add(this.domNode, "alfresco-layout-AlfTabContainer--padded");
         }
      },

      /**
       * This function adds widgets to the TabContainer widget
       * 
       * @instance
       * @param {object} widget The widget to add
       * @param {integer} index The index of the required tab position
       */
      addWidget: function alfresco_layout_AlfTabContainer__addWidget(widget, /*jshint unused:false*/ index) {
         var targetTabId = null;
         if (widget.id)
         {
            targetTabId = this.id + "_" + widget.id;
         }

         var indexOfDuplicateTab = this.indexOfTabId(targetTabId);
         if (indexOfDuplicateTab !== -1)
         {
            // A tab with the requested ID has already been added, so just select it...
            this.tabContainerWidget.selectChild(this.tabContainerWidget.getChildren()[indexOfDuplicateTab]);
         }
         else
         {
            // NOTE: It's important that the DOM structure of the tab is created and added to the page before 
            //       creating each child widget in order that the child widget has access to the initial dimensions
            //       of the tab (e.g. for an AlfSideBarContainer to calculate it's height appropriately)...
            var domNode = domConstruct.create("div", {});
            var cp = new ContentPane({
               id: targetTabId
            });
            domClass.add(cp.domNode, "alfresco-layout-AlfTabContainer__OuterTab");
            this.tabContainerWidget.addChild(cp, widget.tabIndex);

            // 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));
            }

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

            // Should the ContentPane be closable?
            if(widget.closable && typeof widget.closable === "boolean")
            {
               cp.set("closable", widget.closable);
            }

            // Should the ContentPane be disabled?
            if(widget.disabled && typeof widget.disabled === "boolean")
            {
               cp.set("disabled", widget.disabled);
            }

            // If not delayed processing, create the widget and add to the panel
            if(!widget.delayProcessing)
            {
               var widgetNode = this.createWidgetDomNode(widget, cp.domNode);
               var w = this.createWidget(widget, widgetNode);
            }
            // Otherwise record the widget for processing later on
            else
            {
               this._delayedProcessingWidgets.push(
                  {
                     "domNode": cp.domNode,
                     "contentPane": cp,
                     "widget": widget
                  }
               );
            }

            // If we have an index add the ContentPane at a particular position otherwise just add it
            if (widget.selected === true)
            {
               this.tabContainerWidget.selectChild(cp);
            }
         }
      },

      /**
       * 
       * @instance
       * @param {string} id The ID of an existing tab to search for
       * @return {number} The index of the duplicate tab and -1 if a duplicate does not exist
       * @since 1.0.35
       */
      indexOfTabId: function alfresco_layout_AlfTabContainer__indexOfTabId(id) {
         var duplicateTabIndex = -1;
         if (id)
         {
            array.some(this.tabContainerWidget.getChildren(), function(tab, tabIndex) {
               var match = tab.id === id;
               if (match)
               {
                  duplicateTabIndex = tabIndex;
               }
               return match;
            }, this);
         }
         return duplicateTabIndex;
      },

      /**
       * Event triggered when the selected tab changes. Used to load delayed processing widgets.
       *
       * @instance
       * @private
       * @param {string} name
       * @param {object} oldTab
       * @param {object} newTab
       */
      _tabChanged: function alfresco_layout_AlfTabContainer___tabChanged(name, oldTab, newTab) {
         var forDeletion = null;
         for(var i = 0; i < this._delayedProcessingWidgets.length; i++)
         {
            var delayedWidget = this._delayedProcessingWidgets[i];
            if(newTab.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
               newTab.addChild(w);
               forDeletion = i;
               break;
            }
         }
         if(forDeletion || forDeletion === 0)
         {
            this._delayedProcessingWidgets.splice(forDeletion, 1);
         }
         this.alfPublish(topics.PAGE_WIDGETS_READY, {}, true);
         this.alfPublish(topics.WIDGET_PROCESSING_COMPLETE, {}, true);
         this.alfPublishResizeEvent(this.domNode);
      },

      /**
       * Resizes the TabContainer on resize events.
       *
       * @instance
       * @param {object} evt The resize event.
       */
      onResize: function alfresco_layout_AlfTabContainer__onResize() {
         if (!this.tabContainerWidget)
         {
            // If wrapped tab container does not exist then this is likely to mean that it was not
            // possible to create the widget at startup - possibly because of issues around visibility.
            // Therefore we should attempt to create the tab container again (which will get the appropriate
            // sizing)...
            this.createTabContainer();
         }
         else if (this.tabContainerWidget && typeof this.tabContainerWidget.resize === "function")
         {
            this.tabContainerWidget.resize();

            // See AKU-766 - absolutely make sure that the selected child really is selected!
            if (this.tabContainerWidget.selectedChildWidget)
            {
               this.tabContainerWidget._showChild(this.tabContainerWidget.selectedChildWidget);
            }
         }
      },

      /**
       * This function selects a tab based upon parameter 'payload.index' or 'payload.id' or 'payload.title'.
       * 
       * @instance
       * @param {object} payload Details of the tab to select
       */
      onTabSelect: function alfresco_layout_AlfTabContainer__onTabSelect(payload) {
         var tc = this.tabContainerWidget,
             tabs = tc.getChildren();
         if(payload && typeof payload.index === "number" && tabs[payload.index])
         {
            tc.selectChild(tabs[payload.index]);
         }
         else if(payload && (typeof payload.id === "string" || typeof payload.title === "string"))
         {
            var targetTabId = this.id + "_" + payload.id;
            for(var i = 0; i < tabs.length; i++) // tabs does not support forEach
            {
               if((payload.id && tabs[i].id === targetTabId) || (payload.title && tabs[i].title === payload.title))
               {
                  tc.selectChild(tabs[i]);
                  break;
               }
            }
         }
         else
         {
            this.alfLog("warn", "Attempt made to select a TabController tab with an inapproriate payload", this);
         }
      },

      /**
       * This function disables or enables a tab based upon parameter 'payload.index' or 'payload.id' or 'payload.title', and 'payload.value' for the state.
       * 
       * @instance
       * @param {object} payload Details of the tab to disable or enable
       */
      onTabDisable: function alfresco_layout_AlfTabContainer__onTabDisable(payload) {
         var tc = this.tabContainerWidget,
             tabs = tc.getChildren();
         if(payload && typeof payload.index === "number" && tabs[payload.index] && typeof payload.value === "boolean")
         {
            tabs[payload.index].set("disabled", payload.value);
         }
         else if(payload && (typeof payload.id === "string" || typeof payload.title === "string") && typeof payload.value === "boolean")
         {
            for(var i = 0; i < tabs.length; i++) // tabs does not support forEach
            {
               var targetTabId = this.id + "_" + payload.id;
               if((payload.id && tabs[i].id === targetTabId) || (payload.title && tabs[i].title === payload.title))
               {
                  tabs[i].set("disabled", payload.value);
                  break;
               }
            }
         }
         else
         {
            this.alfLog("warn", "Attempt made to disable or enable a TabController tab with an inapproriate payload", this);
         }
      },

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

         // See AKU-583...
         // It is possible to include a "publishOnAdd" attribute when creating tabs that define an additional publication to perform
         // after the tab is added...
         if (payload.publishOnAdd)
         {
            var publication = payload.publishOnAdd;
            if (publication.publishTopic)
            {
               this.alfPublish(publication.publishTopic,
                               publication.publishPayload,
                               publication.publishGlobal,
                               publication.publishToParent,
                               publication.publishScope);
            }
            else
            {
               this.alfLog("warn", "A request to add a tab included a following publication, but it did not include a 'publishTopic' attribute", payload, this);
            }
         }
      },

      /**
       * This function deletes a tab based upon parameter 'payload.index' or 'payload.id' or 'payload.title'.
       * 
       * @instance
       * @param {object} payload Details of the tab to delete
       */
      onTabDelete: function alfresco_layout_AlfTabContainer__onTabDelete(payload) {
         var tc = this.tabContainerWidget,
             tabs = tc.getChildren();
         if(payload && typeof payload.index === "number" && tabs[payload.index])
         {
            tc.removeChild(tabs[payload.index]);
         }
         else if(payload && (typeof payload.id === "string" || typeof payload.title === "string"))
         {
            for(var i = 0; i < tabs.length; i++) // tabs does not support forEach
            {
               var targetTabId = this.id + "_" + payload.id;
               if((payload.id && tabs[i].id === targetTabId) || (payload.title && tabs[i].title === payload.title))
               {
                  tc.removeChild(tabs[i]);
                  break;
               }
            }
         }
         else
         {
            this.alfLog("warn", "Attempt made to remove a TabController tab with an inapproriate payload", this);
         }
      }
   });
});