Source: menus/AlfDropDownMenu.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/>.
 */

/**
 * @module alfresco/menus/AlfDropDownMenu
 * @extends external:dijit/DropDownMenu
 * @mixes module:alfresco/core/Core
 * @mixes module:alfresco/core/CoreWidgetProcessing
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "dijit/DropDownMenu",
        "alfresco/core/Core",
        "alfresco/core/CoreWidgetProcessing",
        "alfresco/menus/AlfMenuItemWrapper",
        "dojo/_base/array",
        "dojo/dom-class"], 
        function(declare, DropDownMenu, AlfCore, CoreWidgetProcessing, AlfMenuItemWrapper, array, domClass) {
   
   return declare([DropDownMenu, AlfCore, CoreWidgetProcessing], {
      
      /**
       * An array of the CSS files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{cssFile:"./css/AlfDropDownMenu.css"}]
       */
      cssRequirements: [{cssFile:"./css/AlfDropDownMenu.css"}],
      
      /**
       * Updates the default template with some additional CSS class information and then processes
       * the widgets supplied.
       * 
       * @instance
       */
      postCreate: function alfresco_menus_AlfDropDownMenu__postCreate() {
         this.inherited(arguments);
         
         // Add a custom class to the container node (this has been done to prevent us overriding the default
         // template unnecessarily and risk losing updates)...
         domClass.add(this.containerNode.parentNode, "alf-dropdown-menu");
         
         // Process all the widgets which in this case will become menu items...
         if (this.widgets)
         {
            this.processWidgets(this.widgets);
         }
      },
      
      /**
       * Callback implementation following instantiation of all of the widgets defined in by the "widgets"
       * instance property. 
       * 
       * @instance
       * @param {array} widgets An array of the instantiated widgets (as defined by the widgets instance property).
       */
      allWidgetsProcessed: function alfresco_menus_AlfDropDownMenu__allWidgetsProcessed(widgets) {
         var _this = this;
         array.forEach(widgets, function(widget) {
             // Add the widget to the drop down menu...
            _this.addChild(widget);
         });
      },
      
      /**
       * 
       * @instance
       * @param {object} widget The widget to add as the new child
       * @param {integer} insertIndex The index at which to insert the child
       */
      addChild: function alfresco_menus_AlfDropDownMenu__addChild(widget, insertIndex) {
         if (widget.domNode.tagName.toUpperCase() !== "TR")
         {
            // If the entry is not a table row then we will wrap it within one (provided by the
            // AlfMenuItemWrapper widget) such that the menu item is rendered correctly within the
            // menu.
            var itemToAdd = new AlfMenuItemWrapper({pubSubScope: widget.pubSubScope, item: widget });
            
            // Call the super class function with NEW arguments...
            this.inherited(arguments, [itemToAdd, insertIndex]);
         }
         else
         {
            // The element is a table row so we're going to assume that this is ok. However, if the
            // row does not contain 4 cells (or does not provide cells that span 4 column) then the
            // rendering could look "odd". This is because the default Dojo menu item is a table row
            // containing 4 columns.
            this.inherited(arguments);
         }
         this.setFirstAndLastMarkerClasses();
      },
      
      /**
       * This extends the super class implementation to allow keyboard navigation to traverse groups. If the
       * current focused child is the last element in the current group then the first item in the next group
       * will be focused (rather than iterating back around to the first item in the current group).
       * 
       * @instance
       */
      focusNext: function alfresco_menus_AlfDropDownMenu__focusNext() {
         var groupParent = this.getParent();
         if (domClass.contains(this.focusedChild.domNode, "last-focusable-entry"))
         {
            this.alfLog("log", "Focus first in next group");
            var nextSibling = groupParent._getSiblingOfChild(this, 1);
            while (nextSibling && typeof nextSibling.hasChildren === "function" && !nextSibling.hasChildren())
            {
               nextSibling = groupParent._getSiblingOfChild(nextSibling, 1);
            }
            if (nextSibling && typeof nextSibling.focusFirstChild === "function")
            {
               // Find a sibling that has a child to try and focus!
               nextSibling.focusFirstChild();
            }
            else
            {
               // Focus the first child of the first group that has children...
               groupParent._getFirstFocusableChild().focusFirstChild();
            }
         }
         else 
         {
            this.alfLog("log", "Focus next in current group");
            this.inherited(arguments);
         }
      },
      
      /**
       * This extends the super class implementation to allow keyboard navigation to traverse groups. If the
       * current focused child is the first element in the current group then the last item in the previous group
       * will be focused (rather than iterating back around to the last item in the current group).
       * 
       * @instance
       */
      focusPrev: function alfresco_menus_AlfDropDownMenu__focusPrev() {
         // Set up sensible variables for the various contexts...
         var groupParent = this.getParent(); 

         if (domClass.contains(this.focusedChild.domNode, "first-focusable-entry"))
         {
            this.alfLog("log", "Focus last in previous group");
            
            // The user is navigating up from the first entry in the group...
            // If this is the first group then we need to move to the LAST entry in the LAST group
            // Otherwise we need to move to the FIRST entry in the NEXT group...
            // Get my previous sibling...
            var previousSibling = groupParent._getSiblingOfChild(this, -1);
            while (previousSibling && typeof previousSibling.hasChildren === "function" && !previousSibling.hasChildren())
            {
               previousSibling = groupParent._getSiblingOfChild(previousSibling, -1);
            }
            if (previousSibling && typeof previousSibling.focusLastChild === "function")
            {
               // Focus on the last child of the previous sibling...
               previousSibling.focusLastChild();
            }
            else
            {
               // Focus the last child of the last group that has children...
               groupParent._getLastFocusableChild().focusLastChild();
            }
         }
         else
         {
            this.alfLog("log", "Focus previous in group");
            this.inherited(arguments);
         }
      },
      
      /**
       * Extends the default implementation to call the "setFirstAndLastMarkerClasses" classes after
       * the child has been removed.
       * 
       * @instance
       * @param {object} widget The child to remove
       */
      removeChild: function alfresco_menus_AlfDropDownMenu__removeChild(/*jshint unused:false*/ widget) {
         this.inherited(arguments);
         this.setFirstAndLastMarkerClasses();
      },
      
      /**
       * This function sets the class "first-focusable-entry" to the first child widget that is focusable and
       * "last-focusable-entry" to the last child widget that is focusable (the same child can be both). The purpose
       * of these markers is to help keyboard users to navigate between multiple groups in a menu.
       * 
       * It also adds "first-entry" and "last-entry" as well for use in styling.
       * 
       * @instance
       */
      setFirstAndLastMarkerClasses: function alfresco_menus_AlfDropDownMenu__setFirstAndLastMarkerClasses() {
         // See AKU-766 - it is necessary to place this code inside a try/catch block when used inside an iframe
         try
         {
            var noOfChildren = this.getChildren().length;
            if (noOfChildren > 0)
            {
               // Remove any previous first/last class markers...
               array.forEach(this.getChildren(), function(child) {
                  domClass.remove(child.domNode, "first-focusable-entry");
                  domClass.remove(child.domNode, "last-focusable-entry");
               });
               
               // Add the "first-focusable-entry" marker class to the first child that is focusable...
               array.some(this.getChildren(), function(child){
                  if (child.isFocusable())
                  {
                     domClass.add(child.domNode, "first-focusable-entry");
                     return true;
                  }
               });

               // Add the "last-focusable-entry" marker class to the last child that is focusable...
               var index = noOfChildren - 1,
                   children = this.getChildren(),
                   setLast = false;
               while (index >= 0 && !setLast)
               {
                  if (children[index].isFocusable())
                  {
                     domClass.add(children[index].domNode, "last-focusable-entry");
                     setLast = true;
                  }
                  index--;
               }
               
               // Add the "first-entry" and "last-entry" markers (these can be used for styling purposes, e.g.
               // not underlining the last menu item, etc
               domClass.add(children[0].domNode, "first-entry");
               domClass.add(children[noOfChildren-1].domNode, "last-entry");
            }
         }
         catch(e)
         {
            this.alfLog("warn", "It was not possible to set the first and last marker classes", this, e);
         }
      }
   });
});