MDL-32843 import YUI 3.5.1
[moodle.git] / lib / yui / 3.5.1 / build / widget-buttons / widget-buttons.js
blobe882cb4d8eeaa717e3619e79128055170d9d0272
1 /*
2 YUI 3.5.1 (build 22)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('widget-buttons', function(Y) {
9 /**
10 Provides header/body/footer button support for Widgets that use the
11 `WidgetStdMod` extension.
13 @module widget-buttons
14 @since 3.4.0
15 **/
17 var YArray  = Y.Array,
18     YLang   = Y.Lang,
19     YObject = Y.Object,
21     ButtonPlugin = Y.Plugin.Button,
22     Widget       = Y.Widget,
23     WidgetStdMod = Y.WidgetStdMod,
25     getClassName = Y.ClassNameManager.getClassName,
26     isArray      = YLang.isArray,
27     isNumber     = YLang.isNumber,
28     isString     = YLang.isString,
29     isValue      = YLang.isValue;
31 // Utility to determine if an object is a Y.Node instance, even if it was
32 // created in a different YUI sandbox.
33 function isNode(node) {
34     return !!node.getDOMNode;
37 /**
38 Provides header/body/footer button support for Widgets that use the
39 `WidgetStdMod` extension.
41 This Widget extension makes it easy to declaratively configure a widget's
42 buttons. It adds a `buttons` attribute along with button- accessor and mutator
43 methods. All button nodes have the `Y.Plugin.Button` plugin applied.
45 This extension also includes `HTML_PARSER` support to seed a widget's `buttons`
46 from those which already exist in its DOM.
48 @class WidgetButtons
49 @extensionfor Widget
50 @since 3.4.0
51 **/
52 function WidgetButtons() {
53     // Require `Y.WidgetStdMod`.
54     if (!this._stdModNode) {
55         Y.error('WidgetStdMod must be added to a Widget before WidgetButtons.');
56     }
58     // Has to be setup before the `initializer()`.
59     this._buttonsHandles = {};
62 WidgetButtons.ATTRS = {
63     /**
64     Collection containing a widget's buttons.
66     The collection is an Object which contains an Array of `Y.Node`s for every
67     `WidgetStdMod` section (header, body, footer) which has one or more buttons.
68     All button nodes have the `Y.Plugin.Button` plugin applied.
70     This attribute is very flexible in the values it will accept. `buttons` can
71     be specified as a single Array, or an Object of Arrays keyed to a particular
72     section.
74     All specified values will be normalized to this type of structure:
76         {
77             header: [...],
78             footer: [...]
79         }
81     A button can be specified as a `Y.Node`, config Object, or String name for a
82     predefined button on the `BUTTONS` prototype property. When a config Object
83     is provided, it will be merged with any defaults provided by a button with
84     the same `name` defined on the `BUTTONS` property.
86     See `addButton()` for the detailed list of configuration properties.
88     For convenience, a widget's buttons will always persist and remain rendered
89     after header/body/footer content updates. Buttons should be removed by
90     updating this attribute or using the `removeButton()` method.
92     @example
93         {
94             // Uses predefined "close" button by string name.
95             header: ['close'],
97             footer: [
98                 {
99                     name  : 'cancel',
100                     label : 'Cancel',
101                     action: 'hide'
102                 },
104                 {
105                     name     : 'okay',
106                     label    : 'Okay',
107                     isDefault: true,
109                     events: {
110                         click: function (e) {
111                             this.hide();
112                         }
113                     }
114                 }
115             ]
116         }
118     @attribute buttons
119     @type Object
120     @default {}
121     @since 3.4.0
122     **/
123     buttons: {
124         getter: '_getButtons',
125         setter: '_setButtons',
126         value : {}
127     },
129     /**
130     The current default button as configured through this widget's `buttons`.
132     A button can be configured as the default button in the following ways:
134       * As a config Object with an `isDefault` property:
135         `{label: 'Okay', isDefault: true}`.
137       * As a Node with a `data-default` attribute:
138         `<button data-default="true">Okay</button>`.
140     This attribute is **read-only**; anytime there are changes to this widget's
141     `buttons`, the `defaultButton` will be updated if needed.
143     **Note:** If two or more buttons are configured to be the default button,
144     the last one wins.
146     @attribute defaultButton
147     @type Node
148     @default null
149     @readOnly
150     @since 3.5.0
151     **/
152     defaultButton: {
153         readOnly: true,
154         value   : null
155     }
159 CSS classes used by `WidgetButtons`.
161 @property CLASS_NAMES
162 @type Object
163 @static
164 @since 3.5.0
166 WidgetButtons.CLASS_NAMES = {
167     button : getClassName('button'),
168     buttons: Widget.getClassName('buttons'),
169     primary: getClassName('button', 'primary')
172 WidgetButtons.HTML_PARSER = {
173     buttons: function (srcNode) {
174         return this._parseButtons(srcNode);
175     }
179 The list of button configuration properties which are specific to
180 `WidgetButtons` and should not be passed to `Y.Plugin.Button.createNode()`.
182 @property NON_BUTTON_NODE_CFG
183 @type Array
184 @static
185 @since 3.5.0
187 WidgetButtons.NON_BUTTON_NODE_CFG = [
188     'action', 'classNames', 'context', 'events', 'isDefault', 'section'
191 WidgetButtons.prototype = {
192     // -- Public Properties ----------------------------------------------------
194     /**
195     Collection of predefined buttons mapped by name -> config.
197     These button configurations will serve as defaults for any button added to a
198     widget's buttons which have the same `name`.
200     See `addButton()` for a list of possible configuration values.
202     @property BUTTONS
203     @type Object
204     @default {}
205     @see addButton()
206     @since 3.5.0
207     **/
208     BUTTONS: {},
210     /**
211     The HTML template to use when creating the node which wraps all buttons of a
212     section. By default it will have the CSS class: "yui3-widget-buttons".
214     @property BUTTONS_TEMPLATE
215     @type String
216     @default "<span />"
217     @since 3.5.0
218     **/
219     BUTTONS_TEMPLATE: '<span />',
221     /**
222     The default section to render buttons in when no section is specified.
224     @property DEFAULT_BUTTONS_SECTION
225     @type String
226     @default Y.WidgetStdMod.FOOTER
227     @since 3.5.0
228     **/
229     DEFAULT_BUTTONS_SECTION: WidgetStdMod.FOOTER,
231     // -- Protected Properties -------------------------------------------------
233     /**
234     A map of button node `_yuid` -> event-handle for all button nodes which were
235     created by this widget.
237     @property _buttonsHandles
238     @type Object
239     @protected
240     @since 3.5.0
241     **/
243     /**
244     A map of this widget's `buttons`, both name -> button and
245     section:name -> button.
247     @property _buttonsMap
248     @type Object
249     @protected
250     @since 3.5.0
251     **/
253     /**
254     Internal reference to this widget's default button.
256     @property _defaultButton
257     @type Node
258     @protected
259     @since 3.5.0
260     **/
262     // -- Lifecycle Methods ----------------------------------------------------
264     initializer: function () {
265         // Creates button mappings and sets the `defaultButton`.
266         this._mapButtons(this.get('buttons'));
267         this._updateDefaultButton();
269         // Bound with `Y.bind()` to make more extensible.
270         this.after('buttonsChange', Y.bind('_afterButtonsChange', this));
272         Y.after(this._bindUIButtons, this, 'bindUI');
273         Y.after(this._syncUIButtons, this, 'syncUI');
274     },
276     destructor: function () {
277         // Detach all event subscriptions this widget added to its `buttons`.
278         YObject.each(this._buttonsHandles, function (handle) {
279             handle.detach();
280         });
282         delete this._buttonsHandles;
283         delete this._buttonsMap;
284         delete this._defaultButton;
285     },
287     // -- Public Methods -------------------------------------------------------
289     /**
290     Adds a button to this widget.
292     The new button node will have the `Y.Plugin.Button` plugin applied, be added
293     to this widget's `buttons`, and rendered in the specified `section` at the
294     specified `index` (or end of the section when no `index` is provided). If
295     the section does not exist, it will be created.
297     This fires the `buttonsChange` event and adds the following properties to
298     the event facade:
300       * `button`: The button node or config object to add.
302       * `section`: The `WidgetStdMod` section (header/body/footer) where the
303         button will be added.
305       * `index`: The index at which the button will be in the section.
307       * `src`: "add"
309     **Note:** The `index` argument will be passed to the Array `splice()`
310     method, therefore a negative value will insert the `button` that many items
311     from the end. The `index` property on the `buttonsChange` event facade is
312     the index at which the `button` was added.
314     @method addButton
315     @param {Node|Object|String} button The button to add. This can be a `Y.Node`
316         instance, config Object, or String name for a predefined button on the
317         `BUTTONS` prototype property. When a config Object is provided, it will
318         be merged with any defaults provided by any `srcNode` and/or a button
319         with the same `name` defined on the `BUTTONS` property. The following
320         are the possible configuration properties beyond what Node plugins
321         accept by default:
322       @param {Function|String} [button.action] The default handler that should
323         be called when the button is clicked. A String name of a Function that
324         exists on the `context` object can also be provided. **Note:**
325         Specifying a set of `events` will override this setting.
326       @param {String|String[]} [button.classNames] Additional CSS classes to add
327         to the button node.
328       @param {Object} [button.context=this] Context which any `events` or
329         `action` should be called with. Defaults to `this`, the widget.
330         **Note:** `e.target` will access the button node in the event handlers.
331       @param {Boolean} [button.disabled=false] Whether the button should be
332         disabled.
333       @param {String|Object} [button.events="click"] Event name, or set of
334         events and handlers to bind to the button node. **See:** `Y.Node.on()`,
335         this value is passed as the first argument to `on()`.
336       @param {Boolean} [button.isDefault=false] Whether the button is the
337         default button.
338       @param {String} [button.label] The visible text/value displayed in the
339         button.
340       @param {String} [button.name] A name which can later be used to reference
341         this button. If a button is defined on the `BUTTONS` property with this
342         same name, its configuration properties will be merged in as defaults.
343       @param {String} [button.section] The `WidgetStdMod` section (header, body,
344         footer) where the button should be added.
345       @param {Node} [button.srcNode] An existing Node to use for the button,
346         default values will be seeded from this node, but are overriden by any
347         values specified in the config object. By default a new &lt;button&gt;
348         node will be created.
349       @param {String} [button.template] A specific template to use when creating
350         a new button node (e.g. "&lt;a /&gt;"). **Note:** Specifying a `srcNode`
351         will overide this.
352     @param {String} [section="footer"] The `WidgetStdMod` section
353         (header/body/footer) where the button should be added. This takes
354         precedence over the `button.section` configuration property.
355     @param {Number} [index] The index at which the button should be inserted. If
356         not specified, the button will be added to the end of the section. This
357         value is passed to the Array `splice()` method, therefore a negative
358         value will insert the `button` that many items from the end.
359     @chainable
360     @see Plugin.Button.createNode()
361     @since 3.4.0
362     **/
363     addButton: function (button, section, index) {
364         var buttons = this.get('buttons'),
365             sectionButtons, atIndex;
367         // Makes sure we have the full config object.
368         if (!isNode(button)) {
369             button = this._mergeButtonConfig(button);
370             section || (section = button.section);
371         }
373         section || (section = this.DEFAULT_BUTTONS_SECTION);
374         sectionButtons = buttons[section] || (buttons[section] = []);
375         isNumber(index) || (index = sectionButtons.length);
377         // Insert new button at the correct position.
378         sectionButtons.splice(index, 0, button);
380         // Determine the index at which the `button` now exists in the array.
381         atIndex = YArray.indexOf(sectionButtons, button);
383         this.set('buttons', buttons, {
384             button : button,
385             section: section,
386             index  : atIndex,
387             src    : 'add'
388         });
390         return this;
391     },
393     /**
394     Returns a button node from this widget's `buttons`.
396     @method getButton
397     @param {Number|String} name The string name or index of the button.
398     @param {String} [section="footer"] The `WidgetStdMod` section
399         (header/body/footer) where the button exists. Only applicable when
400         looking for a button by numerical index, or by name but scoped to a
401         particular section.
402     @return {Node} The button node.
403     @since 3.5.0
404     **/
405     getButton: function (name, section) {
406         if (!isValue(name)) { return; }
408         var map = this._buttonsMap,
409             buttons;
411         section || (section = this.DEFAULT_BUTTONS_SECTION);
413         // Supports `getButton(1, 'header')` signature.
414         if (isNumber(name)) {
415             buttons = this.get('buttons');
416             return buttons[section] && buttons[section][name];
417         }
419         // Looks up button by name or section:name.
420         return arguments.length > 1 ? map[section + ':' + name] : map[name];
421     },
423     /**
424     Removes a button from this widget.
426     The button will be removed from this widget's `buttons` and its DOM. Any
427     event subscriptions on the button which were created by this widget will be
428     detached. If the content section becomes empty after removing the button
429     node, then the section will also be removed.
431     This fires the `buttonsChange` event and adds the following properties to
432     the event facade:
434       * `button`: The button node to remove.
436       * `section`: The `WidgetStdMod` section (header/body/footer) where the
437         button should be removed from.
439       * `index`: The index at which the button exists in the section.
441       * `src`: "remove"
443     @method removeButton
444     @param {Node|Number|String} button The button to remove. This can be a
445         `Y.Node` instance, index, or String name of a button.
446     @param {String} [section="footer"] The `WidgetStdMod` section
447         (header/body/footer) where the button exists. Only applicable when
448         removing a button by numerical index, or by name but scoped to a
449         particular section.
450     @chainable
451     @since 3.5.0
452     **/
453     removeButton: function (button, section) {
454         if (!isValue(button)) { return this; }
456         var buttons = this.get('buttons'),
457             index;
459         // Shortcut if `button` is already an index which is needed for slicing.
460         if (isNumber(button)) {
461             section || (section = this.DEFAULT_BUTTONS_SECTION);
462             index  = button;
463             button = buttons[section][index];
464         } else {
465             // Supports `button` being the string name.
466             if (isString(button)) {
467                 // `getButton()` is called this way because its behavior is
468                 // different based on the number of arguments.
469                 button = this.getButton.apply(this, arguments);
470             }
472             // Determines the `section` and `index` at which the button exists.
473             YObject.some(buttons, function (sectionButtons, currentSection) {
474                 index = YArray.indexOf(sectionButtons, button);
476                 if (index > -1) {
477                     section = currentSection;
478                     return true;
479                 }
480             });
481         }
483         // Button was found at an appropriate index.
484         if (button && index > -1) {
485             // Remove button from `section` array.
486             buttons[section].splice(index, 1);
488             this.set('buttons', buttons, {
489                 button : button,
490                 section: section,
491                 index  : index,
492                 src    : 'remove'
493             });
494         }
496         return this;
497     },
499     // -- Protected Methods ----------------------------------------------------
501     /**
502     Binds UI event listeners. This method is inserted via AOP, and will execute
503     after `bindUI()`.
505     @method _bindUIButtons
506     @protected
507     @since 3.4.0
508     **/
509     _bindUIButtons: function () {
510         // Event handlers are bound with `bind()` to make them more extensible.
512         var afterContentChange = Y.bind('_afterContentChangeButtons', this);
514         this.after({
515             defaultButtonChange: Y.bind('_afterDefaultButtonChange', this),
516             visibleChange      : Y.bind('_afterVisibleChangeButtons', this),
517             headerContentChange: afterContentChange,
518             bodyContentChange  : afterContentChange,
519             footerContentChange: afterContentChange
520         });
521     },
523     /**
524     Returns a button node based on the specified `button` node or configuration.
526     The button node will either be created via `Y.Plugin.Button.createNode()`,
527     or when `button` is specified as a node already, it will by `plug()`ed with
528     `Y.Plugin.Button`.
530     @method _createButton
531     @param {Node|Object} button Button node or configuration object.
532     @return {Node} The button node.
533     @protected
534     @since 3.5.0
535     **/
536     _createButton: function (button) {
537         var config, buttonConfig, nonButtonNodeCfg,
538             i, len, action, context, handle;
540         // Makes sure the exiting `Y.Node` instance is from this YUI sandbox and
541         // is plugged with `Y.Plugin.Button`.
542         if (isNode(button)) {
543             return Y.one(button.getDOMNode()).plug(ButtonPlugin);
544         }
546         // Merge `button` config with defaults and back-compat.
547         config = Y.merge({
548             context: this,
549             events : 'click',
550             label  : button.value
551         }, button);
553         buttonConfig     = Y.merge(config);
554         nonButtonNodeCfg = WidgetButtons.NON_BUTTON_NODE_CFG;
556         // Remove all non-button Node config props.
557         for (i = 0, len = nonButtonNodeCfg.length; i < len; i += 1) {
558             delete buttonConfig[nonButtonNodeCfg[i]];
559         }
561         // Create the button node using the button Node-only config.
562         button = ButtonPlugin.createNode(buttonConfig);
564         context = config.context;
565         action  = config.action;
567         // Supports `action` as a String name of a Function on the `context`
568         // object.
569         if (isString(action)) {
570             action = Y.bind(action, context);
571         }
573         // Supports all types of crazy configs for event subscriptions and
574         // stores a reference to the returned `EventHandle`.
575         handle = button.on(config.events, action, context);
576         this._buttonsHandles[Y.stamp(button, true)] = handle;
578         // Tags the button with the configured `name` and `isDefault` settings.
579         button.setData('name', this._getButtonName(config));
580         button.setData('default', this._getButtonDefault(config));
582         // Add any CSS classnames to the button node.
583         YArray.each(YArray(config.classNames), button.addClass, button);
585         return button;
586     },
588     /**
589     Returns the buttons container for the specified `section`, passing a truthy
590     value for `create` will create the node if it does not already exist.
592     **Note:** It is up to the caller to properly insert the returned container
593     node into the content section.
595     @method _getButtonContainer
596     @param {String} section The `WidgetStdMod` section (header/body/footer).
597     @param {Boolean} create Whether the buttons container should be created if
598         it does not already exist.
599     @return {Node} The buttons container node for the specified `section`.
600     @protected
601     @see BUTTONS_TEMPLATE
602     @since 3.5.0
603     **/
604     _getButtonContainer: function (section, create) {
605         var sectionClassName = WidgetStdMod.SECTION_CLASS_NAMES[section],
606             buttonsClassName = WidgetButtons.CLASS_NAMES.buttons,
607             contentBox       = this.get('contentBox'),
608             containerSelector, container;
610         // Search for an existing buttons container within the section.
611         containerSelector = '.' + sectionClassName + ' .' + buttonsClassName;
612         container         = contentBox.one(containerSelector);
614         // Create the `container` if it doesn't already exist.
615         if (!container && create) {
616             container = Y.Node.create(this.BUTTONS_TEMPLATE);
617             container.addClass(buttonsClassName);
618         }
620         return container;
621     },
623     /**
624     Returns whether or not the specified `button` is configured to be the
625     default button.
627     When a button node is specified, the button's `getData()` method will be
628     used to determine if the button is configured to be the default. When a
629     button config object is specified, the `isDefault` prop will determine
630     whether the button is the default.
632     **Note:** `<button data-default="true"></button>` is supported via the
633     `button.getData('default')` API call.
635     @method _getButtonDefault
636     @param {Node|Object} button The button node or configuration object.
637     @return {Boolean} Whether the button is configured to be the default button.
638     @protected
639     @since 3.5.0
640     **/
641     _getButtonDefault: function (button) {
642         var isDefault = isNode(button) ?
643                 button.getData('default') : button.isDefault;
645         if (isString(isDefault)) {
646             return isDefault.toLowerCase() === 'true';
647         }
649         return !!isDefault;
650     },
652     /**
653     Returns the name of the specified `button`.
655     When a button node is specified, the button's `getData('name')` method is
656     preferred, but will fallback to `get('name')`, and the result will determine
657     the button's name. When a button config object is specified, the `name` prop
658     will determine the button's name.
660     **Note:** `<button data-name="foo"></button>` is supported via the
661     `button.getData('name')` API call.
663     @method _getButtonName
664     @param {Node|Object} button The button node or configuration object.
665     @return {String} The name of the button.
666     @protected
667     @since 3.5.0
668     **/
669     _getButtonName: function (button) {
670         var name;
672         if (isNode(button)) {
673             name = button.getData('name') || button.get('name');
674         } else {
675             name = button && (button.name || button.type);
676         }
678         return name;
679     },
681     /**
682     Getter for the `buttons` attribute. A copy of the `buttons` object is
683     returned so the stored state cannot be modified by the callers of
684     `get('buttons')`.
686     This will recreate a copy of the `buttons` object, and each section array
687     (the button nodes are *not* copied/cloned.)
689     @method _getButtons
690     @param {Object} buttons The widget's current `buttons` state.
691     @return {Object} A copy of the widget's current `buttons` state.
692     @protected
693     @since 3.5.0
694     **/
695     _getButtons: function (buttons) {
696         var buttonsCopy = {};
698         // Creates a new copy of the `buttons` object.
699         YObject.each(buttons, function (sectionButtons, section) {
700             // Creates of copy of the array of button nodes.
701             buttonsCopy[section] = sectionButtons.concat();
702         });
704         return buttonsCopy;
705     },
707     /**
708     Adds the specified `button` to the buttons map (both name -> button and
709     section:name -> button), and sets the button as the default if it is
710     configured as the default button.
712     **Note:** If two or more buttons are configured with the same `name` and/or
713     configured to be the default button, the last one wins.
715     @method _mapButton
716     @param {Node} button The button node to map.
717     @param {String} section The `WidgetStdMod` section (header/body/footer).
718     @protected
719     @since 3.5.0
720     **/
721     _mapButton: function (button, section) {
722         var map       = this._buttonsMap,
723             name      = this._getButtonName(button),
724             isDefault = this._getButtonDefault(button);
726         if (name) {
727             // name -> button
728             map[name] = button;
730             // section:name -> button
731             map[section + ':' + name] = button;
732         }
734         isDefault && (this._defaultButton = button);
735     },
737     /**
738     Adds the specified `buttons` to the buttons map (both name -> button and
739     section:name -> button), and set the a button as the default if one is
740     configured as the default button.
742     **Note:** This will clear all previous button mappings and null-out any
743     previous default button! If two or more buttons are configured with the same
744     `name` and/or configured to be the default button, the last one wins.
746     @method _mapButtons
747     @param {Node[]} buttons The button nodes to map.
748     @protected
749     @since 3.5.0
750     **/
751     _mapButtons: function (buttons) {
752         this._buttonsMap    = {};
753         this._defaultButton = null;
755         YObject.each(buttons, function (sectionButtons, section) {
756             var i, len;
758             for (i = 0, len = sectionButtons.length; i < len; i += 1) {
759                 this._mapButton(sectionButtons[i], section);
760             }
761         }, this);
762     },
764     /**
765     Returns a copy of the specified `config` object merged with any defaults
766     provided by a `srcNode` and/or a predefined configuration for a button
767     with the same `name` on the `BUTTONS` property.
769     @method _mergeButtonConfig
770     @param {Object|String} config Button configuration object, or string name.
771     @return {Object} A copy of the button configuration object merged with any
772         defaults.
773     @protected
774     @since 3.5.0
775     **/
776     _mergeButtonConfig: function (config) {
777         var buttonConfig, defConfig, name, button, tagName, label;
779         // Makes sure `config` is an Object and a copy of the specified value.
780         config = isString(config) ? {name: config} : Y.merge(config);
782         // Seeds default values from the button node, if there is one.
783         if (config.srcNode) {
784             button  = config.srcNode;
785             tagName = button.get('tagName').toLowerCase();
786             label   = button.get(tagName === 'input' ? 'value' : 'text');
788             // Makes sure the button's current values override any defaults.
789             buttonConfig = {
790                 disabled : !!button.get('disabled'),
791                 isDefault: this._getButtonDefault(button),
792                 name     : this._getButtonName(button)
793             };
795             // Label should only be considered when not an empty string.
796             label && (buttonConfig.label = label);
798             // Merge `config` with `buttonConfig` values.
799             Y.mix(config, buttonConfig, false, null, 0, true);
800         }
802         name      = this._getButtonName(config);
803         defConfig = this.BUTTONS && this.BUTTONS[name];
805         // Merge `config` with predefined default values.
806         if (defConfig) {
807             Y.mix(config, defConfig, false, null, 0, true);
808         }
810         return config;
811     },
813     /**
814     `HTML_PARSER` implementation for the `buttons` attribute.
816     **Note:** To determine a button node's name its `data-name` and `name`
817     attributes are examined. Whether the button should be the default is
818     determined by its `data-default` attribute.
820     @method _parseButtons
821     @param {Node} srcNode This widget's srcNode to search for buttons.
822     @return {null|Object} `buttons` Config object parsed from this widget's DOM.
823     @protected
824     @since 3.5.0
825     **/
826     _parseButtons: function (srcNode) {
827         var buttonSelector = '.' + WidgetButtons.CLASS_NAMES.button,
828             sections       = ['header', 'body', 'footer'],
829             buttonsConfig  = null;
831         YArray.each(sections, function (section) {
832             var container = this._getButtonContainer(section),
833                 buttons   = container && container.all(buttonSelector),
834                 sectionButtons;
836             if (!buttons || buttons.isEmpty()) { return; }
838             sectionButtons = [];
840             // Creates a button config object for every button node found and
841             // adds it to the section. This way each button configuration can be
842             // merged with any defaults provided by predefined `BUTTONS`.
843             buttons.each(function (button) {
844                 sectionButtons.push({srcNode: button});
845             });
847             buttonsConfig || (buttonsConfig = {});
848             buttonsConfig[section] = sectionButtons;
849         }, this);
851         return buttonsConfig;
852     },
854     /**
855     Setter for the `buttons` attribute. This processes the specified `config`
856     and returns a new `buttons` object which is stored as the new state; leaving
857     the original, specified `config` unmodified.
859     The button nodes will either be created via `Y.Plugin.Button.createNode()`,
860     or when a button is already a Node already, it will by `plug()`ed with
861     `Y.Plugin.Button`.
863     @method _setButtons
864     @param {Array|Object} config The `buttons` configuration to process.
865     @return {Object} The processed `buttons` object which represents the new
866         state.
867     @protected
868     @since 3.5.0
869     **/
870     _setButtons: function (config) {
871         var defSection = this.DEFAULT_BUTTONS_SECTION,
872             buttons    = {};
874         function processButtons(buttonConfigs, currentSection) {
875             if (!isArray(buttonConfigs)) { return; }
877             var i, len, button, section;
879             for (i = 0, len = buttonConfigs.length; i < len; i += 1) {
880                 button  = buttonConfigs[i];
881                 section = currentSection;
883                 if (!isNode(button)) {
884                     button = this._mergeButtonConfig(button);
885                     section || (section = button.section);
886                 }
888                 // Always passes through `_createButton()` to make sure the node
889                 // is decorated as a button.
890                 button = this._createButton(button);
892                 // Use provided `section` or fallback to the default section.
893                 section || (section = defSection);
895                 // Add button to the array of buttons for the specified section.
896                 (buttons[section] || (buttons[section] = [])).push(button);
897             }
898         }
900         // Handle `config` being either an Array or Object of Arrays.
901         if (isArray(config)) {
902             processButtons.call(this, config);
903         } else {
904             YObject.each(config, processButtons, this);
905         }
907         return buttons;
908     },
910     /**
911     Syncs this widget's current button-related state to its DOM. This method is
912     inserted via AOP, and will execute after `syncUI()`.
914     @method _syncUIButtons
915     @protected
916     @since 3.4.0
917     **/
918     _syncUIButtons: function () {
919         this._uiSetButtons(this.get('buttons'));
920         this._uiSetDefaultButton(this.get('defaultButton'));
921         this._uiSetVisibleButtons(this.get('visible'));
922     },
924     /**
925     Inserts the specified `button` node into this widget's DOM at the specified
926     `section` and `index` and updates the section content.
928     The section and button container nodes will be created if they do not
929     already exist.
931     @method _uiInsertButton
932     @param {Node} button The button node to insert into this widget's DOM.
933     @param {String} section The `WidgetStdMod` section (header/body/footer).
934     @param {Number} index Index at which the `button` should be positioned.
935     @protected
936     @since 3.5.0
937     **/
938     _uiInsertButton: function (button, section, index) {
939         var buttonsClassName = WidgetButtons.CLASS_NAMES.button,
940             buttonContainer  = this._getButtonContainer(section, true),
941             sectionButtons   = buttonContainer.all('.' + buttonsClassName);
943         // Inserts the button node at the correct index.
944         buttonContainer.insertBefore(button, sectionButtons.item(index));
946         // Adds the button container to the section content.
947         this.setStdModContent(section, buttonContainer, 'after');
948     },
950     /**
951     Removes the button node from this widget's DOM and detaches any event
952     subscriptions on the button that were created by this widget. The section
953     content will be updated unless `{preserveContent: true}` is passed in the
954     `options`.
956     By default the button container node will be removed when this removes the
957     last button of the specified `section`; and if no other content remains in
958     the section node, it will also be removed.
960     @method _uiRemoveButton
961     @param {Node} button The button to remove and destroy.
962     @param {String} section The `WidgetStdMod` section (header/body/footer).
963     @param {Object} [options] Additional options.
964       @param {Boolean} [options.preserveContent=false] Whether the section
965         content should be updated.
966     @protected
967     @since 3.5.0
968     **/
969     _uiRemoveButton: function (button, section, options) {
970         var yuid    = Y.stamp(button, this),
971             handles = this._buttonsHandles,
972             handle  = handles[yuid],
973             buttonContainer, buttonClassName;
975         handle && handle.detach();
976         delete handles[yuid];
978         button.remove();
980         options || (options = {});
982         // Remove the button container and section nodes if needed.
983         if (!options.preserveContent) {
984             buttonContainer = this._getButtonContainer(section);
985             buttonClassName = WidgetButtons.CLASS_NAMES.button;
987             // Only matters if we have a button container which is empty.
988             if (buttonContainer &&
989                     buttonContainer.all('.' + buttonClassName).isEmpty()) {
991                 buttonContainer.remove();
992                 this._updateContentButtons(section);
993             }
994         }
995     },
997     /**
998     Sets the current `buttons` state to this widget's DOM by rendering the
999     specified collection of `buttons` and updates the contents of each section
1000     as needed.
1002     Button nodes which already exist in the DOM will remain intact, or will be
1003     moved if they should be in a new position. Old button nodes which are no
1004     longer represented in the specified `buttons` collection will be removed,
1005     and any event subscriptions on the button which were created by this widget
1006     will be detached.
1008     If the button nodes in this widget's DOM actually change, then each content
1009     section will be updated (or removed) appropriately.
1011     @method _uiSetButtons
1012     @param {Object} buttons The current `buttons` state to visually represent.
1013     @protected
1014     @since 3.5.0
1015     **/
1016     _uiSetButtons: function (buttons) {
1017         var buttonClassName = WidgetButtons.CLASS_NAMES.button,
1018             sections        = ['header', 'body', 'footer'];
1020         YArray.each(sections, function (section) {
1021             var sectionButtons  = buttons[section] || [],
1022                 numButtons      = sectionButtons.length,
1023                 buttonContainer = this._getButtonContainer(section, numButtons),
1024                 buttonsUpdated  = false,
1025                 oldNodes, i, button, buttonIndex;
1027             // When there's no button container, there are no new buttons or old
1028             // buttons that we have to deal with for this section.
1029             if (!buttonContainer) { return; }
1031             oldNodes = buttonContainer.all('.' + buttonClassName);
1033             for (i = 0; i < numButtons; i += 1) {
1034                 button      = sectionButtons[i];
1035                 buttonIndex = oldNodes ? oldNodes.indexOf(button) : -1;
1037                 // Buttons already rendered in the Widget should remain there or
1038                 // moved to their new index. New buttons will be added to the
1039                 // current `buttonContainer`.
1040                 if (buttonIndex > -1) {
1041                     // Remove button from existing buttons nodeList since its in
1042                     // the DOM already.
1043                     oldNodes.splice(buttonIndex, 1);
1045                     // Check that the button is at the right position, if not,
1046                     // move it to its new position.
1047                     if (buttonIndex !== i) {
1048                         // Using `i + 1` because the button should be at index
1049                         // `i`; it's inserted before the node which comes after.
1050                         buttonContainer.insertBefore(button, i + 1);
1051                         buttonsUpdated = true;
1052                     }
1053                 } else {
1054                     buttonContainer.appendChild(button);
1055                     buttonsUpdated = true;
1056                 }
1057             }
1059             // Safely removes the old button nodes which are no longer part of
1060             // this widget's `buttons`.
1061             oldNodes.each(function (button) {
1062                 this._uiRemoveButton(button, section, {preserveContent: true});
1063                 buttonsUpdated = true;
1064             }, this);
1066             // Remove leftover empty button containers and updated the StdMod
1067             // content area.
1068             if (numButtons === 0) {
1069                 buttonContainer.remove();
1070                 this._updateContentButtons(section);
1071                 return;
1072             }
1074             // Adds the button container to the section content.
1075             if (buttonsUpdated) {
1076                 this.setStdModContent(section, buttonContainer, 'after');
1077             }
1078         }, this);
1079     },
1081     /**
1082     Adds the "yui3-button-primary" CSS class to the new `defaultButton` and
1083     removes it from the old default button.
1085     @method _uiSetDefaultButton
1086     @param {Node} newButton The new `defaultButton`.
1087     @param {Node} oldButton The old `defaultButton`.
1088     @protected
1089     @since 3.5.0
1090     **/
1091     _uiSetDefaultButton: function (newButton, oldButton) {
1092         var primaryClassName = WidgetButtons.CLASS_NAMES.primary;
1094         newButton && newButton.addClass(primaryClassName);
1095         oldButton && oldButton.removeClass(primaryClassName);
1096     },
1098     /**
1099     Focuses this widget's `defaultButton` if there is one and this widget is
1100     visible.
1102     @method _uiSetVisibleButtons
1103     @param {Boolean} visible Whether this widget is visible.
1104     @protected
1105     @since 3.5.0
1106     **/
1107     _uiSetVisibleButtons: function (visible) {
1108         if (!visible) { return; }
1110         var defaultButton = this.get('defaultButton');
1111         if (defaultButton) {
1112             defaultButton.focus();
1113         }
1114     },
1116     /**
1117     Removes the specified `button` from the buttons map (both name -> button and
1118     section:name -> button), and nulls-out the `defaultButton` if it is
1119     currently the default button.
1121     @method _unMapButton
1122     @param {Node} button The button node to remove from the buttons map.
1123     @param {String} section The `WidgetStdMod` section (header/body/footer).
1124     @protected
1125     @since 3.5.0
1126     **/
1127     _unMapButton: function (button, section) {
1128         var map  = this._buttonsMap,
1129             name = this._getButtonName(button),
1130             sectionName;
1132         // Only delete the map entry if the specified `button` is mapped to it.
1133         if (name) {
1134             // name -> button
1135             if (map[name] === button) {
1136                 delete map[name];
1137             }
1139             // section:name -> button
1140             sectionName = section + ':' + name;
1141             if (map[sectionName] === button) {
1142                 delete map[sectionName];
1143             }
1144         }
1146         // Clear the default button if its the specified `button`.
1147         if (this._defaultButton === button) {
1148             this._defaultButton = null;
1149         }
1150     },
1152     /**
1153     Updates the `defaultButton` attribute if it needs to be updated by comparing
1154     its current value with the protected `_defaultButton` property.
1156     @method _updateDefaultButton
1157     @protected
1158     @since 3.5.0
1159     **/
1160     _updateDefaultButton: function () {
1161         var defaultButton = this._defaultButton;
1163         if (this.get('defaultButton') !== defaultButton) {
1164             this._set('defaultButton', defaultButton);
1165         }
1166     },
1168     /**
1169     Updates the content attribute which corresponds to the specified `section`.
1171     The method updates the section's content to its current `childNodes`
1172     (text and/or HTMLElement), or will null-out its contents if the section is
1173     empty. It also specifies a `src` of `buttons` on the change event facade.
1175     @method _updateContentButtons
1176     @param {String} section The `WidgetStdMod` section (header/body/footer) to
1177         update.
1178     @protected
1179     @since 3.5.0
1180     **/
1181     _updateContentButtons: function (section) {
1182         // `childNodes` return text nodes and HTMLElements.
1183         var sectionContent = this.getStdModNode(section).get('childNodes');
1185         // Updates the section to its current contents, or null if it is empty.
1186         this.set(section + 'Content', sectionContent.isEmpty() ? null :
1187             sectionContent, {src: 'buttons'});
1188     },
1190     // -- Protected Event Handlers ---------------------------------------------
1192     /**
1193     Handles this widget's `buttonsChange` event which fires anytime the
1194     `buttons` attribute is modified.
1196     **Note:** This method special-cases the `buttons` modifications caused by
1197     `addButton()` and `removeButton()`, both of which set the `src` property on
1198     the event facade to "add" and "remove" respectively.
1200     @method _afterButtonsChange
1201     @param {EventFacade} e
1202     @protected
1203     @since 3.4.0
1204     **/
1205     _afterButtonsChange: function (e) {
1206         var buttons = e.newVal,
1207             section = e.section,
1208             index   = e.index,
1209             src     = e.src,
1210             button;
1212         // Special cases `addButton()` to only set and insert the new button.
1213         if (src === 'add') {
1214             // Make sure we have the button node.
1215             button = buttons[section][index];
1217             this._mapButton(button, section);
1218             this._updateDefaultButton();
1219             this._uiInsertButton(button, section, index);
1221             return;
1222         }
1224         // Special cases `removeButton()` to only remove the specified button.
1225         if (src === 'remove') {
1226             // Button node already exists on the event facade.
1227             button = e.button;
1229             this._unMapButton(button, section);
1230             this._updateDefaultButton();
1231             this._uiRemoveButton(button, section);
1233             return;
1234         }
1236         this._mapButtons(buttons);
1237         this._updateDefaultButton();
1238         this._uiSetButtons(buttons);
1239     },
1241     /**
1242     Handles this widget's `headerContentChange`, `bodyContentChange`,
1243     `footerContentChange` events by making sure the `buttons` remain rendered
1244     after changes to the content areas.
1246     These events are very chatty, so extra caution is taken to avoid doing extra
1247     work or getting into an infinite loop.
1249     @method _afterContentChangeButtons
1250     @param {EventFacade} e
1251     @protected
1252     @since 3.5.0
1253     **/
1254     _afterContentChangeButtons: function (e) {
1255         var src     = e.src,
1256             pos     = e.stdModPosition,
1257             replace = !pos || pos === WidgetStdMod.REPLACE;
1259         // Only do work when absolutely necessary.
1260         if (replace && src !== 'buttons' && src !== Widget.UI_SRC) {
1261             this._uiSetButtons(this.get('buttons'));
1262         }
1263     },
1265     /**
1266     Handles this widget's `defaultButtonChange` event by adding the
1267     "yui3-button-primary" CSS class to the new `defaultButton` and removing it
1268     from the old default button.
1270     @method _afterDefaultButtonChange
1271     @param {EventFacade} e
1272     @protected
1273     @since 3.5.0
1274     **/
1275     _afterDefaultButtonChange: function (e) {
1276         this._uiSetDefaultButton(e.newVal, e.prevVal);
1277     },
1279     /**
1280     Handles this widget's `visibleChange` event by focusing the `defaultButton`
1281     if there is one.
1283     @method _afterVisibleChangeButtons
1284     @param {EventFacade} e
1285     @protected
1286     @since 3.5.0
1287     **/
1288     _afterVisibleChangeButtons: function (e) {
1289         this._uiSetVisibleButtons(e.newVal);
1290     }
1293 Y.WidgetButtons = WidgetButtons;
1296 }, '3.5.1' ,{requires:['button-plugin', 'cssbutton', 'widget-stdmod']});