NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / widget-buttons / widget-buttons.js
blob1ebb246f0bf39641a99eaa5d9c4d13f40c405455
1 /*
2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
8 YUI.add('widget-buttons', function (Y, NAME) {
10 /**
11 Provides header/body/footer button support for Widgets that use the
12 `WidgetStdMod` extension.
14 @module widget-buttons
15 @since 3.4.0
16 **/
18 var YArray  = Y.Array,
19     YLang   = Y.Lang,
20     YObject = Y.Object,
22     ButtonPlugin = Y.Plugin.Button,
23     Widget       = Y.Widget,
24     WidgetStdMod = Y.WidgetStdMod,
26     getClassName = Y.ClassNameManager.getClassName,
27     isArray      = YLang.isArray,
28     isNumber     = YLang.isNumber,
29     isString     = YLang.isString,
30     isValue      = YLang.isValue;
32 // Utility to determine if an object is a Y.Node instance, even if it was
33 // created in a different YUI sandbox.
34 function isNode(node) {
35     return !!node.getDOMNode;
38 /**
39 Provides header/body/footer button support for Widgets that use the
40 `WidgetStdMod` extension.
42 This Widget extension makes it easy to declaratively configure a widget's
43 buttons. It adds a `buttons` attribute along with button- accessor and mutator
44 methods. All button nodes have the `Y.Plugin.Button` plugin applied.
46 This extension also includes `HTML_PARSER` support to seed a widget's `buttons`
47 from those which already exist in its DOM.
49 @class WidgetButtons
50 @extensionfor Widget
51 @since 3.4.0
52 **/
53 function WidgetButtons() {
54     // Has to be setup before the `initializer()`.
55     this._buttonsHandles = {};
58 WidgetButtons.ATTRS = {
59     /**
60     Collection containing a widget's buttons.
62     The collection is an Object which contains an Array of `Y.Node`s for every
63     `WidgetStdMod` section (header, body, footer) which has one or more buttons.
64     All button nodes have the `Y.Plugin.Button` plugin applied.
66     This attribute is very flexible in the values it will accept. `buttons` can
67     be specified as a single Array, or an Object of Arrays keyed to a particular
68     section.
70     All specified values will be normalized to this type of structure:
72         {
73             header: [...],
74             footer: [...]
75         }
77     A button can be specified as a `Y.Node`, config Object, or String name for a
78     predefined button on the `BUTTONS` prototype property. When a config Object
79     is provided, it will be merged with any defaults provided by a button with
80     the same `name` defined on the `BUTTONS` property.
82     See `addButton()` for the detailed list of configuration properties.
84     For convenience, a widget's buttons will always persist and remain rendered
85     after header/body/footer content updates. Buttons should be removed by
86     updating this attribute or using the `removeButton()` method.
88     @example
89         {
90             // Uses predefined "close" button by string name.
91             header: ['close'],
93             footer: [
94                 {
95                     name  : 'cancel',
96                     label : 'Cancel',
97                     action: 'hide'
98                 },
100                 {
101                     name     : 'okay',
102                     label    : 'Okay',
103                     isDefault: true,
105                     events: {
106                         click: function (e) {
107                             this.hide();
108                         }
109                     }
110                 }
111             ]
112         }
114     @attribute buttons
115     @type Object
116     @default {}
117     @since 3.4.0
118     **/
119     buttons: {
120         getter: '_getButtons',
121         setter: '_setButtons',
122         value : {}
123     },
125     /**
126     The current default button as configured through this widget's `buttons`.
128     A button can be configured as the default button in the following ways:
130       * As a config Object with an `isDefault` property:
131         `{label: 'Okay', isDefault: true}`.
133       * As a Node with a `data-default` attribute:
134         `<button data-default="true">Okay</button>`.
136     This attribute is **read-only**; anytime there are changes to this widget's
137     `buttons`, the `defaultButton` will be updated if needed.
139     **Note:** If two or more buttons are configured to be the default button,
140     the last one wins.
142     @attribute defaultButton
143     @type Node
144     @default null
145     @readOnly
146     @since 3.5.0
147     **/
148     defaultButton: {
149         readOnly: true,
150         value   : null
151     }
155 CSS classes used by `WidgetButtons`.
157 @property CLASS_NAMES
158 @type Object
159 @static
160 @since 3.5.0
162 WidgetButtons.CLASS_NAMES = {
163     button : getClassName('button'),
164     buttons: Widget.getClassName('buttons'),
165     primary: getClassName('button', 'primary')
168 WidgetButtons.HTML_PARSER = {
169     buttons: function (srcNode) {
170         return this._parseButtons(srcNode);
171     }
175 The list of button configuration properties which are specific to
176 `WidgetButtons` and should not be passed to `Y.Plugin.Button.createNode()`.
178 @property NON_BUTTON_NODE_CFG
179 @type Array
180 @static
181 @since 3.5.0
183 WidgetButtons.NON_BUTTON_NODE_CFG = [
184     'action', 'classNames', 'context', 'events', 'isDefault', 'section'
187 WidgetButtons.prototype = {
188     // -- Public Properties ----------------------------------------------------
190     /**
191     Collection of predefined buttons mapped by name -> config.
193     These button configurations will serve as defaults for any button added to a
194     widget's buttons which have the same `name`.
196     See `addButton()` for a list of possible configuration values.
198     @property BUTTONS
199     @type Object
200     @default {}
201     @see addButton()
202     @since 3.5.0
203     **/
204     BUTTONS: {},
206     /**
207     The HTML template to use when creating the node which wraps all buttons of a
208     section. By default it will have the CSS class: "yui3-widget-buttons".
210     @property BUTTONS_TEMPLATE
211     @type String
212     @default "<span />"
213     @since 3.5.0
214     **/
215     BUTTONS_TEMPLATE: '<span />',
217     /**
218     The default section to render buttons in when no section is specified.
220     @property DEFAULT_BUTTONS_SECTION
221     @type String
222     @default Y.WidgetStdMod.FOOTER
223     @since 3.5.0
224     **/
225     DEFAULT_BUTTONS_SECTION: WidgetStdMod.FOOTER,
227     // -- Protected Properties -------------------------------------------------
229     /**
230     A map of button node `_yuid` -> event-handle for all button nodes which were
231     created by this widget.
233     @property _buttonsHandles
234     @type Object
235     @protected
236     @since 3.5.0
237     **/
239     /**
240     A map of this widget's `buttons`, both name -> button and
241     section:name -> button.
243     @property _buttonsMap
244     @type Object
245     @protected
246     @since 3.5.0
247     **/
249     /**
250     Internal reference to this widget's default button.
252     @property _defaultButton
253     @type Node
254     @protected
255     @since 3.5.0
256     **/
258     // -- Lifecycle Methods ----------------------------------------------------
260     initializer: function () {
261         // Require `Y.WidgetStdMod`.
262         if (!this._stdModNode) {
263             Y.error('WidgetStdMod must be added to a Widget before WidgetButtons.');
264         }
266         // Creates button mappings and sets the `defaultButton`.
267         this._mapButtons(this.get('buttons'));
268         this._updateDefaultButton();
270         // Bound with `Y.bind()` to make more extensible.
271         this.after({
272             buttonsChange      : Y.bind('_afterButtonsChange', this),
273             defaultButtonChange: Y.bind('_afterDefaultButtonChange', this)
274         });
276         Y.after(this._bindUIButtons, this, 'bindUI');
277         Y.after(this._syncUIButtons, this, 'syncUI');
278     },
280     destructor: function () {
281         // Detach all event subscriptions this widget added to its `buttons`.
282         YObject.each(this._buttonsHandles, function (handle) {
283             handle.detach();
284         });
286         delete this._buttonsHandles;
287         delete this._buttonsMap;
288         delete this._defaultButton;
289     },
291     // -- Public Methods -------------------------------------------------------
293     /**
294     Adds a button to this widget.
296     The new button node will have the `Y.Plugin.Button` plugin applied, be added
297     to this widget's `buttons`, and rendered in the specified `section` at the
298     specified `index` (or end of the section when no `index` is provided). If
299     the section does not exist, it will be created.
301     This fires the `buttonsChange` event and adds the following properties to
302     the event facade:
304       * `button`: The button node or config object to add.
306       * `section`: The `WidgetStdMod` section (header/body/footer) where the
307         button will be added.
309       * `index`: The index at which the button will be in the section.
311       * `src`: "add"
313     **Note:** The `index` argument will be passed to the Array `splice()`
314     method, therefore a negative value will insert the `button` that many items
315     from the end. The `index` property on the `buttonsChange` event facade is
316     the index at which the `button` was added.
318     @method addButton
319     @param {Node|Object|String} button The button to add. This can be a `Y.Node`
320         instance, config Object, or String name for a predefined button on the
321         `BUTTONS` prototype property. When a config Object is provided, it will
322         be merged with any defaults provided by any `srcNode` and/or a button
323         with the same `name` defined on the `BUTTONS` property. The following
324         are the possible configuration properties beyond what Node plugins
325         accept by default:
326       @param {Function|String} [button.action] The default handler that should
327         be called when the button is clicked. A String name of a Function that
328         exists on the `context` object can also be provided. **Note:**
329         Specifying a set of `events` will override this setting.
330       @param {String|String[]} [button.classNames] Additional CSS classes to add
331         to the button node.
332       @param {Object} [button.context=this] Context which any `events` or
333         `action` should be called with. Defaults to `this`, the widget.
334         **Note:** `e.target` will access the button node in the event handlers.
335       @param {Boolean} [button.disabled=false] Whether the button should be
336         disabled.
337       @param {String|Object} [button.events="click"] Event name, or set of
338         events and handlers to bind to the button node. **See:** `Y.Node.on()`,
339         this value is passed as the first argument to `on()`.
340       @param {Boolean} [button.isDefault=false] Whether the button is the
341         default button.
342       @param {String} [button.label] The visible text/value displayed in the
343         button.
344       @param {String} [button.name] A name which can later be used to reference
345         this button. If a button is defined on the `BUTTONS` property with this
346         same name, its configuration properties will be merged in as defaults.
347       @param {String} [button.section] The `WidgetStdMod` section (header, body,
348         footer) where the button should be added.
349       @param {Node} [button.srcNode] An existing Node to use for the button,
350         default values will be seeded from this node, but are overriden by any
351         values specified in the config object. By default a new &lt;button&gt;
352         node will be created.
353       @param {String} [button.template] A specific template to use when creating
354         a new button node (e.g. "&lt;a /&gt;"). **Note:** Specifying a `srcNode`
355         will overide this.
356     @param {String} [section="footer"] The `WidgetStdMod` section
357         (header/body/footer) where the button should be added. This takes
358         precedence over the `button.section` configuration property.
359     @param {Number} [index] The index at which the button should be inserted. If
360         not specified, the button will be added to the end of the section. This
361         value is passed to the Array `splice()` method, therefore a negative
362         value will insert the `button` that many items from the end.
363     @chainable
364     @see Plugin.Button.createNode()
365     @since 3.4.0
366     **/
367     addButton: function (button, section, index) {
368         var buttons = this.get('buttons'),
369             sectionButtons, atIndex;
371         // Makes sure we have the full config object.
372         if (!isNode(button)) {
373             button = this._mergeButtonConfig(button);
374             section || (section = button.section);
375         }
377         section || (section = this.DEFAULT_BUTTONS_SECTION);
378         sectionButtons = buttons[section] || (buttons[section] = []);
379         isNumber(index) || (index = sectionButtons.length);
381         // Insert new button at the correct position.
382         sectionButtons.splice(index, 0, button);
384         // Determine the index at which the `button` now exists in the array.
385         atIndex = YArray.indexOf(sectionButtons, button);
387         this.set('buttons', buttons, {
388             button : button,
389             section: section,
390             index  : atIndex,
391             src    : 'add'
392         });
394         return this;
395     },
397     /**
398     Returns a button node from this widget's `buttons`.
400     @method getButton
401     @param {Number|String} name The string name or index of the button.
402     @param {String} [section="footer"] The `WidgetStdMod` section
403         (header/body/footer) where the button exists. Only applicable when
404         looking for a button by numerical index, or by name but scoped to a
405         particular section.
406     @return {Node} The button node.
407     @since 3.5.0
408     **/
409     getButton: function (name, section) {
410         if (!isValue(name)) { return; }
412         var map = this._buttonsMap,
413             buttons;
415         section || (section = this.DEFAULT_BUTTONS_SECTION);
417         // Supports `getButton(1, 'header')` signature.
418         if (isNumber(name)) {
419             buttons = this.get('buttons');
420             return buttons[section] && buttons[section][name];
421         }
423         // Looks up button by name or section:name.
424         return arguments.length > 1 ? map[section + ':' + name] : map[name];
425     },
427     /**
428     Removes a button from this widget.
430     The button will be removed from this widget's `buttons` and its DOM. Any
431     event subscriptions on the button which were created by this widget will be
432     detached. If the content section becomes empty after removing the button
433     node, then the section will also be removed.
435     This fires the `buttonsChange` event and adds the following properties to
436     the event facade:
438       * `button`: The button node to remove.
440       * `section`: The `WidgetStdMod` section (header/body/footer) where the
441         button should be removed from.
443       * `index`: The index at which the button exists in the section.
445       * `src`: "remove"
447     @method removeButton
448     @param {Node|Number|String} button The button to remove. This can be a
449         `Y.Node` instance, index, or String name of a button.
450     @param {String} [section="footer"] The `WidgetStdMod` section
451         (header/body/footer) where the button exists. Only applicable when
452         removing a button by numerical index, or by name but scoped to a
453         particular section.
454     @chainable
455     @since 3.5.0
456     **/
457     removeButton: function (button, section) {
458         if (!isValue(button)) { return this; }
460         var buttons = this.get('buttons'),
461             index;
463         // Shortcut if `button` is already an index which is needed for slicing.
464         if (isNumber(button)) {
465             section || (section = this.DEFAULT_BUTTONS_SECTION);
466             index  = button;
467             button = buttons[section][index];
468         } else {
469             // Supports `button` being the string name.
470             if (isString(button)) {
471                 // `getButton()` is called this way because its behavior is
472                 // different based on the number of arguments.
473                 button = this.getButton.apply(this, arguments);
474             }
476             // Determines the `section` and `index` at which the button exists.
477             YObject.some(buttons, function (sectionButtons, currentSection) {
478                 index = YArray.indexOf(sectionButtons, button);
480                 if (index > -1) {
481                     section = currentSection;
482                     return true;
483                 }
484             });
485         }
487         // Button was found at an appropriate index.
488         if (button && index > -1) {
489             // Remove button from `section` array.
490             buttons[section].splice(index, 1);
492             this.set('buttons', buttons, {
493                 button : button,
494                 section: section,
495                 index  : index,
496                 src    : 'remove'
497             });
498         }
500         return this;
501     },
503     // -- Protected Methods ----------------------------------------------------
505     /**
506     Binds UI event listeners. This method is inserted via AOP, and will execute
507     after `bindUI()`.
509     @method _bindUIButtons
510     @protected
511     @since 3.4.0
512     **/
513     _bindUIButtons: function () {
514         // Event handlers are bound with `bind()` to make them more extensible.
515         var afterContentChange = Y.bind('_afterContentChangeButtons', this);
517         this.after({
518             visibleChange      : Y.bind('_afterVisibleChangeButtons', this),
519             headerContentChange: afterContentChange,
520             bodyContentChange  : afterContentChange,
521             footerContentChange: afterContentChange
522         });
523     },
525     /**
526     Returns a button node based on the specified `button` node or configuration.
528     The button node will either be created via `Y.Plugin.Button.createNode()`,
529     or when `button` is specified as a node already, it will by `plug()`ed with
530     `Y.Plugin.Button`.
532     @method _createButton
533     @param {Node|Object} button Button node or configuration object.
534     @return {Node} The button node.
535     @protected
536     @since 3.5.0
537     **/
538     _createButton: function (button) {
539         var config, buttonConfig, nonButtonNodeCfg,
540             i, len, action, context, handle;
542         // Makes sure the exiting `Y.Node` instance is from this YUI sandbox and
543         // is plugged with `Y.Plugin.Button`.
544         if (isNode(button)) {
545             return Y.one(button.getDOMNode()).plug(ButtonPlugin);
546         }
548         // Merge `button` config with defaults and back-compat.
549         config = Y.merge({
550             context: this,
551             events : 'click',
552             label  : button.value
553         }, button);
555         buttonConfig     = Y.merge(config);
556         nonButtonNodeCfg = WidgetButtons.NON_BUTTON_NODE_CFG;
558         // Remove all non-button Node config props.
559         for (i = 0, len = nonButtonNodeCfg.length; i < len; i += 1) {
560             delete buttonConfig[nonButtonNodeCfg[i]];
561         }
563         // Create the button node using the button Node-only config.
564         button = ButtonPlugin.createNode(buttonConfig);
566         context = config.context;
567         action  = config.action;
569         // Supports `action` as a String name of a Function on the `context`
570         // object.
571         if (isString(action)) {
572             action = Y.bind(action, context);
573         }
575         // Supports all types of crazy configs for event subscriptions and
576         // stores a reference to the returned `EventHandle`.
577         handle = button.on(config.events, action, context);
578         this._buttonsHandles[Y.stamp(button, true)] = handle;
580         // Tags the button with the configured `name` and `isDefault` settings.
581         button.setData('name', this._getButtonName(config));
582         button.setData('default', this._getButtonDefault(config));
584         // Add any CSS classnames to the button node.
585         YArray.each(YArray(config.classNames), button.addClass, button);
587         return button;
588     },
590     /**
591     Returns the buttons container for the specified `section`, passing a truthy
592     value for `create` will create the node if it does not already exist.
594     **Note:** It is up to the caller to properly insert the returned container
595     node into the content section.
597     @method _getButtonContainer
598     @param {String} section The `WidgetStdMod` section (header/body/footer).
599     @param {Boolean} create Whether the buttons container should be created if
600         it does not already exist.
601     @return {Node} The buttons container node for the specified `section`.
602     @protected
603     @see BUTTONS_TEMPLATE
604     @since 3.5.0
605     **/
606     _getButtonContainer: function (section, create) {
607         var sectionClassName = WidgetStdMod.SECTION_CLASS_NAMES[section],
608             buttonsClassName = WidgetButtons.CLASS_NAMES.buttons,
609             contentBox       = this.get('contentBox'),
610             containerSelector, container;
612         // Search for an existing buttons container within the section.
613         containerSelector = '.' + sectionClassName + ' .' + buttonsClassName;
614         container         = contentBox.one(containerSelector);
616         // Create the `container` if it doesn't already exist.
617         if (!container && create) {
618             container = Y.Node.create(this.BUTTONS_TEMPLATE);
619             container.addClass(buttonsClassName);
620         }
622         return container;
623     },
625     /**
626     Returns whether or not the specified `button` is configured to be the
627     default button.
629     When a button node is specified, the button's `getData()` method will be
630     used to determine if the button is configured to be the default. When a
631     button config object is specified, the `isDefault` prop will determine
632     whether the button is the default.
634     **Note:** `<button data-default="true"></button>` is supported via the
635     `button.getData('default')` API call.
637     @method _getButtonDefault
638     @param {Node|Object} button The button node or configuration object.
639     @return {Boolean} Whether the button is configured to be the default button.
640     @protected
641     @since 3.5.0
642     **/
643     _getButtonDefault: function (button) {
644         var isDefault = isNode(button) ?
645                 button.getData('default') : button.isDefault;
647         if (isString(isDefault)) {
648             return isDefault.toLowerCase() === 'true';
649         }
651         return !!isDefault;
652     },
654     /**
655     Returns the name of the specified `button`.
657     When a button node is specified, the button's `getData('name')` method is
658     preferred, but will fallback to `get('name')`, and the result will determine
659     the button's name. When a button config object is specified, the `name` prop
660     will determine the button's name.
662     **Note:** `<button data-name="foo"></button>` is supported via the
663     `button.getData('name')` API call.
665     @method _getButtonName
666     @param {Node|Object} button The button node or configuration object.
667     @return {String} The name of the button.
668     @protected
669     @since 3.5.0
670     **/
671     _getButtonName: function (button) {
672         var name;
674         if (isNode(button)) {
675             name = button.getData('name') || button.get('name');
676         } else {
677             name = button && (button.name || button.type);
678         }
680         return name;
681     },
683     /**
684     Getter for the `buttons` attribute. A copy of the `buttons` object is
685     returned so the stored state cannot be modified by the callers of
686     `get('buttons')`.
688     This will recreate a copy of the `buttons` object, and each section array
689     (the button nodes are *not* copied/cloned.)
691     @method _getButtons
692     @param {Object} buttons The widget's current `buttons` state.
693     @return {Object} A copy of the widget's current `buttons` state.
694     @protected
695     @since 3.5.0
696     **/
697     _getButtons: function (buttons) {
698         var buttonsCopy = {};
700         // Creates a new copy of the `buttons` object.
701         YObject.each(buttons, function (sectionButtons, section) {
702             // Creates of copy of the array of button nodes.
703             buttonsCopy[section] = sectionButtons.concat();
704         });
706         return buttonsCopy;
707     },
709     /**
710     Adds the specified `button` to the buttons map (both name -> button and
711     section:name -> button), and sets the button as the default if it is
712     configured as the default button.
714     **Note:** If two or more buttons are configured with the same `name` and/or
715     configured to be the default button, the last one wins.
717     @method _mapButton
718     @param {Node} button The button node to map.
719     @param {String} section The `WidgetStdMod` section (header/body/footer).
720     @protected
721     @since 3.5.0
722     **/
723     _mapButton: function (button, section) {
724         var map       = this._buttonsMap,
725             name      = this._getButtonName(button),
726             isDefault = this._getButtonDefault(button);
728         if (name) {
729             // name -> button
730             map[name] = button;
732             // section:name -> button
733             map[section + ':' + name] = button;
734         }
736         isDefault && (this._defaultButton = button);
737     },
739     /**
740     Adds the specified `buttons` to the buttons map (both name -> button and
741     section:name -> button), and set the a button as the default if one is
742     configured as the default button.
744     **Note:** This will clear all previous button mappings and null-out any
745     previous default button! If two or more buttons are configured with the same
746     `name` and/or configured to be the default button, the last one wins.
748     @method _mapButtons
749     @param {Node[]} buttons The button nodes to map.
750     @protected
751     @since 3.5.0
752     **/
753     _mapButtons: function (buttons) {
754         this._buttonsMap    = {};
755         this._defaultButton = null;
757         YObject.each(buttons, function (sectionButtons, section) {
758             var i, len;
760             for (i = 0, len = sectionButtons.length; i < len; i += 1) {
761                 this._mapButton(sectionButtons[i], section);
762             }
763         }, this);
764     },
766     /**
767     Returns a copy of the specified `config` object merged with any defaults
768     provided by a `srcNode` and/or a predefined configuration for a button
769     with the same `name` on the `BUTTONS` property.
771     @method _mergeButtonConfig
772     @param {Object|String} config Button configuration object, or string name.
773     @return {Object} A copy of the button configuration object merged with any
774         defaults.
775     @protected
776     @since 3.5.0
777     **/
778     _mergeButtonConfig: function (config) {
779         var buttonConfig, defConfig, name, button, tagName, label;
781         // Makes sure `config` is an Object and a copy of the specified value.
782         config = isString(config) ? {name: config} : Y.merge(config);
784         // Seeds default values from the button node, if there is one.
785         if (config.srcNode) {
786             button  = config.srcNode;
787             tagName = button.get('tagName').toLowerCase();
788             label   = button.get(tagName === 'input' ? 'value' : 'text');
790             // Makes sure the button's current values override any defaults.
791             buttonConfig = {
792                 disabled : !!button.get('disabled'),
793                 isDefault: this._getButtonDefault(button),
794                 name     : this._getButtonName(button)
795             };
797             // Label should only be considered when not an empty string.
798             label && (buttonConfig.label = label);
800             // Merge `config` with `buttonConfig` values.
801             Y.mix(config, buttonConfig, false, null, 0, true);
802         }
804         name      = this._getButtonName(config);
805         defConfig = this.BUTTONS && this.BUTTONS[name];
807         // Merge `config` with predefined default values.
808         if (defConfig) {
809             Y.mix(config, defConfig, false, null, 0, true);
810         }
812         return config;
813     },
815     /**
816     `HTML_PARSER` implementation for the `buttons` attribute.
818     **Note:** To determine a button node's name its `data-name` and `name`
819     attributes are examined. Whether the button should be the default is
820     determined by its `data-default` attribute.
822     @method _parseButtons
823     @param {Node} srcNode This widget's srcNode to search for buttons.
824     @return {null|Object} `buttons` Config object parsed from this widget's DOM.
825     @protected
826     @since 3.5.0
827     **/
828     _parseButtons: function (srcNode) {
829         var buttonSelector = '.' + WidgetButtons.CLASS_NAMES.button,
830             sections       = ['header', 'body', 'footer'],
831             buttonsConfig  = null;
833         YArray.each(sections, function (section) {
834             var container = this._getButtonContainer(section),
835                 buttons   = container && container.all(buttonSelector),
836                 sectionButtons;
838             if (!buttons || buttons.isEmpty()) { return; }
840             sectionButtons = [];
842             // Creates a button config object for every button node found and
843             // adds it to the section. This way each button configuration can be
844             // merged with any defaults provided by predefined `BUTTONS`.
845             buttons.each(function (button) {
846                 sectionButtons.push({srcNode: button});
847             });
849             buttonsConfig || (buttonsConfig = {});
850             buttonsConfig[section] = sectionButtons;
851         }, this);
853         return buttonsConfig;
854     },
856     /**
857     Setter for the `buttons` attribute. This processes the specified `config`
858     and returns a new `buttons` object which is stored as the new state; leaving
859     the original, specified `config` unmodified.
861     The button nodes will either be created via `Y.Plugin.Button.createNode()`,
862     or when a button is already a Node already, it will by `plug()`ed with
863     `Y.Plugin.Button`.
865     @method _setButtons
866     @param {Array|Object} config The `buttons` configuration to process.
867     @return {Object} The processed `buttons` object which represents the new
868         state.
869     @protected
870     @since 3.5.0
871     **/
872     _setButtons: function (config) {
873         var defSection = this.DEFAULT_BUTTONS_SECTION,
874             buttons    = {};
876         function processButtons(buttonConfigs, currentSection) {
877             if (!isArray(buttonConfigs)) { return; }
879             var i, len, button, section;
881             for (i = 0, len = buttonConfigs.length; i < len; i += 1) {
882                 button  = buttonConfigs[i];
883                 section = currentSection;
885                 if (!isNode(button)) {
886                     button = this._mergeButtonConfig(button);
887                     section || (section = button.section);
888                 }
890                 // Always passes through `_createButton()` to make sure the node
891                 // is decorated as a button.
892                 button = this._createButton(button);
894                 // Use provided `section` or fallback to the default section.
895                 section || (section = defSection);
897                 // Add button to the array of buttons for the specified section.
898                 (buttons[section] || (buttons[section] = [])).push(button);
899             }
900         }
902         // Handle `config` being either an Array or Object of Arrays.
903         if (isArray(config)) {
904             processButtons.call(this, config);
905         } else {
906             YObject.each(config, processButtons, this);
907         }
909         return buttons;
910     },
912     /**
913     Syncs this widget's current button-related state to its DOM. This method is
914     inserted via AOP, and will execute after `syncUI()`.
916     @method _syncUIButtons
917     @protected
918     @since 3.4.0
919     **/
920     _syncUIButtons: function () {
921         this._uiSetButtons(this.get('buttons'));
922         this._uiSetDefaultButton(this.get('defaultButton'));
923         this._uiSetVisibleButtons(this.get('visible'));
924     },
926     /**
927     Inserts the specified `button` node into this widget's DOM at the specified
928     `section` and `index` and updates the section content.
930     The section and button container nodes will be created if they do not
931     already exist.
933     @method _uiInsertButton
934     @param {Node} button The button node to insert into this widget's DOM.
935     @param {String} section The `WidgetStdMod` section (header/body/footer).
936     @param {Number} index Index at which the `button` should be positioned.
937     @protected
938     @since 3.5.0
939     **/
940     _uiInsertButton: function (button, section, index) {
941         var buttonsClassName = WidgetButtons.CLASS_NAMES.button,
942             buttonContainer  = this._getButtonContainer(section, true),
943             sectionButtons   = buttonContainer.all('.' + buttonsClassName);
945         // Inserts the button node at the correct index.
946         buttonContainer.insertBefore(button, sectionButtons.item(index));
948         // Adds the button container to the section content.
949         this.setStdModContent(section, buttonContainer, 'after');
950     },
952     /**
953     Removes the button node from this widget's DOM and detaches any event
954     subscriptions on the button that were created by this widget. The section
955     content will be updated unless `{preserveContent: true}` is passed in the
956     `options`.
958     By default the button container node will be removed when this removes the
959     last button of the specified `section`; and if no other content remains in
960     the section node, it will also be removed.
962     @method _uiRemoveButton
963     @param {Node} button The button to remove and destroy.
964     @param {String} section The `WidgetStdMod` section (header/body/footer).
965     @param {Object} [options] Additional options.
966       @param {Boolean} [options.preserveContent=false] Whether the section
967         content should be updated.
968     @protected
969     @since 3.5.0
970     **/
971     _uiRemoveButton: function (button, section, options) {
972         var yuid    = Y.stamp(button, this),
973             handles = this._buttonsHandles,
974             handle  = handles[yuid],
975             buttonContainer, buttonClassName;
977         if (handle) {
978             handle.detach();
979         }
981         delete handles[yuid];
983         button.remove();
985         options || (options = {});
987         // Remove the button container and section nodes if needed.
988         if (!options.preserveContent) {
989             buttonContainer = this._getButtonContainer(section);
990             buttonClassName = WidgetButtons.CLASS_NAMES.button;
992             // Only matters if we have a button container which is empty.
993             if (buttonContainer &&
994                     buttonContainer.all('.' + buttonClassName).isEmpty()) {
996                 buttonContainer.remove();
997                 this._updateContentButtons(section);
998             }
999         }
1000     },
1002     /**
1003     Sets the current `buttons` state to this widget's DOM by rendering the
1004     specified collection of `buttons` and updates the contents of each section
1005     as needed.
1007     Button nodes which already exist in the DOM will remain intact, or will be
1008     moved if they should be in a new position. Old button nodes which are no
1009     longer represented in the specified `buttons` collection will be removed,
1010     and any event subscriptions on the button which were created by this widget
1011     will be detached.
1013     If the button nodes in this widget's DOM actually change, then each content
1014     section will be updated (or removed) appropriately.
1016     @method _uiSetButtons
1017     @param {Object} buttons The current `buttons` state to visually represent.
1018     @protected
1019     @since 3.5.0
1020     **/
1021     _uiSetButtons: function (buttons) {
1022         var buttonClassName = WidgetButtons.CLASS_NAMES.button,
1023             sections        = ['header', 'body', 'footer'];
1025         YArray.each(sections, function (section) {
1026             var sectionButtons  = buttons[section] || [],
1027                 numButtons      = sectionButtons.length,
1028                 buttonContainer = this._getButtonContainer(section, numButtons),
1029                 buttonsUpdated  = false,
1030                 oldNodes, i, button, buttonIndex;
1032             // When there's no button container, there are no new buttons or old
1033             // buttons that we have to deal with for this section.
1034             if (!buttonContainer) { return; }
1036             oldNodes = buttonContainer.all('.' + buttonClassName);
1038             for (i = 0; i < numButtons; i += 1) {
1039                 button      = sectionButtons[i];
1040                 buttonIndex = oldNodes.indexOf(button);
1042                 // Buttons already rendered in the Widget should remain there or
1043                 // moved to their new index. New buttons will be added to the
1044                 // current `buttonContainer`.
1045                 if (buttonIndex > -1) {
1046                     // Remove button from existing buttons nodeList since its in
1047                     // the DOM already.
1048                     oldNodes.splice(buttonIndex, 1);
1050                     // Check that the button is at the right position, if not,
1051                     // move it to its new position.
1052                     if (buttonIndex !== i) {
1053                         // Using `i + 1` because the button should be at index
1054                         // `i`; it's inserted before the node which comes after.
1055                         buttonContainer.insertBefore(button, i + 1);
1056                         buttonsUpdated = true;
1057                     }
1058                 } else {
1059                     buttonContainer.appendChild(button);
1060                     buttonsUpdated = true;
1061                 }
1062             }
1064             // Safely removes the old button nodes which are no longer part of
1065             // this widget's `buttons`.
1066             oldNodes.each(function (button) {
1067                 this._uiRemoveButton(button, section, {preserveContent: true});
1068                 buttonsUpdated = true;
1069             }, this);
1071             // Remove leftover empty button containers and updated the StdMod
1072             // content area.
1073             if (numButtons === 0) {
1074                 buttonContainer.remove();
1075                 this._updateContentButtons(section);
1076                 return;
1077             }
1079             // Adds the button container to the section content.
1080             if (buttonsUpdated) {
1081                 this.setStdModContent(section, buttonContainer, 'after');
1082             }
1083         }, this);
1084     },
1086     /**
1087     Adds the "yui3-button-primary" CSS class to the new `defaultButton` and
1088     removes it from the old default button.
1090     @method _uiSetDefaultButton
1091     @param {Node} newButton The new `defaultButton`.
1092     @param {Node} oldButton The old `defaultButton`.
1093     @protected
1094     @since 3.5.0
1095     **/
1096     _uiSetDefaultButton: function (newButton, oldButton) {
1097         var primaryClassName = WidgetButtons.CLASS_NAMES.primary;
1099         if (newButton) { newButton.addClass(primaryClassName); }
1100         if (oldButton) { oldButton.removeClass(primaryClassName); }
1101     },
1103     /**
1104     Focuses this widget's `defaultButton` if there is one and this widget is
1105     visible.
1107     @method _uiSetVisibleButtons
1108     @param {Boolean} visible Whether this widget is visible.
1109     @protected
1110     @since 3.5.0
1111     **/
1112     _uiSetVisibleButtons: function (visible) {
1113         if (!visible) { return; }
1115         var defaultButton = this.get('defaultButton');
1116         if (defaultButton) {
1117             defaultButton.focus();
1118         }
1119     },
1121     /**
1122     Removes the specified `button` from the buttons map (both name -> button and
1123     section:name -> button), and nulls-out the `defaultButton` if it is
1124     currently the default button.
1126     @method _unMapButton
1127     @param {Node} button The button node to remove from the buttons map.
1128     @param {String} section The `WidgetStdMod` section (header/body/footer).
1129     @protected
1130     @since 3.5.0
1131     **/
1132     _unMapButton: function (button, section) {
1133         var map  = this._buttonsMap,
1134             name = this._getButtonName(button),
1135             sectionName;
1137         // Only delete the map entry if the specified `button` is mapped to it.
1138         if (name) {
1139             // name -> button
1140             if (map[name] === button) {
1141                 delete map[name];
1142             }
1144             // section:name -> button
1145             sectionName = section + ':' + name;
1146             if (map[sectionName] === button) {
1147                 delete map[sectionName];
1148             }
1149         }
1151         // Clear the default button if its the specified `button`.
1152         if (this._defaultButton === button) {
1153             this._defaultButton = null;
1154         }
1155     },
1157     /**
1158     Updates the `defaultButton` attribute if it needs to be updated by comparing
1159     its current value with the protected `_defaultButton` property.
1161     @method _updateDefaultButton
1162     @protected
1163     @since 3.5.0
1164     **/
1165     _updateDefaultButton: function () {
1166         var defaultButton = this._defaultButton;
1168         if (this.get('defaultButton') !== defaultButton) {
1169             this._set('defaultButton', defaultButton);
1170         }
1171     },
1173     /**
1174     Updates the content attribute which corresponds to the specified `section`.
1176     The method updates the section's content to its current `childNodes`
1177     (text and/or HTMLElement), or will null-out its contents if the section is
1178     empty. It also specifies a `src` of `buttons` on the change event facade.
1180     @method _updateContentButtons
1181     @param {String} section The `WidgetStdMod` section (header/body/footer) to
1182         update.
1183     @protected
1184     @since 3.5.0
1185     **/
1186     _updateContentButtons: function (section) {
1187         // `childNodes` return text nodes and HTMLElements.
1188         var sectionContent = this.getStdModNode(section).get('childNodes');
1190         // Updates the section to its current contents, or null if it is empty.
1191         this.set(section + 'Content', sectionContent.isEmpty() ? null :
1192             sectionContent, {src: 'buttons'});
1193     },
1195     // -- Protected Event Handlers ---------------------------------------------
1197     /**
1198     Handles this widget's `buttonsChange` event which fires anytime the
1199     `buttons` attribute is modified.
1201     **Note:** This method special-cases the `buttons` modifications caused by
1202     `addButton()` and `removeButton()`, both of which set the `src` property on
1203     the event facade to "add" and "remove" respectively.
1205     @method _afterButtonsChange
1206     @param {EventFacade} e
1207     @protected
1208     @since 3.4.0
1209     **/
1210     _afterButtonsChange: function (e) {
1211         var buttons = e.newVal,
1212             section = e.section,
1213             index   = e.index,
1214             src     = e.src,
1215             button;
1217         // Special cases `addButton()` to only set and insert the new button.
1218         if (src === 'add') {
1219             // Make sure we have the button node.
1220             button = buttons[section][index];
1222             this._mapButton(button, section);
1223             this._updateDefaultButton();
1224             this._uiInsertButton(button, section, index);
1226             return;
1227         }
1229         // Special cases `removeButton()` to only remove the specified button.
1230         if (src === 'remove') {
1231             // Button node already exists on the event facade.
1232             button = e.button;
1234             this._unMapButton(button, section);
1235             this._updateDefaultButton();
1236             this._uiRemoveButton(button, section);
1238             return;
1239         }
1241         this._mapButtons(buttons);
1242         this._updateDefaultButton();
1243         this._uiSetButtons(buttons);
1244     },
1246     /**
1247     Handles this widget's `headerContentChange`, `bodyContentChange`,
1248     `footerContentChange` events by making sure the `buttons` remain rendered
1249     after changes to the content areas.
1251     These events are very chatty, so extra caution is taken to avoid doing extra
1252     work or getting into an infinite loop.
1254     @method _afterContentChangeButtons
1255     @param {EventFacade} e
1256     @protected
1257     @since 3.5.0
1258     **/
1259     _afterContentChangeButtons: function (e) {
1260         var src     = e.src,
1261             pos     = e.stdModPosition,
1262             replace = !pos || pos === WidgetStdMod.REPLACE;
1264         // Only do work when absolutely necessary.
1265         if (replace && src !== 'buttons' && src !== Widget.UI_SRC) {
1266             this._uiSetButtons(this.get('buttons'));
1267         }
1268     },
1270     /**
1271     Handles this widget's `defaultButtonChange` event by adding the
1272     "yui3-button-primary" CSS class to the new `defaultButton` and removing it
1273     from the old default button.
1275     @method _afterDefaultButtonChange
1276     @param {EventFacade} e
1277     @protected
1278     @since 3.5.0
1279     **/
1280     _afterDefaultButtonChange: function (e) {
1281         this._uiSetDefaultButton(e.newVal, e.prevVal);
1282     },
1284     /**
1285     Handles this widget's `visibleChange` event by focusing the `defaultButton`
1286     if there is one.
1288     @method _afterVisibleChangeButtons
1289     @param {EventFacade} e
1290     @protected
1291     @since 3.5.0
1292     **/
1293     _afterVisibleChangeButtons: function (e) {
1294         this._uiSetVisibleButtons(e.newVal);
1295     }
1298 Y.WidgetButtons = WidgetButtons;
1301 }, '3.13.0', {"requires": ["button-plugin", "cssbutton", "widget-stdmod"]});