Source: forms/controls/PushButtonsControl.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/>.
 */

/**
 * An input control that offers the functionality of radio-buttons or checkboxes (depending
 * on [the mode]{@link module:alfresco/forms/controls/PushButtonsControl#multiMode}, rendered as a
 * single control with push-button options.
 *
 * @module alfresco/forms/controls/PushButtonsControl
 * @extends external:dijit/_WidgetBase
 * @mixes external:dijit/_TemplatedMixin
 * @mixes external:dijit/_FocusMixin
 * @mixes module:alfresco/core/Core
 * @mixes module:alfresco/core/ObjectProcessingMixin
 * @author Martin Doyle
 * @since 1.0.44
 */
define(["alfresco/core/Core", 
        "alfresco/core/ObjectProcessingMixin", 
        "dijit/_FocusMixin", 
        "dijit/_TemplatedMixin", 
        "dijit/_WidgetBase", 
        "dojo/_base/array", 
        "dojo/_base/declare", 
        "dojo/_base/lang", 
        "dojo/dom-class", 
        "dojo/dom-construct", 
        "dojo/dom-style", 
        "dojo/on", 
        "dojo/text!./templates/PushButtonsControl.html"], 
        function(Core, ObjectProcessingMixin, _FocusMixin, _TemplatedMixin, _WidgetBase, array, declare, lang, domClass, domConstruct, domStyle, on, template) {

      return declare([_WidgetBase, _TemplatedMixin, _FocusMixin, Core, ObjectProcessingMixin], {

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

         /**
          * The root class of this widget
          *
          * @instance
          * @type {string}
          */
         rootClass: "alfresco-forms-controls-PushButtonsControl",

         /**
          * The HTML template to use for the widget.
          *
          * @override
          * @instance
          * @type {String}
          */
         templateString: template,

         /**
          * The ID of this control
          *
          * @instance
          * @type {string}
          * @default
          */
         id: null,

         /**
          * <p>The maximum number of choices selectable by the user. This will always
          * be one in single-item mode. Attempts to select an item once this limit
          * has been reached will result in the oldest selection being deselected.
          * If the value is 0 (the default), then there will be no forced limit on
          * the number of choices in multi-item mode.</p>
          *
          * <p><strong>NOTE:</strong> The accessibility of this control relies on
          * the fact that, underneath, it's backed by either radio buttons or
          * checkboxes. The use of this property can make checkboxes behave like
          * radio buttons, and so its accessibility could be affected.</p>
          *
          * @instance
          * @type {number}
          * @default
          * @since 1.0.65
          */
         maxChoices: 0,

         /**
          * Maximum number of buttons on a line (zero means no limit)
          *
          * @instance
          * @type {number}
          * @default
          */
         maxLineLength: 0,

         /**
          * The minimum padding to apply to the side of the buttons. This is
          * ignored on fixed-width controls.
          *
          * @instance
          * @type {number}
          * @default
          */
         minPadding: 50,

         /**
          * Whether to run the control in multi-value mode (i.e. behave like
          * checkboxs rather than the default radio-buttons behaviour).
          *
          * @instance
          * @type {boolean}
          * @default
          */
         multiMode: false,

         /**
          * The name of this control in the form
          *
          * @instance
          * @type {string}
          * @default
          */
         name: null,

         /**
          * Disable line-wrap on the labels if set to true.
          *
          * @instance
          * @type {boolean}
          * @default
          */
         noWrap: false,

         /**
          * Local store of the current control options, as an ordered
          * collection of option objects.
          *
          * @instance
          * @type {object[]}
          * @readonly
          * @default
          */
         opts: null,

         /**
          * The gap (margin) to have between buttons, expressed as a percentage
          * of the total width of the control.
          *
          * @instance
          * @type {number}
          * @default
          */
         percentGap: 1,

         /**
          * A FIFO queue of selections that is active only in multi-item mode when
          * maxChoices is not zero, and will be used to deselect oldest selections
          * when the maxChoices is exceeeded.
          *
          * @instance
          * @type {Object[]}
          * @default
          * @since 1.0.65
          */
         selections: null,

         /**
          * When the simple layout is selected control of the layout of buttons is surrendered
          * to the default behaviour. This means that buttons are not of equal widths and 
          * will fill the available space as best they can. Enabling this feature disables
          * [width]{@link module:alfresco/forms/controls/PushButtonsControl#width},
          * [percentGap]{@link module:alfresco/forms/controls/PushButtonsControl#percentGap},
          * [noWrap]{@link module:alfresco/forms/controls/PushButtonsControl#noWrap} and
          * [maxLineLength]{@link module:alfresco/forms/controls/PushButtonsControl#maxLineLength}.
          * 
          * @instance
          * @type {boolean}
          * @default
          * @since 1.0.72
          */
         simpleLayout: false,

         /**
          * The total width of the control in pixels. Zero indicates that
          * the control should take as much space as needed.
          *
          * @instance
          * @type {number}
          * @default
          */
         width: 0,

         /**
          * Constructor
          *
          * @instance
          */
         constructor: function() {
            this.opts = [];
            this.selections = [];
         },

         /**
          * Widget has been created (but not necessarily sub-widgets)
          *
          * @instance
          * @override
          */
         postCreate: function alfresco_forms_controls_PushButtonsControl__postCreate() {
            this.inherited(arguments);
            if (this.multiMode) {
               domClass.add(this.domNode, this.rootClass + "--multi-mode");
            }
            this.resize();
         },

         /**
          * Add an option to the control.
          *
          * @instance
          * @param {object} option The option to be added
          */
         addOption: function alfresco_forms_controls_PushButtonsControl__addOption(option) {

            // Duplicate values are unsupported
            var optionAlreadyExists = array.some(this.opts, function(opt) {
               return opt.option.value === option.value;
            });
            if (optionAlreadyExists) {
               this.alfLog("error", "Attempted to add option with duplicate value: ", option);
               return;
            }

            // Setup the DOM structure
            var optionId = this.id + "_" + this.generateUuid(),
               inputNode = domConstruct.create("input", {
                  name: this.name,
                  id: optionId,
                  className: this.rootClass + "__input",
                  type: this.multiMode ? "checkbox" : "radio",
                  value: option.value
               }, this.domNode),
               labelNode = domConstruct.create("label", {
                  "for": optionId, // Quoted because reserved word
                  className: this.rootClass + "__label"
               }, this.domNode),
               labelContentNode = domConstruct.create("div", {
                  className: this.rootClass + "__label__content"
               }, labelNode);

            // Add the label content (just text initially)
            var labelText = (option.label || option.name || option.value);
            labelContentNode.textContent = labelText;
            labelNode.setAttribute("title", labelText);

            // Setup change-listener
            var changeListener = on(inputNode, "change", lang.hitch(this, this._onInputChanged));
            this.own(changeListener);

            // Put the new option into the options-map
            this.opts.push({
               id: optionId,
               inputNode: inputNode,
               labelNode: labelNode,
               labelContentNode: labelContentNode,
               changeListener: changeListener,
               option: option
            });

            // Resize the buttons
            this.resize();
         },

         /**
          * Get the current options
          *
          * @instance
          * @override
          * @returns {object[]} The current options
          */
         getOptions: function alfresco_forms_controls_PushButtonsControl__getOptions() {
            return array.map(this.opts, function(opt) {
               return opt.option;
            });
         },

         /**
          * Get the current value(s) of this control
          *
          * @instance
          * @returns {object[]} The currently selected options
          */
         getValue: function alfresco_forms_controls_PushButtonsControl__getValue() {
            var values = [];
            array.forEach(this.opts, function(opt) {
               opt.inputNode.checked && values.push(opt.option.value);
            }, this);
            return this.multiMode ? values : values[0];
         },

         /**
          * Remove the selected option from the control.
          *
          * @instance
          * @param {object} option The option to be removed
          */
         removeOption: function alfresco_forms_controls_PushButtonsControl__removeOption(option) {

            // Remove the option
            this.opts = array.filter(this.opts, function(opt) {
               return opt.option !== option;
            });

            // Resize the buttons
            this.resize();
         },

         /**
          * Resize the control
          *
          * @instance
          */
         resize: function alfresco_forms_controls_PushButtonsControl__resize() {

            if (this.simpleLayout)
            {
               domClass.add(this.domNode, "alfresco-forms-controls-PushButtonsControl--simple-layout");
            }
            else
            {
               // Set overall control width
               domStyle.set(this.domNode, "width", isNaN(this.width) ? this.width : this.width + "px");
            
               // Set the width of each button
               var buttonsPerLine = this.maxLineLength ? Math.min(this.maxLineLength, this.opts.length) : this.opts.length;
               var gapsPerLine = buttonsPerLine - 1;
               var availButtonPercent = 100 - (gapsPerLine * this.percentGap);
               var buttonWidthPercent = availButtonPercent / buttonsPerLine;
               
               array.forEach(this.opts, function(opt, buttonIndex) {
                  var indexInRow = buttonIndex % buttonsPerLine;
                  var lastInRow = indexInRow + 1 === buttonsPerLine;
                  var isFirstRow = buttonIndex < buttonsPerLine;
                  domStyle.set(opt.labelNode, {
                     width: buttonWidthPercent + "%",
                     marginTop: isFirstRow ? 0 : this.percentGap + "%",
                     marginRight: lastInRow ? 0 : this.percentGap + "%",
                     whiteSpace: this.noWrap ? "nowrap" : "normal"
                  });
               }, this);

               // If width not hard-coded, set the width of the control based on the content plus padding
               if (!this.width) 
               {
                  var maxLabelWidth = 0;
                  array.forEach(this.opts, function(opt) {
                     maxLabelWidth = Math.max(maxLabelWidth, opt.labelContentNode.scrollWidth);
                  });
                  var minLineLength = (maxLabelWidth + this.minPadding) * buttonsPerLine / (availButtonPercent / 100);
                  domStyle.set(this.domNode, "width", Math.ceil(minLineLength) + "px");
               }
            }
         },

         /**
          * <p>Set the value of the control. When not in [multi-value mode]{@link module:alfresco/forms/controls/PushButtonControl#multiMode},
          * the value must should be a single value, rather than an array.</p>
          * 
          * <p>All values in the array must be valid values for the control's
          * current options, or this function will do nothing.</p>
          *
          * @instance
          * @param {*} value The value to be set
          */
         setValue: function alfresco_forms_controls_PushButtonsControl__setValue(value) {

            // Discard null, undefined and empty-string values
            if (value === null || typeof value === "undefined") {
               this.alfLog("warn", "setValue() not completed as invalid value passed to PushButtonsControl (" + value + ")");
               return;
            }

            // Make sure value is/isn't an array as appropriate
            var valueIsArray = value.constructor === Array;
            if (!this.multiMode && valueIsArray) {
               this.alfLog("warn", "setValue() not completed as value for single-value mode must not be array (value=" + JSON.stringify(value) + ")");
               return;
            }

            // Make sure the maxChoices value prevents setting too many values
            if (valueIsArray && this.maxChoices && value.length > this.maxChoices) {
               this.alfLog("warn", "setValue() not completed as number of values (" + value.length + ") exceeds max-choices setting (" + this.maxChoices + ")");
               return;
            }

            // Get valid values and normalise value to an array
            var validValues = array.map(this.opts, function(opt) {
                  return opt.option.value;
               }, this),
               valuesToUse = value;
            if (!valueIsArray) {
               valuesToUse = [value];
            }

            // Check that all new values are valid
            var hasInvalidValues = array.some(valuesToUse, function(nextValueToUse) {
               return !validValues.some(function(nextValidValue) {
                  return nextValidValue === nextValueToUse;
               });
            });
            if (hasInvalidValues) {
               this.alfLog("warn", "setValue() not completed as supplied value " + JSON.stringify(value) + " does not match current option values: " + JSON.stringify(validValues));
               return;
            }

            // Set the values (and if we're in multi-mode then we deselect the other values)
            array.forEach(this.opts, function(opt) {
               var checkedBefore = opt.inputNode.checked;
               if (this.multiMode) {
                  opt.inputNode.checked = false;
               }
               if (array.indexOf(valuesToUse, opt.option.value) !== -1) {
                  opt.inputNode.checked = true;
               }
               if (opt.inputNode.checked !== checkedBefore) {
                  on.emit(opt.inputNode, "change", {
                     bubbles: true,
                     cancelable: true
                  });
               }
            }, this);
         },

         /**
          * Fires when the value is changed.
          *
          * @instance
          * @param {Object} evt The change event
          * @since 1.0.65
          */
         _onInputChanged: function alfresco_forms_controls_PushButtonsControl___onInputChanged(evt) {
            var input = evt.target,
               isSelected = input.checked,
               nodeToDeselect;
            if (this.multiMode && this.maxChoices) {
               var newSelections = this.selections.slice();
               if (isSelected) {
                  newSelections.push(input);
                  if (newSelections.length > this.maxChoices) {
                     nodeToDeselect = newSelections.shift();
                     nodeToDeselect.checked = false;
                     on.emit(nodeToDeselect, "change", {
                        bubbles: true,
                        cancelable: true
                     });
                  }
               } else {
                  newSelections = newSelections.filter(function(selectedNode) {
                     return selectedNode !== input;
                  });
               }
               this.selections = newSelections;
            }
            this._changeAttrValue("value", this.getValue());
         }
      });
   }
);