Merge branch 'MDL-32509' of git://github.com/danpoltawski/moodle
[moodle.git] / lib / yui / 3.5.0 / build / widget-buttons / widget-buttons-debug.js
blobc0e04d60697a3c592cf6a6b5bf0f81b66303106d
1 /*
2 YUI 3.5.0 (build 5089)
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 /**
32 Provides header/body/footer button support for Widgets that use the
33 `WidgetStdMod` extension.
35 This Widget extension makes it easy to declaratively configure a widget's
36 buttons. It adds a `buttons` attribute along with button- accessor and mutator
37 methods. All button nodes have the `Y.Plugin.Button` plugin applied.
39 This extension also includes `HTML_PARSER` support to seed a widget's `buttons`
40 from those which already exist in its DOM.
42 @class WidgetButtons
43 @extensionfor Widget
44 @since 3.4.0
45 **/
46 function WidgetButtons() {
47     // Require `Y.WidgetStdMod`.
48     if (!this._stdModNode) {
49         Y.error('WidgetStdMod must be added to a Widget before WidgetButtons.');
50     }
52     // Has to be setup before the `initializer()`.
53     this._buttonsHandles = {};
56 WidgetButtons.ATTRS = {
57     /**
58     Collection containing a widget's buttons.
60     The collection is an Object which contains an Array of `Y.Node`s for every
61     `WidgetStdMod` section (header, body, footer) which has one or more buttons.
62     All button nodes have the `Y.Plugin.Button` plugin applied.
64     This attribute is very flexible in the values it will accept. `buttons` can
65     be specified as a single Array, or an Object of Arrays keyed to a particular
66     section.
68     All specified values will be normalized to this type of structure:
70         {
71             header: [...],
72             footer: [...]
73         }
75     A button can be specified as a `Y.Node`, config Object, or String name for a
76     predefined button on the `BUTTONS` prototype property. When a config Object
77     is provided, it will be merged with any defaults provided by a button with
78     the same `name` defined on the `BUTTONS` property.
80     See `addButton()` for the detailed list of configuration properties.
82     For convenience, a widget's buttons will always persist and remain rendered
83     after header/body/footer content updates. Buttons should be removed by
84     updating this attribute or using the `removeButton()` method.
86     @example
87         {
88             // Uses predefined "close" button by string name.
89             header: ['close'],
91             footer: [
92                 {
93                     name  : 'cancel',
94                     label : 'Cancel',
95                     action: 'hide'
96                 },
98                 {
99                     name     : 'okay',
100                     label    : 'Okay',
101                     isDefault: true,
103                     events: {
104                         click: function (e) {
105                             this.hide();
106                         }
107                     }
108                 }
109             ]
110         }
112     @attribute buttons
113     @type Object
114     @default {}
115     @since 3.4.0
116     **/
117     buttons: {
118         getter: '_getButtons',
119         setter: '_setButtons',
120         value : {}
121     },
123     /**
124     The current default button as configured through this widget's `buttons`.
126     A button can be configured as the default button in the following ways:
128       * As a config Object with an `isDefault` property:
129         `{label: 'Okay', isDefault: true}`.
131       * As a Node with a `data-default` attribute:
132         `<button data-default="true">Okay</button>`.
134     This attribute is **read-only**; anytime there are changes to this widget's
135     `buttons`, the `defaultButton` will be updated if needed.
137     **Note:** If two or more buttons are configured to be the default button,
138     the last one wins.
140     @attribute defaultButton
141     @type Node
142     @default null
143     @readOnly
144     @since 3.5.0
145     **/
146     defaultButton: {
147         readOnly: true,
148         value   : null
149     }
153 CSS classes used by `WidgetButtons`.
155 @property CLASS_NAMES
156 @type Object
157 @static
158 @since 3.5.0
160 WidgetButtons.CLASS_NAMES = {
161     button : getClassName('button'),
162     buttons: Widget.getClassName('buttons'),
163     primary: getClassName('button', 'primary')
166 WidgetButtons.HTML_PARSER = {
167     buttons: function (srcNode) {
168         return this._parseButtons(srcNode);
169     }
173 The list of button configuration properties which are specific to
174 `WidgetButtons` and should not be passed to `Y.Plugin.Button.createNode()`.
176 @property NON_BUTTON_NODE_CFG
177 @type Array
178 @static
179 @since 3.5.0
181 WidgetButtons.NON_BUTTON_NODE_CFG = [
182     'action', 'classNames', 'context', 'events', 'isDefault', 'section'
185 WidgetButtons.prototype = {
186     // -- Public Properties ----------------------------------------------------
188     /**
189     Collection of predefined buttons mapped by name -> config.
191     These button configurations will serve as defaults for any button added to a
192     widget's buttons which have the same `name`.
194     See `addButton()` for a list of possible configuration values.
196     @property BUTTONS
197     @type Object
198     @default {}
199     @see addButton()
200     @since 3.5.0
201     **/
202     BUTTONS: {},
204     /**
205     The HTML template to use when creating the node which wraps all buttons of a
206     section. By default it will have the CSS class: "yui3-widget-buttons".
208     @property BUTTONS_TEMPLATE
209     @type String
210     @default "<span />"
211     @since 3.5.0
212     **/
213     BUTTONS_TEMPLATE: '<span />',
215     /**
216     The default section to render buttons in when no section is specified.
218     @property DEFAULT_BUTTONS_SECTION
219     @type String
220     @default Y.WidgetStdMod.FOOTER
221     @since 3.5.0
222     **/
223     DEFAULT_BUTTONS_SECTION: WidgetStdMod.FOOTER,
225     // -- Protected Properties -------------------------------------------------
227     /**
228     A map of button node `_yuid` -> event-handle for all button nodes which were
229     created by this widget.
231     @property _buttonsHandles
232     @type Object
233     @protected
234     @since 3.5.0
235     **/
237     /**
238     A map of this widget's `buttons`, both name -> button and
239     section:name -> button.
241     @property _buttonsMap
242     @type Object
243     @protected
244     @since 3.5.0
245     **/
247     /**
248     Internal reference to this widget's default button.
250     @property _defaultButton
251     @type Node
252     @protected
253     @since 3.5.0
254     **/
256     // -- Lifecycle Methods ----------------------------------------------------
258     initializer: function () {
259         // Creates button mappings and sets the `defaultButton`.
260         this._mapButtons(this.get('buttons'));
261         this._updateDefaultButton();
263         // Bound with `Y.bind()` to make more extensible.
264         this.after('buttonsChange', Y.bind('_afterButtonsChange', this));
266         Y.after(this._bindUIButtons, this, 'bindUI');
267         Y.after(this._syncUIButtons, this, 'syncUI');
268     },
270     destructor: function () {
271         // Detach all event subscriptions this widget added to its `buttons`.
272         YObject.each(this._buttonsHandles, function (handle) {
273             handle.detach();
274         });
276         delete this._buttonsHandles;
277         delete this._buttonsMap;
278         delete this._defaultButton;
279     },
281     // -- Public Methods -------------------------------------------------------
283     /**
284     Adds a button to this widget.
286     The new button node will have the `Y.Plugin.Button` plugin applied, be added
287     to this widget's `buttons`, and rendered in the specified `section` at the
288     specified `index` (or end of the section). If the section does not exist, it
289     will be created.
291     This fires the `buttonsChange` event and adds the following properties to
292     the event facade:
294       * `button`: The button node or config object to add.
296       * `section`: The `WidgetStdMod` section (header/body/footer) where the
297         button should be added.
299       * `index`: The index at which to add the button to the section.
301       * `src`: "add"
303     @method addButton
304     @param {Node|Object|String} button The button to add. This can be a `Y.Node`
305         instance, config Object, or String name for a predefined button on the
306         `BUTTONS` prototype property. When a config Object is provided, it will
307         be merged with any defaults provided by any `srcNode` and/or a button
308         with the same `name` defined on the `BUTTONS` property. The following
309         are the possible configuration properties beyond what Node plugins
310         accept by default:
311       @param {Function|String} [button.action] The default handler that should
312         be called when the button is clicked. A String name of a Function that
313         exists on the `context` object can also be provided. **Note:**
314         Specifying a set of `events` will override this setting.
315       @param {String|String[]} [button.classNames] Additional CSS classes to add
316         to the button node.
317       @param {Object} [button.context=this] Context which any `events` or
318         `action` should be called with. Defaults to `this`, the widget.
319         **Note:** `e.target` will access the button node in the event handlers.
320       @param {Boolean} [button.disabled=false] Whether the button should be
321         disabled.
322       @param {String|Object} [button.events="click"] Event name, or set of
323         events and handlers to bind to the button node. **See:** `Y.Node.on()`,
324         this value is passed as the first argument to `on()`.
325       @param {Boolean} [button.isDefault=false] Whether the button is the
326         default button.
327       @param {String} [button.label] The visible text/value displayed in the
328         button.
329       @param {String} [button.name] A name which can later be used to reference
330         this button. If a button is defined on the `BUTTONS` property with this
331         same name, its configuration properties will be merged in as defaults.
332       @param {String} [button.section] The `WidgetStdMod` section (header, body,
333         footer) where the button should be added.
334       @param {Node} [button.srcNode] An existing Node to use for the button,
335         default values will be seeded from this node, but are overriden by any
336         values specified in the config object. By default a new &lt;button&gt;
337         node will be created.
338       @param {String} [button.template] A specific template to use when creating
339         a new button node (e.g. "&lt;a /&gt;"). **Note:** Specifying a `srcNode`
340         will overide this.
341     @param {String} [section="footer"] The `WidgetStdMod` section
342         (header/body/footer) where the button should be added. This takes
343         precedence over the `button.section` configuration property.
344     @param {Number} [index] The index at which the button should be inserted. If
345         not specified, the button will be added to the end of the section.
346     @chainable
347     @see Plugin.Button.createNode()
348     @since 3.4.0
349     **/
350     addButton: function (button, section, index) {
351         var buttons = this.get('buttons'),
352             sectionButtons;
354         // Makes sure we have the full config object.
355         if (!Y.instanceOf(button, Y.Node)) {
356             button = this._mergeButtonConfig(button);
357             section || (section = button.section);
358         }
360         section || (section = this.DEFAULT_BUTTONS_SECTION);
361         sectionButtons = buttons[section] || (buttons[section] = []);
362         isNumber(index) || (index = sectionButtons.length);
364         // Insert new button at the correct position.
365         sectionButtons.splice(index, 0, button);
367         this.set('buttons', buttons, {
368             button : button,
369             section: section,
370             index  : index,
371             src    : 'add'
372         });
374         return this;
375     },
377     /**
378     Returns a button node from this widget's `buttons`.
380     @method getButton
381     @param {Number|String} name The string name or index of the button.
382     @param {String} [section="footer"] The `WidgetStdMod` section
383         (header/body/footer) where the button exists. Only applicable when
384         looking for a button by numerical index, or by name but scoped to a
385         particular section.
386     @return {Node} The button node.
387     @since 3.5.0
388     **/
389     getButton: function (name, section) {
390         if (!isValue(name)) { return; }
392         var map = this._buttonsMap,
393             buttons;
395         section || (section = this.DEFAULT_BUTTONS_SECTION);
397         // Supports `getButton(1, 'header')` signature.
398         if (isNumber(name)) {
399             buttons = this.get('buttons');
400             return buttons[section] && buttons[section][name];
401         }
403         // Looks up button by name or section:name.
404         return arguments.length > 1 ? map[section + ':' + name] : map[name];
405     },
407     /**
408     Removes a button from this widget.
410     The button will be removed from this widget's `buttons` and its DOM. Any
411     event subscriptions on the button which were created by this widget will be
412     detached. If the content section becomes empty after removing the button
413     node, then the section will also be removed.
415     This fires the `buttonsChange` event and adds the following properties to
416     the event facade:
418       * `button`: The button node to remove.
420       * `section`: The `WidgetStdMod` section (header/body/footer) where the
421         button should be removed from.
423       * `index`: The index at which at which the button exists in the section.
425       * `src`: "remove"
427     @method removeButton
428     @param {Node|Number|String} button The button to remove. This can be a
429         `Y.Node` instance, index, or String name of a button.
430     @param {String} [section="footer"] The `WidgetStdMod` section
431         (header/body/footer) where the button exists. Only applicable when
432         removing a button by numerical index, or by name but scoped to a
433         particular section.
434     @chainable
435     @since 3.5.0
436     **/
437     removeButton: function (button, section) {
438         if (!isValue(button)) { return this; }
440         var buttons = this.get('buttons'),
441             index;
443         // Shortcut if `button` is already an index which is needed for slicing.
444         if (isNumber(button)) {
445             section || (section = this.DEFAULT_BUTTONS_SECTION);
446             index  = button;
447             button = buttons[section][index];
448         } else {
449             // Supports `button` being the string name.
450             if (isString(button)) {
451                 button = this.getButton.apply(this, arguments);
452             }
454             // Determines the `section` and `index` at which the button exists.
455             YObject.some(buttons, function (sectionButtons, currentSection) {
456                 index = YArray.indexOf(sectionButtons, button);
458                 if (index > -1) {
459                     section = currentSection;
460                     return true;
461                 }
462             });
463         }
465         // Button was found at an appropriate index.
466         if (button && index > -1) {
467             // Remove button from `section` array.
468             buttons[section].splice(index, 1);
470             this.set('buttons', buttons, {
471                 button : button,
472                 section: section,
473                 index  : index,
474                 src    : 'remove'
475             });
476         }
478         return this;
479     },
481     // -- Protected Methods ----------------------------------------------------
483     /**
484     Binds UI event listeners. This method is inserted via AOP, and will execute
485     after `bindUI()`.
487     @method _bindUIButtons
488     @protected
489     @since 3.4.0
490     **/
491     _bindUIButtons: function () {
492         // Event handlers are bound with `bind()` to make them more extensible.
494         var afterContentChange = Y.bind('_afterContentChangeButtons', this);
496         this.after({
497             defaultButtonChange: Y.bind('_afterDefaultButtonChange', this),
498             visibleChange      : Y.bind('_afterVisibleChangeButtons', this),
499             headerContentChange: afterContentChange,
500             bodyContentChange  : afterContentChange,
501             footerContentChange: afterContentChange
502         });
503     },
505     /**
506     Returns a button node based on the specified `button` node or configuration.
508     The button node will either be created via `Y.Plugin.Button.createNode()`,
509     or when `button` is specified as a node already, it will by `plug()`ed with
510     `Y.Plugin.Button`.
512     @method _createButton
513     @param {Node|Object} button Button node or configuration object.
514     @return {Node} The button node.
515     @protected
516     @since 3.5.0
517     **/
518     _createButton: function (button) {
519         var config, buttonConfig, nonButtonNodeCfg,
520             i, len, action, context, handle;
522         // Plug and return an existing Y.Node instance.
523         if (Y.instanceOf(button, Y.Node)) {
524             return button.plug(ButtonPlugin);
525         }
527         // Merge `button` config with defaults and back-compat.
528         config = Y.merge({
529             context: this,
530             events : 'click',
531             label  : button.value
532         }, button);
534         buttonConfig     = Y.merge(config);
535         nonButtonNodeCfg = WidgetButtons.NON_BUTTON_NODE_CFG;
537         // Remove all non-button Node config props.
538         for (i = 0, len = nonButtonNodeCfg.length; i < len; i += 1) {
539             delete buttonConfig[nonButtonNodeCfg[i]];
540         }
542         // Create the button node using the button Node-only config.
543         button = ButtonPlugin.createNode(buttonConfig);
545         context = config.context;
546         action  = config.action;
548         // Supports `action` as a String name of a Function on the `context`
549         // object.
550         if (isString(action)) {
551             action = Y.bind(action, context);
552         }
554         // Supports all types of crazy configs for event subscriptions and
555         // stores a reference to the returned `EventHandle`.
556         handle = button.on(config.events, action, context);
557         this._buttonsHandles[Y.stamp(button, true)] = handle;
559         // Tags the button with the configured `name` and `isDefault` settings.
560         button.setData('name', this._getButtonName(config));
561         button.setData('default', this._getButtonDefault(config));
563         // Add any CSS classnames to the button node.
564         YArray.each(YArray(config.classNames), button.addClass, button);
566         return button;
567     },
569     /**
570     Returns the buttons container for the specified `section`, passing a truthy
571     value for `create` will create the node if it does not already exist.
573     **Note:** It is up to the caller to properly insert the returned container
574     node into the content section.
576     @method _getButtonContainer
577     @param {String} section The `WidgetStdMod` section (header/body/footer).
578     @param {Boolean} create Whether the buttons container should be created if
579         it does not already exist.
580     @return {Node} The buttons container node for the specified `section`.
581     @protected
582     @see BUTTONS_TEMPLATE
583     @since 3.5.0
584     **/
585     _getButtonContainer: function (section, create) {
586         var sectionClassName = WidgetStdMod.SECTION_CLASS_NAMES[section],
587             buttonsClassName = WidgetButtons.CLASS_NAMES.buttons,
588             contentBox       = this.get('contentBox'),
589             containerSelector, container;
591         // Search for an existing buttons container within the section.
592         containerSelector = '.' + sectionClassName + ' .' + buttonsClassName;
593         container         = contentBox.one(containerSelector);
595         // Create the `container` if it doesn't already exist.
596         if (!container && create) {
597             container = Y.Node.create(this.BUTTONS_TEMPLATE);
598             container.addClass(buttonsClassName);
599         }
601         return container;
602     },
604     /**
605     Returns whether or not the specified `button` is configured to be the
606     default button.
608     When a button node is specified, the button's `getData()` method will be
609     used to determine if the button is configured to be the default. When a
610     button config object is specified, the `isDefault` prop will determine
611     whether the button is the default.
613     **Note:** `<button data-default="true"></button>` is supported via the
614     `button.getData('default')` API call.
616     @method _getButtonDefault
617     @param {Node|Object} button The button node or configuration object.
618     @return {Boolean} Whether the button is configured to be the default button.
619     @protected
620     @since 3.5.0
621     **/
622     _getButtonDefault: function (button) {
623         var isDefault = Y.instanceOf(button, Y.Node) ?
624                 button.getData('default') : button.isDefault;
626         if (isString(isDefault)) {
627             return isDefault.toLowerCase() === 'true';
628         }
630         return !!isDefault;
631     },
633     /**
634     Returns the name of the specified `button`.
636     When a button node is specified, the button's `getData('name')` method is
637     preferred, but will fallback to `get('name')`, and the result will determine
638     the button's name. When a button config object is specified, the `name` prop
639     will determine the button's name.
641     **Note:** `<button data-name="foo"></button>` is supported via the
642     `button.getData('name')` API call.
644     @method _getButtonName
645     @param {Node|Object} button The button node or configuration object.
646     @return {String} The name of the button.
647     @protected
648     @since 3.5.0
649     **/
650     _getButtonName: function (button) {
651         var name;
653         if (Y.instanceOf(button, Y.Node)) {
654             name = button.getData('name') || button.get('name');
655         } else {
656             name = button && (button.name || button.type);
657         }
659         return name;
660     },
662     /**
663     Getter for the `buttons` attribute. A copy of the `buttons` object is
664     returned so the stored state cannot be modified by the callers of
665     `get('buttons')`.
667     This will recreate a copy of the `buttons` object, and each section array
668     (the button nodes are *not* copied/cloned.)
670     @method _getButtons
671     @param {Object} buttons The widget's current `buttons` state.
672     @return {Object} A copy of the widget's current `buttons` state.
673     @protected
674     @since 3.5.0
675     **/
676     _getButtons: function (buttons) {
677         var buttonsCopy = {};
679         // Creates a new copy of the `buttons` object.
680         YObject.each(buttons, function (sectionButtons, section) {
681             // Creates of copy of the array of button nodes.
682             buttonsCopy[section] = sectionButtons.concat();
683         });
685         return buttonsCopy;
686     },
688     /**
689     Adds the specified `button` to the buttons map (both name -> button and
690     section:name -> button), and sets the button as the default if it is
691     configured as the default button.
693     **Note:** If two or more buttons are configured with the same `name` and/or
694     configured to be the default button, the last one wins.
696     @method _mapButton
697     @param {Node} button The button node to map.
698     @param {String} section The `WidgetStdMod` section.
699     @protected
700     @since 3.5.0
701     **/
702     _mapButton: function (button, section) {
703         var map       = this._buttonsMap,
704             name      = this._getButtonName(button),
705             isDefault = this._getButtonDefault(button);
707         if (name) {
708             // name -> button
709             map[name] = button;
711             // section:name -> button
712             map[section + ':' + name] = button;
713         }
715         isDefault && (this._defaultButton = button);
716     },
718     /**
719     Adds the specified `buttons` to the buttons map (both name -> button and
720     section:name -> button), and set the a button as the default if one is
721     configured as the default button.
723     **Note:** This will clear all previous button mappings and null-out any
724     previous default button! If two or more buttons are configured with the same
725     `name` and/or configured to be the default button, the last one wins.
727     @method _mapButtons
728     @param {Node[]} buttons The button nodes to map.
729     @protected
730     @since 3.5.0
731     **/
732     _mapButtons: function (buttons) {
733         this._buttonsMap    = {};
734         this._defaultButton = null;
736         YObject.each(buttons, function (sectionButtons, section) {
737             var i, len;
739             for (i = 0, len = sectionButtons.length; i < len; i += 1) {
740                 this._mapButton(sectionButtons[i], section);
741             }
742         }, this);
743     },
745     /**
746     Returns a copy of the specified `config` object merged with any defaults
747     provided by a `srcNode` and/or a predefined configuration for a button
748     with the same `name` on the `BUTTONS` property.
750     @method _mergeButtonConfig
751     @param {Object|String} config Button configuration object, or string name.
752     @return {Object} A copy of the button configuration object merged with any
753         defaults.
754     @protected
755     @since 3.5.0
756     **/
757     _mergeButtonConfig: function (config) {
758         var buttonConfig, defConfig, name, button, tagName, label;
760         // Makes sure `config` is an Object and a copy of the specified value.
761         config = isString(config) ? {name: config} : Y.merge(config);
763         // Seeds default values from the button node, if there is one.
764         if (config.srcNode) {
765             button  = config.srcNode;
766             tagName = button.get('tagName').toLowerCase();
767             label   = button.get(tagName === 'input' ? 'value' : 'text');
769             // Makes sure the button's current values override any defaults.
770             buttonConfig = {
771                 disabled : !!button.get('disabled'),
772                 isDefault: this._getButtonDefault(button),
773                 name     : this._getButtonName(button)
774             };
776             // Label should only be considered when not an empty string.
777             label && (buttonConfig.label = label);
779             // Merge `config` with `buttonConfig` values.
780             Y.mix(config, buttonConfig, false, null, 0, true);
781         }
783         name      = this._getButtonName(config);
784         defConfig = this.BUTTONS && this.BUTTONS[name];
786         // Merge `config` with predefined default values.
787         if (defConfig) {
788             Y.mix(config, defConfig, false, null, 0, true);
789         }
791         return config;
792     },
794     /**
795     `HTML_PARSER` implementation for the `buttons` attribute.
797     **Note:** To determine a button node's name its `data-name` and `name`
798     attributes are examined. Whether the button should be the default is
799     determined by its `data-default` attribute.
801     @method _parseButtons
802     @param {Node} srcNode This widget's srcNode to search for buttons.
803     @return {null|Object} `buttons` Config object parsed from this widget's DOM.
804     @protected
805     @since 3.5.0
806     **/
807     _parseButtons: function (srcNode) {
808         var buttonSelector = '.' + WidgetButtons.CLASS_NAMES.button,
809             sections       = ['header', 'body', 'footer'],
810             buttonsConfig  = null;
812         YArray.each(sections, function (section) {
813             var container = this._getButtonContainer(section),
814                 buttons   = container && container.all(buttonSelector),
815                 sectionButtons;
817             if (!buttons || buttons.isEmpty()) { return; }
819             sectionButtons = [];
821             // Creates a button config object for every button node found and
822             // adds it to the section. This way each button configuration can be
823             // merged with any defaults provided by predefined `BUTTONS`.
824             buttons.each(function (button) {
825                 sectionButtons.push({srcNode: button});
826             });
828             buttonsConfig || (buttonsConfig = {});
829             buttonsConfig[section] = sectionButtons;
830         }, this);
832         return buttonsConfig;
833     },
835     /**
836     Setter for the `buttons` attribute. This processes the specified `config`
837     and returns a new `buttons` object which is stored as the new state; leaving
838     the original, specified `config` unmodified.
840     The button nodes will either be created via `Y.Plugin.Button.createNode()`,
841     or when a button is already a Node already, it will by `plug()`ed with
842     `Y.Plugin.Button`.
844     @method _setButtons
845     @param {Array|Object} config The `buttons` configuration to process.
846     @return {Object} The processed `buttons` object which represents the new
847         state.
848     @protected
849     @since 3.5.0
850     **/
851     _setButtons: function (config) {
852         var defSection = this.DEFAULT_BUTTONS_SECTION,
853             buttons    = {};
855         function processButtons(buttonConfigs, currentSection) {
856             if (!isArray(buttonConfigs)) { return; }
858             var i, len, button, section;
860             for (i = 0, len = buttonConfigs.length; i < len; i += 1) {
861                 button  = buttonConfigs[i];
862                 section = currentSection;
864                 if (!Y.instanceOf(button, Y.Node)) {
865                     button = this._mergeButtonConfig(button);
866                     section || (section = button.section);
867                 }
869                 // Always passes through `_createButton()` to make sure the node
870                 // is decorated as a button.
871                 button = this._createButton(button);
873                 // Use provided `section` or fallback to the default section.
874                 section || (section = defSection);
876                 // Add button to the array of buttons for the specified section.
877                 (buttons[section] || (buttons[section] = [])).push(button);
878             }
879         }
881         // Handle `config` being either an Array or Object of Arrays.
882         if (isArray(config)) {
883             processButtons.call(this, config);
884         } else {
885             YObject.each(config, processButtons, this);
886         }
888         return buttons;
889     },
891     /**
892     Syncs this widget's current button-related state to its DOM. This method is
893     inserted via AOP, and will execute after `syncUI()`.
895     @method _syncUIButtons
896     @protected
897     @since 3.4.0
898     **/
899     _syncUIButtons: function () {
900         this._uiSetButtons(this.get('buttons'));
901         this._uiSetDefaultButton(this.get('defaultButton'));
902         this._uiSetVisibleButtons(this.get('visible'));
903     },
905     /**
906     Inserts the specified `button` node into this widget's DOM at the specified
907     `section` and `index` and updates the section content.
909     The section and button container nodes will be created if they do not
910     already exist.
912     @method _uiInsertButton
913     @param {Node} button The button node to insert into this widget's DOM.
914     @param {String} section The `WidgetStdMod` section (header/body/footer).
915     @param {Number} index Index at which the `button` should be positioned.
916     @protected
917     @since 3.5.0
918     **/
919     _uiInsertButton: function (button, section, index) {
920         var buttonsClassName = WidgetButtons.CLASS_NAMES.button,
921             buttonContainer  = this._getButtonContainer(section, true),
922             sectionButtons   = buttonContainer.all('.' + buttonsClassName);
924         // Inserts the button node at the correct index.
925         buttonContainer.insertBefore(button, sectionButtons.item(index));
927         // Adds the button container to the section content.
928         this.setStdModContent(section, buttonContainer, 'after');
929     },
931     /**
932     Removes the button node from this widget's DOM and detaches any event
933     subscriptions on the button that were created by this widget. The section
934     content will be updated unless `{preserveContent: true}` is passed in the
935     `options`.
937     By default the button container node will be removed when this removes the
938     last button of the specified `section`; and if no other content remains in
939     the section node, it will also be removed.
941     @method _uiRemoveButton
942     @param {Node} button The button to remove and destroy.
943     @param {String} section The `WidgetStdMod` section (header/body/footer).
944     @param {Object} [options] Additional options.
945       @param {Boolean} [options.preserveContent=false] Whether the section
946         content should be updated.
947     @protected
948     @since 3.5.0
949     **/
950     _uiRemoveButton: function (button, section, options) {
951         var yuid    = Y.stamp(button, this),
952             handles = this._buttonsHandles,
953             handle  = handles[yuid],
954             buttonContainer, buttonClassName;
956         handle && handle.detach();
957         delete handles[yuid];
959         button.remove();
961         options || (options = {});
963         // Remove the button container and section nodes if needed.
964         if (!options.preserveContent) {
965             buttonContainer = this._getButtonContainer(section);
966             buttonClassName = WidgetButtons.CLASS_NAMES.button;
968             // Only matters if we have a button container which is empty.
969             if (buttonContainer &&
970                     buttonContainer.all('.' + buttonClassName).isEmpty()) {
972                 buttonContainer.remove();
973                 this._updateContentButtons(section);
974             }
975         }
976     },
978     /**
979     Sets the current `buttons` state to this widget's DOM by rendering the
980     specified collection of `buttons` and updates the contents of each section
981     as needed.
983     Button nodes which already exist in the DOM will remain intact, or will be
984     moved if they should be in a new position. Old button nodes which are no
985     longer represented in the specified `buttons` collection will be removed,
986     and any event subscriptions on the button which were created by this widget
987     will be detached.
989     If the button nodes in this widget's DOM actually change, then each content
990     section will be updated (or removed) appropriately.
992     @method _uiSetButtons
993     @param {Object} buttons The current `buttons` state to visually represent.
994     @protected
995     @since 3.5.0
996     **/
997     _uiSetButtons: function (buttons) {
998         var buttonClassName = WidgetButtons.CLASS_NAMES.button,
999             sections        = ['header', 'body', 'footer'];
1001         YArray.each(sections, function (section) {
1002             var sectionButtons  = buttons[section] || [],
1003                 numButtons      = sectionButtons.length,
1004                 buttonContainer = this._getButtonContainer(section, numButtons),
1005                 buttonsUpdated  = false,
1006                 oldNodes, i, button, buttonIndex;
1008             // When there's no button container, there are no new buttons or old
1009             // buttons that we have to deal with for this section.
1010             if (!buttonContainer) { return; }
1012             oldNodes = buttonContainer.all('.' + buttonClassName);
1014             for (i = 0; i < numButtons; i += 1) {
1015                 button      = sectionButtons[i];
1016                 buttonIndex = oldNodes ? oldNodes.indexOf(button) : -1;
1018                 // Buttons already rendered in the Widget should remain there or
1019                 // moved to their new index. New buttons will be added to the
1020                 // current `buttonContainer`.
1021                 if (buttonIndex > -1) {
1022                     // Remove button from existing buttons nodeList since its in
1023                     // the DOM already.
1024                     oldNodes.splice(buttonIndex, 1);
1026                     // Check that the button is at the right position, if not,
1027                     // move it to its new position.
1028                     if (buttonIndex !== i) {
1029                         // Using `i + 1` because the button should be at index
1030                         // `i`; it's inserted before the node which comes after.
1031                         buttonContainer.insertBefore(button, i + 1);
1032                         buttonsUpdated = true;
1033                     }
1034                 } else {
1035                     buttonContainer.appendChild(button);
1036                     buttonsUpdated = true;
1037                 }
1038             }
1040             // Safely removes the old button nodes which are no longer part of
1041             // this widget's `buttons`.
1042             oldNodes.each(function (button) {
1043                 this._uiRemoveButton(button, section, {preserveContent: true});
1044                 buttonsUpdated = true;
1045             }, this);
1047             // Remove leftover empty button containers and updated the StdMod
1048             // content area.
1049             if (numButtons === 0) {
1050                 buttonContainer.remove();
1051                 this._updateContentButtons(section);
1052                 return;
1053             }
1055             // Adds the button container to the section content.
1056             if (buttonsUpdated) {
1057                 this.setStdModContent(section, buttonContainer, 'after');
1058             }
1059         }, this);
1060     },
1062     /**
1063     Adds the "yui3-button-primary" CSS class to the new `defaultButton` and
1064     removes it from the old default button.
1066     @method _uiSetDefaultButton
1067     @param {Node} newButton The new `defaultButton`.
1068     @param {Node} oldButton The old `defaultButton`.
1069     @protected
1070     @since 3.5.0
1071     **/
1072     _uiSetDefaultButton: function (newButton, oldButton) {
1073         var primaryClassName = WidgetButtons.CLASS_NAMES.primary;
1075         newButton && newButton.addClass(primaryClassName);
1076         oldButton && oldButton.removeClass(primaryClassName);
1077     },
1079     /**
1080     Focuses this widget's `defaultButton` if there is one and this widget is
1081     visible.
1083     @method _uiSetVisibleButtons
1084     @param {Boolean} visible Whether this widget is visible.
1085     @protected
1086     @since 3.5.0
1087     **/
1088     _uiSetVisibleButtons: function (visible) {
1089         if (!visible) { return; }
1091         var defaultButton = this.get('defaultButton');
1092         if (defaultButton) {
1093             defaultButton.focus();
1094         }
1095     },
1097     /**
1098     Removes the specified `button` to the buttons map, and nulls-out the
1099     `defaultButton` if it is currently the default button.
1101     @method _unMapButton
1102     @param {Node} button The button node to remove from the buttons map.
1103     @protected
1104     @since 3.5.0
1105     **/
1106     _unMapButton: function (button, section) {
1107         var map  = this._buttonsMap,
1108             name = this._getButtonName(button),
1109             sectionName;
1111         // Only delete the map entry if the specified `button` is mapped to it.
1112         if (name) {
1113             // name -> button
1114             if (map[name] === button) {
1115                 delete map[name];
1116             }
1118             // section:name -> button
1119             sectionName = section + ':' + name;
1120             if (map[sectionName] === button) {
1121                 delete map[sectionName];
1122             }
1123         }
1125         // Clear the default button if its the specified `button`.
1126         if (this._defaultButton === button) {
1127             this._defaultButton = null;
1128         }
1129     },
1131     /**
1132     Updates the `defaultButton` attribute if it needs to be updated by comparing
1133     its current value with the protected `_defaultButton` property.
1135     @method _updateDefaultButton
1136     @protected
1137     @since 3.5.0
1138     **/
1139     _updateDefaultButton: function () {
1140         var defaultButton = this._defaultButton;
1142         if (this.get('defaultButton') !== defaultButton) {
1143             this._set('defaultButton', defaultButton);
1144         }
1145     },
1147     /**
1148     Updates the content attribute which corresponds to the specified `section`.
1150     The method updates the section's content to its current `childNodes`
1151     (text and/or HTMLElement), or will null-out its contents if the section is
1152     empty. It also specifies a `src` of `buttons` on the change event facade.
1154     @method _updateContentButtons
1155     @param {String} section The `WidgetStdMod` section (header/body/footer) to
1156         update.
1157     @protected
1158     @since 3.5.0
1159     **/
1160     _updateContentButtons: function (section) {
1161         // `childNodes` return text nodes and HTMLElements.
1162         var sectionContent = this.getStdModNode(section).get('childNodes');
1164         // Updates the section to its current contents, or null if it is empty.
1165         this.set(section + 'Content', sectionContent.isEmpty() ? null :
1166             sectionContent, {src: 'buttons'});
1167     },
1169     // -- Protected Event Handlers ---------------------------------------------
1171     /**
1172     Handles this widget's `buttonsChange` event which fires anytime the
1173     `buttons` attribute is modified.
1175     **Note:** This method special-cases the `buttons` modifications caused by
1176     `addButton()` and `removeButton()`, both of which set the `src` property on
1177     the event facade to "add" and "remove" respectively.
1179     @method _afterButtonsChange
1180     @param {EventFacade} e
1181     @protected
1182     @since 3.4.0
1183     **/
1184     _afterButtonsChange: function (e) {
1185         var buttons = e.newVal,
1186             section = e.section,
1187             index   = e.index,
1188             src     = e.src,
1189             button;
1191         // Special cases `addButton()` to only set and insert the new button.
1192         if (src === 'add') {
1193             // Make sure we have the button node.
1194             button = buttons[section][index];
1196             this._mapButton(button, section);
1197             this._updateDefaultButton();
1198             this._uiInsertButton(button, section, index);
1200             return;
1201         }
1203         // Special cases `removeButton()` to only remove the specified button.
1204         if (src === 'remove') {
1205             // Button node already exists on the event facade.
1206             button = e.button;
1208             this._unMapButton(button, section);
1209             this._updateDefaultButton();
1210             this._uiRemoveButton(button, section);
1212             return;
1213         }
1215         this._mapButtons(buttons);
1216         this._updateDefaultButton();
1217         this._uiSetButtons(buttons);
1218     },
1220     /**
1221     Handles this widget's `headerContentChange`, `bodyContentChange`,
1222     `footerContentChange` events by making sure the `buttons` remain rendered
1223     after changes to the content areas.
1225     These events are very chatty, so extra caution is taken to avoid doing extra
1226     work or getting into an infinite loop.
1228     @method _afterContentChangeButtons
1229     @param {EventFacade} e
1230     @protected
1231     @since 3.5.0
1232     **/
1233     _afterContentChangeButtons: function (e) {
1234         var src     = e.src,
1235             pos     = e.stdModPosition,
1236             replace = !pos || pos === WidgetStdMod.REPLACE;
1238         // Only do work when absolutely necessary.
1239         if (replace && src !== 'buttons' && src !== Widget.UI_SRC) {
1240             this._uiSetButtons(this.get('buttons'));
1241         }
1242     },
1244     /**
1245     Handles this widget's `defaultButtonChange` event by adding the
1246     "yui3-button-primary" CSS class to the new `defaultButton` and removing it
1247     from the old default button.
1249     @method _afterDefaultButtonChange
1250     @param {EventFacade} e
1251     @protected
1252     @since 3.5.0
1253     **/
1254     _afterDefaultButtonChange: function (e) {
1255         this._uiSetDefaultButton(e.newVal, e.prevVal);
1256     },
1258     /**
1259     Handles this widget's `visibleChange` event by focusing the `defaultButton`
1260     if there is one.
1262     @method _afterVisibleChangeButtons
1263     @param {EventFacade} e
1264     @protected
1265     @since 3.5.0
1266     **/
1267     _afterVisibleChangeButtons: function (e) {
1268         this._uiSetVisibleButtons(e.newVal);
1269     }
1272 Y.WidgetButtons = WidgetButtons;
1275 }, '3.5.0' ,{requires:['button-plugin', 'cssbutton', 'widget-stdmod']});