Source: layout/HorizontalWidgets.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 should be used to lay widgets out in a horizontal line. If no specific widths are requested
 * by the child widgets then each will be allotted an equal amount of the available space. However, it is
 * possible for each widget to request a width in either pixels or percentage (and it is possible to mix
 * and match). Pixel dimensions will be allocated first and the percentages will be of the remaining available
 * width after fixed sizes are deducted. Any widgets that do not request a specific width will be allocated 
 * an equal amount of whatever is left.</p>
 * 
 * <p>It is also possible to define gaps between widgets by using the 
 * [widgetMarginLeft]{@link module:alfresco/layout/HorizontalWidgets#widgetMarginLeft} and
 * [widgetMarginRight]{@link module:alfresco/layout/HorizontalWidgets#widgetMarginRight} attributes (but you should bear
 * in mind that if using both attributes then the gap between 2 widgets will be the <b>combination</b> of both values).</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 horizontally aligned controls then you should use the 
 * [ControlRow]{@link module:alfresco/forms/ControlRow} widget</p>
 *
 * <p><b>PLEASE NOTE: Resize operations are not currently handled - this will be addressed in the future</b></p>
 * 
 * @example <caption>Sample usage:</caption>
 * {
 *    "name": "alfresco/layout/HorizontalWidgets",
 *    "config": {
 *       "widgetMarginLeft": 10,
 *       "widgetMarginRight": 10,
 *       "widgets": [
 *          {
 *             "name": "alfresco/logo/Logo",
 *             "widthPx": 300
 *          },
 *          {
 *             "name": "alfresco/logo/Logo",
 *             "widthPc": 50
 *          }
 *       ]
 *    }
 * }
 * 
 * @module alfresco/layout/HorizontalWidgets
 * @extends module:alfresco/core/ProcessWidgets
 * @mixes module:alfresco/core/ResizeMixin
 * @mixes module:alfresco/alfresco/layout/DynamicVisibilityResizingMixin
 * @author Dave Draper
 */
define(["alfresco/core/ProcessWidgets",
        "dojo/_base/declare",
        "dojo/text!./templates/HorizontalWidgets.html",
        "alfresco/core/ResizeMixin",
        "alfresco/layout/DynamicVisibilityResizingMixin",
        "dojo/_base/lang",
        "dojo/_base/array",
        "dojo/dom-class",
        "dojo/dom-construct",
        "dojo/dom-style",
        "dojo/dom-geometry",
        "dojo/when",
        "jquery"], 
        function(ProcessWidgets, declare, template, ResizeMixin, DynamicVisibilityResizingMixin, lang, array, 
                 domClass, domConstruct, domStyle, domGeom, when, $) {
   
   return declare([ProcessWidgets, ResizeMixin, DynamicVisibilityResizingMixin], {
      
      /**
       * An array of the CSS files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{cssFile:"./css/HorizontalWidgets.css"}]
       */
      cssRequirements: [{cssFile:"./css/HorizontalWidgets.css"}],

      /**
       * The HTML template to use for the widget.
       * @instance
       * @type {string}
       */
      templateString: template,
      
      /**
       * The CSS class (or a space separated list of classes) to include in the DOM node.
       * 
       * @instance
       * @type {string}
       * @default
       */
      baseClass: "horizontal-widgets",
      
      /**
       * This will be set to a percentage value such that each widget displayed has an equal share
       * of page width. 
       * 
       * @instance
       * @type {string}
       * @default
       */
      widgetWidth: null,
      
      /**
       * This is the size of margin (in pixels) that will appear to the left of every widget added. 
       *
       * @instance
       * @type {number}
       * @default
       */
      widgetMarginLeft: null,

      /**
       * This is the size of margin (in pixels) that will appear to the right of every widget added. 
       *
       * @instance
       * @type {number}
       * @default
       */
      widgetMarginRight: null,

      /**
       * Indicates whether or not the widget has dimensions to work with when created. This mainly refers
       * to the available width and when there is no width to consume then is not "safe" to create child
       * widgets.
       * 
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.46
       */
      _hasInitialDimensions: false,

      /**
       * Extends the [inherited function]{@link module:alfresco/core/CoreWidgetProcessing#allWidgetsProcessed}
       * to set up subscriptions for the [visibilityRuleTopics]{@link module:alfresco/layout/DynamicVisibilityResizingMixin#visibilityRuleTopics}
       * that are returned by calling [getVisibilityRuleTopics]{@link module:alfresco/layout/DynamicVisibilityResizingMixin#getVisibilityRuleTopics}
       * on the first pass through the [doWidthProcessing]{@link module:alfresco/layout/HorizontalWidgets#doWidthProcessing}
       * function. The subscriptions need to be created after the widgets have been created in order that their visibility 
       * is adjusted before the [onResize]{@link module:alfresco/layout/HorizontalWidgets#onResize} function that is bound 
       * to is called.
       *
       * @instance
       * @param {object[]} widgets The widgets that have been created
       * @since 1.0.33
       */
      allWidgetsProcessed: function alfresco_layout_HorizontalWidgets__allWidgetsProcessed(/*jshint unused:false*/ widgets) {
         this.inherited(arguments);
         this.subscribeToVisibilityRuleTopics(this.onResize);
      },

      /**
       * Sets up the default width to be allocated to each child widget to be added.
       * 
       * @instance
       */
      postCreate: function alfresco_layout_HorizontalWidgets__postCreate() {
         // Split the full width between all widgets... 
         // We should update this to allow for specific widget width requests...
         this.visibilityRuleTopics = this.getVisibilityRuleTopics(this.widgets);

         // NOTE: Here we're very deliberately using JQuery to get the available width, this
         //       is to address a very specific issue with Firefox failing when using iframes
         //       and the dojo/dom-geometry code (in particular the getComputedStyle) - see AKU-692
         var overallwidth = $(this.domNode).width();
         if (overallwidth)
         {
            this._hasInitialDimensions = true;
            this.doWidthProcessing(this.widgets, true);
         }
         this.inherited(arguments);

         // Update the grid as the window changes...
         this.alfSetupResizeSubscriptions(this.onResize, this);
      },
      
      /**
       * Calculates the widths of each widget based on the requested sizes defined in either pixels or
       * percentage. The pixel widths take precedence and the percentages are calculated as the percentage
       * of whatever remains. If a widget has not requested a width then it will get an even share
       * of whatever horizontal space remains.
       *
       * @instance
       * @param {array} widgets The widgets or widget configurations to process the widths for
       * @param {boolean} firstPass Indicates whether this is the first pass (this determines whether to look at visibility
       * rule configuration or DOM node visibility).
       */
      doWidthProcessing: function alfresco_layout_HorizontalWidgets__doWidthProcessing(widgets, firstPass) {
         // jshint maxstatements:false
         if (widgets && this.domNode)
         {
            try
            {
               // Get the dimensions of the current DOM node...
               var computedStyle = domStyle.getComputedStyle(this.domNode);
               var output = domGeom.getMarginBox(this.domNode, computedStyle);
               var overallwidth = output.w;
               overallwidth -= widgets.length;

               // Always allow some pixels for potential scrollbars...
               overallwidth -= 30;
               
               // Subtract the margins from the overall width
               var leftMarginsSize = 0,
                   rightMarginsSize = 0;

               // Filter out any widgets that are configured to be initially invisible (on first pass processing)
               // or that have a DOM node that is not displayed (on resizing)...
               if (firstPass)
               {
                  widgets = array.filter(widgets, function(widget) {
                     var visibleInitialValue = lang.getObject("config.visibilityConfig.initialValue", false, widget);
                     var invisibleInitialValue = lang.getObject("config.invisibilityConfig.initialValue", false, widget);
                     return visibleInitialValue !== false && invisibleInitialValue !== true;
                  });
               }
               else
               {
                  widgets = array.filter(widgets, function(widget) {
                     return widget.domNode && domStyle.get(widget.domNode, "display") !== "none";
                  });
               }
               
               // NOTE: In the "if" statements below we're not worried about widgetMarginLeft 
               //       or widgetMarginRight being 0 and thus the statement failing to evaluate
               //       to true since the calculated size would remain 0 anyway
               if (this.widgetMarginLeft && !isNaN(this.widgetMarginLeft))
               {
                  leftMarginsSize = widgets.length * parseInt(this.widgetMarginLeft, 10);
               }
               else
               {
                  this.widgetMarginLeft = 0;
               }
               if (this.widgetMarginRight && !isNaN(this.widgetMarginRight))
               {
                  rightMarginsSize = widgets.length * parseInt(this.widgetMarginRight, 10);
               }
               else
               {
                  this.widgetMarginRight = 0;
               }
               var remainingWidth = overallwidth - leftMarginsSize - rightMarginsSize;

               // Work out how many pixels widgets have requested and subtract that from the remainder...
               var widgetRequestedWidth = 0;
               var widgetsWithNoWidthReq = 0;
               array.forEach(widgets, function(widget) {
                  if ((widget.widthPx || widget.widthPx === 0) && !isNaN(widget.widthPx))
                  {
                     widgetRequestedWidth += parseInt(widget.widthPx, 10);
                     widget.widthCalc = widget.widthPx;
                  }
                  else if ((widget.widthPc || widget.widthPc === 0) && !isNaN(widget.widthPc))
                  {
                     // No action, just avoiding adding to the count of widgets that don't request
                     // a width as either a pixel or percentage size.
                  }
                  else
                  {
                     // The current widget either hasn't requested a width or has requested it with a value
                     // that is not a number. It will therefore get an equal share of whatever remainder is left.
                     widgetsWithNoWidthReq++;
                  }
               });

               // Check to see if there is actually any space left across the page...
               // There's not really a lot we can do about it if not but it's useful to warn developers so that they
               // can spot that there's a potential fault...
               remainingWidth = remainingWidth - widgetRequestedWidth;
               if (remainingWidth < 0)
               {
                  this.alfLog("warn", "There is no horizontal space left for widgets requesting a percentage of available space", this);
               }

               // Update the widgets that have requested a percentage of space with a value that is calculated from the remaining space
               var totalWidthAsRequestedPercentage = 0;
               array.forEach(widgets, function(widget) {
                  if ((widget.widthPc || widget.widthPc === 0) && !isNaN(widget.widthPc))
                  {
                     var pc = parseInt(widget.widthPc, 10);
                     totalWidthAsRequestedPercentage += pc;

                     if (pc > 100)
                     {
                        this.alfLog("warn", "A widget has requested more than 100% of available horizontal space", widget, this);
                     }

                     widget.widthCalc = remainingWidth * (pc/100);
                  }
               }, this);

               // Work out the remaining percentage of the page that can be divided between widgets that haven't requested a specific
               // widget in either pixels or as a percentage...
               var remainingPercentage = 0;
               if (totalWidthAsRequestedPercentage > 100)
               {
                  this.alfLog("warn", "Widgets have requested more than 100% of the available horizontal space", this);
               }
               else
               {
                  remainingPercentage = 100 - totalWidthAsRequestedPercentage;
               }

               // Divide up the remaining horizontal space between the remaining widgets...
               remainingPercentage = remainingPercentage / widgetsWithNoWidthReq;
               var standardWidgetWidth = remainingWidth * (remainingPercentage/100);
               array.forEach(widgets, function(widget) {
                  if (((widget.widthPc || widget.widthPc === 0) && !isNaN(widget.widthPc)) ||
                      ((widget.widthPx || widget.widthPx === 0) && !isNaN(widget.widthPx)))
                  {
                     // No action required. 
                  }
                  else
                  {
                     widget.widthCalc = standardWidgetWidth;
                  }
               });
            }
            catch (e)
            {
               this.alfLog("warn", "Failure to calculate widths correctly", e, this);
            }
         }
      },

      /**
       * Updates the sizes of each widget when the window is resized.
       *
       * @instance
       * @param {object} evt The resize event.
       */
      onResize: function alfresco_layout_HorizontalWidget__onResize() {
         // This function is called whenever a resize event occurs, but also when a widget 
         // changes in visibility. This allows us to hook into any ancestor widget publishing
         // a resize request that occurs as it becomes visible. This allows us then to check
         // for some dimensions to work with...
         if (!this._hasInitialDimensions)
         {
            var overallwidth = $(this.domNode).width();
            if (overallwidth)
            {
               this._hasInitialDimensions = true;
               this.doWidthProcessing(this.widgets, true);
               this.alfPublishResizeEvent(this.domNode);
            }
         }
         else
         {
            when(this.getProcessedWidgets(), lang.hitch(this, function(processedWidgets) {
               this.doWidthProcessing(processedWidgets, false);
               array.forEach(processedWidgets, function(widget) {
                  if (widget && widget.domNode && widget.widthCalc)
                  {
                     var currentWidth = domGeom.getMarginBox(widget.domNode.parentNode).w;
                     domStyle.set(widget.domNode.parentNode, "width", widget.widthCalc + "px");
                     if (currentWidth !== widget.widthCalc)
                     {
                        this.alfPublishResizeEvent(widget.domNode.parentNode);
                     }
                  }
               }, this);
            }));
         }
      },

      /**
       * This overrides the default implementation to ensure that each each child widget added has the 
       * appropriate CSS classes applied such that they appear horizontally. It also sets the width
       * of each widget appropriately (either based on the default generated width which is an equal
       * percentage assigned to each child widget) or the specific width configured for the widget.
       * 
       * @instance
       * @param {object} widget The widget definition to create the DOM node for
       * @param {element} rootNode The DOM node to create the new DOM node as a child of
       * @param {string} rootClassName A string containing one or more space separated CSS classes to set on the DOM node
       * @returns {element} A new DOM node for the widget to be attached to
       */
      createWidgetDomNode: function alfresco_layout_HorizontalWidgets__createWidgetDomNode(widget, /*jshint unused:false*/ rootNode, rootClassName) {
         var outerDiv = domConstruct.create("div", { className: "horizontal-widget"}, this.containerNode);
         
         // Set the width of each widget according to how many there are...
         var style = {
            "marginLeft": this.widgetMarginLeft + "px",
            "marginRight": this.widgetMarginRight + "px"
         };
         if (widget.widthCalc)
         {
            style.width =  widget.widthCalc + "px";
         }
         domStyle.set(outerDiv, style);
         
         var innerDiv = domConstruct.create("div", {}, outerDiv);
         return innerDiv;
      },

      /**
       *
       * @instance
       * @param {object} config The configuration for the widget
       * @param {element} domNode The DOM node to attach the widget to
       * @param {function} callback A function to call once the widget has been instantiated
       * @param {object} callbackScope The scope with which to call the callback
       * @param {number} index The index of the widget to create (this will effect it's location in the 
       * [_processedWidgets]{@link module:alfresco/core/Core#_processedWidgets} array)
       */
      createWidget: function alfresco_layout_HorizontalWidgets__createWidget(config, /*jshint unused:false*/ domNode, callback, callbackScope, index) {
         var widget = this.inherited(arguments);
         if (widget)
         {
            if ((config.widthPx || config.widgetPx === 0) && !isNaN(config.widthPx))
            {
               widget.widthPx = config.widthPx;
            }
            else if ((config.widthPc || config.widthPc === 0) && !isNaN(config.widthPc))
            {
               widget.widthPc = config.widthPc;
            }
            else
            {
               // No action required
            }
         }
         return widget;
      }
   });
});