3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('widget-buttons', function(Y) {
10 Provides header/body/footer button support for Widgets that use the
11 `WidgetStdMod` extension.
13 @module widget-buttons
21 ButtonPlugin = Y.Plugin.Button,
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;
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.
46 function WidgetButtons() {
47 // Require `Y.WidgetStdMod`.
48 if (!this._stdModNode) {
49 Y.error('WidgetStdMod must be added to a Widget before WidgetButtons.');
52 // Has to be setup before the `initializer()`.
53 this._buttonsHandles = {};
56 WidgetButtons.ATTRS = {
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
68 All specified values will be normalized to this type of structure:
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.
88 // Uses predefined "close" button by string name.
104 click: function (e) {
118 getter: '_getButtons',
119 setter: '_setButtons',
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,
140 @attribute defaultButton
153 CSS classes used by `WidgetButtons`.
155 @property CLASS_NAMES
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);
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
181 WidgetButtons.NON_BUTTON_NODE_CFG = [
182 'action', 'classNames', 'context', 'events', 'isDefault', 'section'
185 WidgetButtons.prototype = {
186 // -- Public Properties ----------------------------------------------------
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.
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
213 BUTTONS_TEMPLATE: '<span />',
216 The default section to render buttons in when no section is specified.
218 @property DEFAULT_BUTTONS_SECTION
220 @default Y.WidgetStdMod.FOOTER
223 DEFAULT_BUTTONS_SECTION: WidgetStdMod.FOOTER,
225 // -- Protected Properties -------------------------------------------------
228 A map of button node `_yuid` -> event-handle for all button nodes which were
229 created by this widget.
231 @property _buttonsHandles
238 A map of this widget's `buttons`, both name -> button and
239 section:name -> button.
241 @property _buttonsMap
248 Internal reference to this widget's default button.
250 @property _defaultButton
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');
270 destructor: function () {
271 // Detach all event subscriptions this widget added to its `buttons`.
272 YObject.each(this._buttonsHandles, function (handle) {
276 delete this._buttonsHandles;
277 delete this._buttonsMap;
278 delete this._defaultButton;
281 // -- Public Methods -------------------------------------------------------
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
291 This fires the `buttonsChange` event and adds the following properties to
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.
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
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
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
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
327 @param {String} [button.label] The visible text/value displayed in the
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 <button>
337 node will be created.
338 @param {String} [button.template] A specific template to use when creating
339 a new button node (e.g. "<a />"). **Note:** Specifying a `srcNode`
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.
347 @see Plugin.Button.createNode()
350 addButton: function (button, section, index) {
351 var buttons = this.get('buttons'),
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);
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, {
378 Returns a button node from this widget's `buttons`.
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
386 @return {Node} The button node.
389 getButton: function (name, section) {
390 if (!isValue(name)) { return; }
392 var map = this._buttonsMap,
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];
403 // Looks up button by name or section:name.
404 return arguments.length > 1 ? map[section + ':' + name] : map[name];
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
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.
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
437 removeButton: function (button, section) {
438 if (!isValue(button)) { return this; }
440 var buttons = this.get('buttons'),
443 // Shortcut if `button` is already an index which is needed for slicing.
444 if (isNumber(button)) {
445 section || (section = this.DEFAULT_BUTTONS_SECTION);
447 button = buttons[section][index];
449 // Supports `button` being the string name.
450 if (isString(button)) {
451 button = this.getButton.apply(this, arguments);
454 // Determines the `section` and `index` at which the button exists.
455 YObject.some(buttons, function (sectionButtons, currentSection) {
456 index = YArray.indexOf(sectionButtons, button);
459 section = currentSection;
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, {
481 // -- Protected Methods ----------------------------------------------------
484 Binds UI event listeners. This method is inserted via AOP, and will execute
487 @method _bindUIButtons
491 _bindUIButtons: function () {
492 // Event handlers are bound with `bind()` to make them more extensible.
494 var afterContentChange = Y.bind('_afterContentChangeButtons', this);
497 defaultButtonChange: Y.bind('_afterDefaultButtonChange', this),
498 visibleChange : Y.bind('_afterVisibleChangeButtons', this),
499 headerContentChange: afterContentChange,
500 bodyContentChange : afterContentChange,
501 footerContentChange: afterContentChange
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
512 @method _createButton
513 @param {Node|Object} button Button node or configuration object.
514 @return {Node} The button node.
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);
527 // Merge `button` config with defaults and back-compat.
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]];
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`
550 if (isString(action)) {
551 action = Y.bind(action, context);
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);
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`.
582 @see BUTTONS_TEMPLATE
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);
605 Returns whether or not the specified `button` is configured to be the
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.
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';
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.
650 _getButtonName: function (button) {
653 if (Y.instanceOf(button, Y.Node)) {
654 name = button.getData('name') || button.get('name');
656 name = button && (button.name || button.type);
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
667 This will recreate a copy of the `buttons` object, and each section array
668 (the button nodes are *not* copied/cloned.)
671 @param {Object} buttons The widget's current `buttons` state.
672 @return {Object} A copy of the widget's current `buttons` state.
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();
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.
697 @param {Node} button The button node to map.
698 @param {String} section The `WidgetStdMod` section.
702 _mapButton: function (button, section) {
703 var map = this._buttonsMap,
704 name = this._getButtonName(button),
705 isDefault = this._getButtonDefault(button);
711 // section:name -> button
712 map[section + ':' + name] = button;
715 isDefault && (this._defaultButton = button);
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.
728 @param {Node[]} buttons The button nodes to map.
732 _mapButtons: function (buttons) {
733 this._buttonsMap = {};
734 this._defaultButton = null;
736 YObject.each(buttons, function (sectionButtons, section) {
739 for (i = 0, len = sectionButtons.length; i < len; i += 1) {
740 this._mapButton(sectionButtons[i], section);
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
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.
771 disabled : !!button.get('disabled'),
772 isDefault: this._getButtonDefault(button),
773 name : this._getButtonName(button)
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);
783 name = this._getButtonName(config);
784 defConfig = this.BUTTONS && this.BUTTONS[name];
786 // Merge `config` with predefined default values.
788 Y.mix(config, defConfig, false, null, 0, true);
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.
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),
817 if (!buttons || buttons.isEmpty()) { return; }
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});
828 buttonsConfig || (buttonsConfig = {});
829 buttonsConfig[section] = sectionButtons;
832 return buttonsConfig;
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
845 @param {Array|Object} config The `buttons` configuration to process.
846 @return {Object} The processed `buttons` object which represents the new
851 _setButtons: function (config) {
852 var defSection = this.DEFAULT_BUTTONS_SECTION,
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);
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);
881 // Handle `config` being either an Array or Object of Arrays.
882 if (isArray(config)) {
883 processButtons.call(this, config);
885 YObject.each(config, processButtons, this);
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
899 _syncUIButtons: function () {
900 this._uiSetButtons(this.get('buttons'));
901 this._uiSetDefaultButton(this.get('defaultButton'));
902 this._uiSetVisibleButtons(this.get('visible'));
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
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.
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');
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
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.
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];
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);
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
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
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.
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
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;
1035 buttonContainer.appendChild(button);
1036 buttonsUpdated = true;
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;
1047 // Remove leftover empty button containers and updated the StdMod
1049 if (numButtons === 0) {
1050 buttonContainer.remove();
1051 this._updateContentButtons(section);
1055 // Adds the button container to the section content.
1056 if (buttonsUpdated) {
1057 this.setStdModContent(section, buttonContainer, 'after');
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`.
1072 _uiSetDefaultButton: function (newButton, oldButton) {
1073 var primaryClassName = WidgetButtons.CLASS_NAMES.primary;
1075 newButton && newButton.addClass(primaryClassName);
1076 oldButton && oldButton.removeClass(primaryClassName);
1080 Focuses this widget's `defaultButton` if there is one and this widget is
1083 @method _uiSetVisibleButtons
1084 @param {Boolean} visible Whether this widget is visible.
1088 _uiSetVisibleButtons: function (visible) {
1089 if (!visible) { return; }
1091 var defaultButton = this.get('defaultButton');
1092 if (defaultButton) {
1093 defaultButton.focus();
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.
1106 _unMapButton: function (button, section) {
1107 var map = this._buttonsMap,
1108 name = this._getButtonName(button),
1111 // Only delete the map entry if the specified `button` is mapped to it.
1114 if (map[name] === button) {
1118 // section:name -> button
1119 sectionName = section + ':' + name;
1120 if (map[sectionName] === button) {
1121 delete map[sectionName];
1125 // Clear the default button if its the specified `button`.
1126 if (this._defaultButton === button) {
1127 this._defaultButton = null;
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
1139 _updateDefaultButton: function () {
1140 var defaultButton = this._defaultButton;
1142 if (this.get('defaultButton') !== defaultButton) {
1143 this._set('defaultButton', defaultButton);
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
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'});
1169 // -- Protected Event Handlers ---------------------------------------------
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
1184 _afterButtonsChange: function (e) {
1185 var buttons = e.newVal,
1186 section = e.section,
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);
1203 // Special cases `removeButton()` to only remove the specified button.
1204 if (src === 'remove') {
1205 // Button node already exists on the event facade.
1208 this._unMapButton(button, section);
1209 this._updateDefaultButton();
1210 this._uiRemoveButton(button, section);
1215 this._mapButtons(buttons);
1216 this._updateDefaultButton();
1217 this._uiSetButtons(buttons);
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
1233 _afterContentChangeButtons: function (e) {
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'));
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
1254 _afterDefaultButtonChange: function (e) {
1255 this._uiSetDefaultButton(e.newVal, e.prevVal);
1259 Handles this widget's `visibleChange` event by focusing the `defaultButton`
1262 @method _afterVisibleChangeButtons
1263 @param {EventFacade} e
1267 _afterVisibleChangeButtons: function (e) {
1268 this._uiSetVisibleButtons(e.newVal);
1272 Y.WidgetButtons = WidgetButtons;
1275 }, '3.5.0' ,{requires:['button-plugin', 'cssbutton', 'widget-stdmod']});