Source: menus/AlfCheckableMenuItem.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 is an extension of the [standard menu item]{@link module:alfresco/menus/AlfMenuItem} where the 
 * menu item has on and off toggle state represented by a check mark. The menu items can be placed in a group
 * so that only one item within the group can be checked at a time or can be used as individual items for toggling
 * things on or off. Each time the a menu item is actioned it will publish on the configured topic and the payload
 * will include a "selected" attribute indicating whether or not the item has been toggled to the on or off state.
 * Items can be grouped by simply assigning multiple items with the same 
 * [group]{@link module:alfresco/menus/AlfCheckableMenuItem#group} value. The other items in the group will automatically
 * be toggled to the off state when another item is toggled to the on state.</p>
 * 
 * @example <caption>This is an example configuration of an ungrouped, initially checked item:</caption>
 * {
 *    name: "alfresco/menus/AlfCheckableMenuItem",
 *    config: {
 *       label: "Checkable",
 *       value: "SOME_VALUE",
 *       checked: true,
 *       publishTopic: "CUSTOM_TOPIC",
 *       publishPayload: {
 *          additional: "bonus data"
 *       }
 *    }
 * }
 *
 * @example <caption>This is an example configuration of a grouped item:</caption>
 * {
 *    name: "alfresco/menus/AlfCheckableMenuItem",
 *    config: {
 *       label: "Grouped Checkable",
 *       value: "SOME_VALUE",
 *       group: "GROUP1",
 *       publishTopic: "CUSTOM_TOPIC"
 *    }
 * }
 * 
 * @module alfresco/menus/AlfCheckableMenuItem
 * @extends module:alfresco/menus/AlfMenuItem
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "alfresco/menus/AlfMenuItem",
        "dojo/_base/lang",
        "dojo/_base/event",
        "dojo/dom-construct",
        "dojo/dom-class",
        "dojo/dom-style",
        "alfresco/util/hashUtils",
        "dojo/io-query"],
        function(declare, AlfMenuItem, lang, event, domConstruct, domClass, domStyle, hashUtils, ioQuery) {

   return declare([AlfMenuItem], {

      /**
       * An array of the CSS files to use with this widget.
       *
       * @instance
       * @type {object[]}
       * @default [{cssFile:"./css/AlfCheckableMenuItem.css"}]
       */
      cssRequirements: [{cssFile:"./css/AlfCheckableMenuItem.css"}],

      /**
       * A reference to the cell that will be created by postCreate to handle toggling selection
       * 
       * @instance
       * @type {object}
       * @default
       */
      checkCell: null,

      /**
       * Indicates whether or not the menu item is selected or not
       * 
       * @instance
       * @type {boolean}
       * @default
       */
      checked: false,

      /**
       * The class to apply to render some checked indicator
       * 
       * @instance
       * @type {string}
       * @default
       */
      checkedIconClass: "alfresco-menus-AlfCheckableMenuItem--checked",

      /**
       * If this is set then it indicates that this item is a member of a group. When in a group this cannot be unselected. Unselection
       * will only occur when another item in the group is selected. Setting this attribute to anything other than null makes this
       * item behave as though it were a member of a radio button group.
       *
       * @instance
       * @type {string}
       * @default
       */
      group: null,

      /**
       * The payload that will be published when the item is selected
       *
       * @instance
       * @type {object}
       * @default
       */
      publishPayload: null,

      /**
       * A handle to the publication topic. This is set initially in post create and then removed
       * just before publishing to prevent a widget handling its own generated event
       *
       * @instance
       * @type {object}
       * @default
       */
      subscriptionHandle: null,

      /**
       * If this instance should be set according to a hash fragment value then this attribute should be
       * set to the name of the fragment parameter to use. If the hash fragment parameter exists and its
       * value matches the current value then this will automatically be checked.
       *
       * @instance
       * @type {string}
       * @default
       */
      hashName: null,

      /**
       * @instance
       */
      postCreate: function alfresco_menus_AlfCheckableMenuItem__postCreate() {
         // jshint maxcomplexity:false
         domClass.add(this.domNode, "alfresco-menus-AlfCheckableMenuItem");

         // Set the correct role...
         if (this.group)
         {
            // If a group has been assigned then only one menu item will be checkable at a time within the group
            // so assign the role of "menuitemradio" (https://www.w3.org/TR/wai-aria/roles#menuitemradio)
            this.domNode.setAttribute("role", "menuitemradio");
         }
         else
         {
            // If a group has NOT been assigned then this menu item can be toggled on and off independently so it
            // should be given the role of "menuitemcheckbox" (https://www.w3.org/TR/wai-aria/roles#menuitemcheckbox)
            this.domNode.setAttribute("role", "menuitemcheckbox");
         }
         
         if (!this.publishPayload)
         {
            this.publishPayload = {};
         }

         // If configured to use a hashName and that hashName is on the 
         if (this.hashName)
         {
            var currHash = hashUtils.getHash();
            if (currHash[this.hashName])
            {
               // If the hashName is present in the URL hash, set the checked state based on the URL...
               this.checked = currHash[this.hashName] === this.value;
            }
            else if (this.checked)
            {
               // ...but if the hashName is NOT present in the URL hash, but the menu item has been 
               // configured to be checked then update the URL hash...
               currHash[this.hashName] = this.value;
               this.alfPublish("ALF_NAVIGATE_TO_PAGE", {
                  url: ioQuery.objectToQuery(currHash),
                  type: "HASH"
               }, true);
            }
         }

         this.alfLog("log", "Create checkable cell");
         this.checkCell = domConstruct.create("td", { className: "alfresco-menus-AlfCheckableMenuItem__icon", innerHTML: " "}, this.focusNode, "first");

         if (this.checked)
         {
            domClass.add(this.domNode, this.checkedIconClass);
         }

         if (this.group)
         {
            this.alfSubscribe("ALF_CHECKABLE_MENU_ITEM__" + this.group, lang.hitch(this, this.onGroupSelection));
         }

         this.inherited(arguments);

         if (this.checked)
         {
            if (this.group)
            {
                // publish an event to indicate that all other members of the group must be deselected
                this.alfPublish("ALF_CHECKABLE_MENU_ITEM__" + this.group, {
                    value : this.value
                });
            }
            this.publishSelection();
         }

         // If a publish topic has been provided then we also want to subscribe to that topic to ensure that
         // we can reflect changes that are made outside of this widget (e.g. if the selection is updated by
         // some other widget).
         if (this.publishTopic)
         {
            this.subscriptionHandle = this.alfSubscribe(this.publishTopic, lang.hitch(this, this.onPublishTopicEvent));
         }

         // Set the appropriate initial aria-state for the role type...
         this.domNode.setAttribute(this.group ? "aria-selected": "aria-checked", this.checked);
      },

      /**
       * Handles the events published on the publish topic. This allows the widget to update itself based on
       * events generated by other widgets.
       *
       * @instance
       * @param {object} payload The payload published on the subscribed topic
       */
      onPublishTopicEvent: function alfresco_menus_AlfCheckableMenuItem__onPublishTopicEvent(payload) {
         if (payload &&
             payload.selected !== null &&
             payload.value === this.value)
         {
            if (this.group)
            {
               if (payload.selected === true)
               {
                  // Clear all group settings if the payload value matches the value represented by the
                  // widget. We need to ensure that only one item in the group is selected...
                  this.alfPublish("ALF_CHECKABLE_MENU_ITEM__" + this.group, {
                     value: this.value
                  });
                  this.checked = true;
               }
               else
               {
                  this.checked = false;
               }
            }
            else
            {
               this.checked = payload.selected;
            }
            this.render();
         }
      },

      /**
       * Overrides the default value provided by the _AlfMenuItemMixin
       * @instance
       * @type {boolean}
       * @default
       */
      closeOnClick: true,

      /**
       * @instance
       * @param {object} evt The click event
       */
      onClick: function alfresco_menus_AlfCheckableMenuItem__onClick(evt) {
         // Emit the event to close popups in the stack...
         this.emitClosePopupEvent();
         this.toggleSelection(evt);
      },

      /**
       * @instance
       * @param {object} evt The click event that resulted in the toggle
       */
      toggleSelection: function alfresco_menus_AlfCheckableMenuItem__toggleSelection(evt) {
         this.alfLog("log", "Toggle selection");
         event.stop(evt);

         if (this.group)
         {

            // If this is the member of a group we need to handle things a bit differently...
            if (!this.checked)
            {
               // If this is currently NOT checked, then allow selection to occur but publish
               // an event to indicate that all other members of the group must be deselected
               this.alfPublish("ALF_CHECKABLE_MENU_ITEM__" + this.group, {
                   value : this.value
               });
               this.checked = true;
               this.render();
               this.publishSelection();
            }

            // Set the appropriate aria-state for the role type...
            this.domNode.setAttribute("aria-selected", this.checked);
         }
         else
         {
            // Perform the toggle...
            this.checked = !this.checked;
            this.render();
            this.publishSelection();

            // Set the appropriate aria-state for the role type...
            this.domNode.setAttribute(this.group ? "aria-selected": "aria-checked", this.checked);
         }

         // Clicking on the check cell will result in the menu item being marked as selected
         // but we want to ensure that this is not the case so always remove the class that
         // indicates selection...
         if (this.checkCell && this.checkCell.parentNode)
         {
            domClass.remove(this.checkCell.parentNode, "dijitMenuItemSelected");
         }
      },

      /**
       * Renders the menu item appropriately according to the current checked value. By default
       * this simply adds or removes the 'checkedIconClass' from the checkCell appropriately
       *
       * @instance
       */
      render: function alfresco_menus_AlfCheckableMenuItem__render() {
         if (this.checked)
         {
            domClass.add(this.domNode, this.checkedIconClass);
         }
         else
         {
            domClass.remove(this.domNode, this.checkedIconClass);
         }
      },

      /**
       * Handles publishing the selection event.
       *
       * @instance
       */
      publishSelection: function alfresco_menus_AlfCheckableMenuItem_publishSelection() {
         this.alfLog("log", "Publishing selection", this.publishTopic, this.value, this.checked);
         if (this.publishTopic)
         {
            // Perform publish...
            this.publishPayload.selected = this.checked;
            this.publishPayload.value = this.value;
            this.publishPayload.label = this.label;

            this.alfPublish(this.publishTopic, this.publishPayload);
         }
      },

      /**
       * This function is called when a topic is published indicating that another member of the group
       * has been selected. The payload is irrelevant, the menu item should be marked as deslected.
       * @instance
       * @param {object} payload
       */
      onGroupSelection: function alfresco_menus_AlfCheckableMenuItem__toggleSelection(payload) {
         if (payload.value !== this.value)
         {
            this.domNode.setAttribute("aria-selected", false);
            this.checked = false;
            this.render();
         } 
      }
   });
});