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;
31 // Utility to determine if an object is a Y.Node instance, even if it was
32 // created in a different YUI sandbox.
33 function isNode(node) {
34 return !!node.getDOMNode;
38 Provides header/body/footer button support for Widgets that use the
39 `WidgetStdMod` extension.
41 This Widget extension makes it easy to declaratively configure a widget's
42 buttons. It adds a `buttons` attribute along with button- accessor and mutator
43 methods. All button nodes have the `Y.Plugin.Button` plugin applied.
45 This extension also includes `HTML_PARSER` support to seed a widget's `buttons`
46 from those which already exist in its DOM.
52 function WidgetButtons() {
53 // Require `Y.WidgetStdMod`.
54 if (!this._stdModNode) {
55 Y.error('WidgetStdMod must be added to a Widget before WidgetButtons.');
58 // Has to be setup before the `initializer()`.
59 this._buttonsHandles = {};
62 WidgetButtons.ATTRS = {
64 Collection containing a widget's buttons.
66 The collection is an Object which contains an Array of `Y.Node`s for every
67 `WidgetStdMod` section (header, body, footer) which has one or more buttons.
68 All button nodes have the `Y.Plugin.Button` plugin applied.
70 This attribute is very flexible in the values it will accept. `buttons` can
71 be specified as a single Array, or an Object of Arrays keyed to a particular
74 All specified values will be normalized to this type of structure:
81 A button can be specified as a `Y.Node`, config Object, or String name for a
82 predefined button on the `BUTTONS` prototype property. When a config Object
83 is provided, it will be merged with any defaults provided by a button with
84 the same `name` defined on the `BUTTONS` property.
86 See `addButton()` for the detailed list of configuration properties.
88 For convenience, a widget's buttons will always persist and remain rendered
89 after header/body/footer content updates. Buttons should be removed by
90 updating this attribute or using the `removeButton()` method.
94 // Uses predefined "close" button by string name.
110 click: function (e) {
124 getter: '_getButtons',
125 setter: '_setButtons',
130 The current default button as configured through this widget's `buttons`.
132 A button can be configured as the default button in the following ways:
134 * As a config Object with an `isDefault` property:
135 `{label: 'Okay', isDefault: true}`.
137 * As a Node with a `data-default` attribute:
138 `<button data-default="true">Okay</button>`.
140 This attribute is **read-only**; anytime there are changes to this widget's
141 `buttons`, the `defaultButton` will be updated if needed.
143 **Note:** If two or more buttons are configured to be the default button,
146 @attribute defaultButton
159 CSS classes used by `WidgetButtons`.
161 @property CLASS_NAMES
166 WidgetButtons.CLASS_NAMES = {
167 button : getClassName('button'),
168 buttons: Widget.getClassName('buttons'),
169 primary: getClassName('button', 'primary')
172 WidgetButtons.HTML_PARSER = {
173 buttons: function (srcNode) {
174 return this._parseButtons(srcNode);
179 The list of button configuration properties which are specific to
180 `WidgetButtons` and should not be passed to `Y.Plugin.Button.createNode()`.
182 @property NON_BUTTON_NODE_CFG
187 WidgetButtons.NON_BUTTON_NODE_CFG = [
188 'action', 'classNames', 'context', 'events', 'isDefault', 'section'
191 WidgetButtons.prototype = {
192 // -- Public Properties ----------------------------------------------------
195 Collection of predefined buttons mapped by name -> config.
197 These button configurations will serve as defaults for any button added to a
198 widget's buttons which have the same `name`.
200 See `addButton()` for a list of possible configuration values.
211 The HTML template to use when creating the node which wraps all buttons of a
212 section. By default it will have the CSS class: "yui3-widget-buttons".
214 @property BUTTONS_TEMPLATE
219 BUTTONS_TEMPLATE: '<span />',
222 The default section to render buttons in when no section is specified.
224 @property DEFAULT_BUTTONS_SECTION
226 @default Y.WidgetStdMod.FOOTER
229 DEFAULT_BUTTONS_SECTION: WidgetStdMod.FOOTER,
231 // -- Protected Properties -------------------------------------------------
234 A map of button node `_yuid` -> event-handle for all button nodes which were
235 created by this widget.
237 @property _buttonsHandles
244 A map of this widget's `buttons`, both name -> button and
245 section:name -> button.
247 @property _buttonsMap
254 Internal reference to this widget's default button.
256 @property _defaultButton
262 // -- Lifecycle Methods ----------------------------------------------------
264 initializer: function () {
265 // Creates button mappings and sets the `defaultButton`.
266 this._mapButtons(this.get('buttons'));
267 this._updateDefaultButton();
269 // Bound with `Y.bind()` to make more extensible.
270 this.after('buttonsChange', Y.bind('_afterButtonsChange', this));
272 Y.after(this._bindUIButtons, this, 'bindUI');
273 Y.after(this._syncUIButtons, this, 'syncUI');
276 destructor: function () {
277 // Detach all event subscriptions this widget added to its `buttons`.
278 YObject.each(this._buttonsHandles, function (handle) {
282 delete this._buttonsHandles;
283 delete this._buttonsMap;
284 delete this._defaultButton;
287 // -- Public Methods -------------------------------------------------------
290 Adds a button to this widget.
292 The new button node will have the `Y.Plugin.Button` plugin applied, be added
293 to this widget's `buttons`, and rendered in the specified `section` at the
294 specified `index` (or end of the section when no `index` is provided). If
295 the section does not exist, it will be created.
297 This fires the `buttonsChange` event and adds the following properties to
300 * `button`: The button node or config object to add.
302 * `section`: The `WidgetStdMod` section (header/body/footer) where the
303 button will be added.
305 * `index`: The index at which the button will be in the section.
309 **Note:** The `index` argument will be passed to the Array `splice()`
310 method, therefore a negative value will insert the `button` that many items
311 from the end. The `index` property on the `buttonsChange` event facade is
312 the index at which the `button` was added.
315 @param {Node|Object|String} button The button to add. This can be a `Y.Node`
316 instance, config Object, or String name for a predefined button on the
317 `BUTTONS` prototype property. When a config Object is provided, it will
318 be merged with any defaults provided by any `srcNode` and/or a button
319 with the same `name` defined on the `BUTTONS` property. The following
320 are the possible configuration properties beyond what Node plugins
322 @param {Function|String} [button.action] The default handler that should
323 be called when the button is clicked. A String name of a Function that
324 exists on the `context` object can also be provided. **Note:**
325 Specifying a set of `events` will override this setting.
326 @param {String|String[]} [button.classNames] Additional CSS classes to add
328 @param {Object} [button.context=this] Context which any `events` or
329 `action` should be called with. Defaults to `this`, the widget.
330 **Note:** `e.target` will access the button node in the event handlers.
331 @param {Boolean} [button.disabled=false] Whether the button should be
333 @param {String|Object} [button.events="click"] Event name, or set of
334 events and handlers to bind to the button node. **See:** `Y.Node.on()`,
335 this value is passed as the first argument to `on()`.
336 @param {Boolean} [button.isDefault=false] Whether the button is the
338 @param {String} [button.label] The visible text/value displayed in the
340 @param {String} [button.name] A name which can later be used to reference
341 this button. If a button is defined on the `BUTTONS` property with this
342 same name, its configuration properties will be merged in as defaults.
343 @param {String} [button.section] The `WidgetStdMod` section (header, body,
344 footer) where the button should be added.
345 @param {Node} [button.srcNode] An existing Node to use for the button,
346 default values will be seeded from this node, but are overriden by any
347 values specified in the config object. By default a new <button>
348 node will be created.
349 @param {String} [button.template] A specific template to use when creating
350 a new button node (e.g. "<a />"). **Note:** Specifying a `srcNode`
352 @param {String} [section="footer"] The `WidgetStdMod` section
353 (header/body/footer) where the button should be added. This takes
354 precedence over the `button.section` configuration property.
355 @param {Number} [index] The index at which the button should be inserted. If
356 not specified, the button will be added to the end of the section. This
357 value is passed to the Array `splice()` method, therefore a negative
358 value will insert the `button` that many items from the end.
360 @see Plugin.Button.createNode()
363 addButton: function (button, section, index) {
364 var buttons = this.get('buttons'),
365 sectionButtons, atIndex;
367 // Makes sure we have the full config object.
368 if (!isNode(button)) {
369 button = this._mergeButtonConfig(button);
370 section || (section = button.section);
373 section || (section = this.DEFAULT_BUTTONS_SECTION);
374 sectionButtons = buttons[section] || (buttons[section] = []);
375 isNumber(index) || (index = sectionButtons.length);
377 // Insert new button at the correct position.
378 sectionButtons.splice(index, 0, button);
380 // Determine the index at which the `button` now exists in the array.
381 atIndex = YArray.indexOf(sectionButtons, button);
383 this.set('buttons', buttons, {
394 Returns a button node from this widget's `buttons`.
397 @param {Number|String} name The string name or index of the button.
398 @param {String} [section="footer"] The `WidgetStdMod` section
399 (header/body/footer) where the button exists. Only applicable when
400 looking for a button by numerical index, or by name but scoped to a
402 @return {Node} The button node.
405 getButton: function (name, section) {
406 if (!isValue(name)) { return; }
408 var map = this._buttonsMap,
411 section || (section = this.DEFAULT_BUTTONS_SECTION);
413 // Supports `getButton(1, 'header')` signature.
414 if (isNumber(name)) {
415 buttons = this.get('buttons');
416 return buttons[section] && buttons[section][name];
419 // Looks up button by name or section:name.
420 return arguments.length > 1 ? map[section + ':' + name] : map[name];
424 Removes a button from this widget.
426 The button will be removed from this widget's `buttons` and its DOM. Any
427 event subscriptions on the button which were created by this widget will be
428 detached. If the content section becomes empty after removing the button
429 node, then the section will also be removed.
431 This fires the `buttonsChange` event and adds the following properties to
434 * `button`: The button node to remove.
436 * `section`: The `WidgetStdMod` section (header/body/footer) where the
437 button should be removed from.
439 * `index`: The index at which the button exists in the section.
444 @param {Node|Number|String} button The button to remove. This can be a
445 `Y.Node` instance, index, or String name of a button.
446 @param {String} [section="footer"] The `WidgetStdMod` section
447 (header/body/footer) where the button exists. Only applicable when
448 removing a button by numerical index, or by name but scoped to a
453 removeButton: function (button, section) {
454 if (!isValue(button)) { return this; }
456 var buttons = this.get('buttons'),
459 // Shortcut if `button` is already an index which is needed for slicing.
460 if (isNumber(button)) {
461 section || (section = this.DEFAULT_BUTTONS_SECTION);
463 button = buttons[section][index];
465 // Supports `button` being the string name.
466 if (isString(button)) {
467 // `getButton()` is called this way because its behavior is
468 // different based on the number of arguments.
469 button = this.getButton.apply(this, arguments);
472 // Determines the `section` and `index` at which the button exists.
473 YObject.some(buttons, function (sectionButtons, currentSection) {
474 index = YArray.indexOf(sectionButtons, button);
477 section = currentSection;
483 // Button was found at an appropriate index.
484 if (button && index > -1) {
485 // Remove button from `section` array.
486 buttons[section].splice(index, 1);
488 this.set('buttons', buttons, {
499 // -- Protected Methods ----------------------------------------------------
502 Binds UI event listeners. This method is inserted via AOP, and will execute
505 @method _bindUIButtons
509 _bindUIButtons: function () {
510 // Event handlers are bound with `bind()` to make them more extensible.
512 var afterContentChange = Y.bind('_afterContentChangeButtons', this);
515 defaultButtonChange: Y.bind('_afterDefaultButtonChange', this),
516 visibleChange : Y.bind('_afterVisibleChangeButtons', this),
517 headerContentChange: afterContentChange,
518 bodyContentChange : afterContentChange,
519 footerContentChange: afterContentChange
524 Returns a button node based on the specified `button` node or configuration.
526 The button node will either be created via `Y.Plugin.Button.createNode()`,
527 or when `button` is specified as a node already, it will by `plug()`ed with
530 @method _createButton
531 @param {Node|Object} button Button node or configuration object.
532 @return {Node} The button node.
536 _createButton: function (button) {
537 var config, buttonConfig, nonButtonNodeCfg,
538 i, len, action, context, handle;
540 // Makes sure the exiting `Y.Node` instance is from this YUI sandbox and
541 // is plugged with `Y.Plugin.Button`.
542 if (isNode(button)) {
543 return Y.one(button.getDOMNode()).plug(ButtonPlugin);
546 // Merge `button` config with defaults and back-compat.
553 buttonConfig = Y.merge(config);
554 nonButtonNodeCfg = WidgetButtons.NON_BUTTON_NODE_CFG;
556 // Remove all non-button Node config props.
557 for (i = 0, len = nonButtonNodeCfg.length; i < len; i += 1) {
558 delete buttonConfig[nonButtonNodeCfg[i]];
561 // Create the button node using the button Node-only config.
562 button = ButtonPlugin.createNode(buttonConfig);
564 context = config.context;
565 action = config.action;
567 // Supports `action` as a String name of a Function on the `context`
569 if (isString(action)) {
570 action = Y.bind(action, context);
573 // Supports all types of crazy configs for event subscriptions and
574 // stores a reference to the returned `EventHandle`.
575 handle = button.on(config.events, action, context);
576 this._buttonsHandles[Y.stamp(button, true)] = handle;
578 // Tags the button with the configured `name` and `isDefault` settings.
579 button.setData('name', this._getButtonName(config));
580 button.setData('default', this._getButtonDefault(config));
582 // Add any CSS classnames to the button node.
583 YArray.each(YArray(config.classNames), button.addClass, button);
589 Returns the buttons container for the specified `section`, passing a truthy
590 value for `create` will create the node if it does not already exist.
592 **Note:** It is up to the caller to properly insert the returned container
593 node into the content section.
595 @method _getButtonContainer
596 @param {String} section The `WidgetStdMod` section (header/body/footer).
597 @param {Boolean} create Whether the buttons container should be created if
598 it does not already exist.
599 @return {Node} The buttons container node for the specified `section`.
601 @see BUTTONS_TEMPLATE
604 _getButtonContainer: function (section, create) {
605 var sectionClassName = WidgetStdMod.SECTION_CLASS_NAMES[section],
606 buttonsClassName = WidgetButtons.CLASS_NAMES.buttons,
607 contentBox = this.get('contentBox'),
608 containerSelector, container;
610 // Search for an existing buttons container within the section.
611 containerSelector = '.' + sectionClassName + ' .' + buttonsClassName;
612 container = contentBox.one(containerSelector);
614 // Create the `container` if it doesn't already exist.
615 if (!container && create) {
616 container = Y.Node.create(this.BUTTONS_TEMPLATE);
617 container.addClass(buttonsClassName);
624 Returns whether or not the specified `button` is configured to be the
627 When a button node is specified, the button's `getData()` method will be
628 used to determine if the button is configured to be the default. When a
629 button config object is specified, the `isDefault` prop will determine
630 whether the button is the default.
632 **Note:** `<button data-default="true"></button>` is supported via the
633 `button.getData('default')` API call.
635 @method _getButtonDefault
636 @param {Node|Object} button The button node or configuration object.
637 @return {Boolean} Whether the button is configured to be the default button.
641 _getButtonDefault: function (button) {
642 var isDefault = isNode(button) ?
643 button.getData('default') : button.isDefault;
645 if (isString(isDefault)) {
646 return isDefault.toLowerCase() === 'true';
653 Returns the name of the specified `button`.
655 When a button node is specified, the button's `getData('name')` method is
656 preferred, but will fallback to `get('name')`, and the result will determine
657 the button's name. When a button config object is specified, the `name` prop
658 will determine the button's name.
660 **Note:** `<button data-name="foo"></button>` is supported via the
661 `button.getData('name')` API call.
663 @method _getButtonName
664 @param {Node|Object} button The button node or configuration object.
665 @return {String} The name of the button.
669 _getButtonName: function (button) {
672 if (isNode(button)) {
673 name = button.getData('name') || button.get('name');
675 name = button && (button.name || button.type);
682 Getter for the `buttons` attribute. A copy of the `buttons` object is
683 returned so the stored state cannot be modified by the callers of
686 This will recreate a copy of the `buttons` object, and each section array
687 (the button nodes are *not* copied/cloned.)
690 @param {Object} buttons The widget's current `buttons` state.
691 @return {Object} A copy of the widget's current `buttons` state.
695 _getButtons: function (buttons) {
696 var buttonsCopy = {};
698 // Creates a new copy of the `buttons` object.
699 YObject.each(buttons, function (sectionButtons, section) {
700 // Creates of copy of the array of button nodes.
701 buttonsCopy[section] = sectionButtons.concat();
708 Adds the specified `button` to the buttons map (both name -> button and
709 section:name -> button), and sets the button as the default if it is
710 configured as the default button.
712 **Note:** If two or more buttons are configured with the same `name` and/or
713 configured to be the default button, the last one wins.
716 @param {Node} button The button node to map.
717 @param {String} section The `WidgetStdMod` section (header/body/footer).
721 _mapButton: function (button, section) {
722 var map = this._buttonsMap,
723 name = this._getButtonName(button),
724 isDefault = this._getButtonDefault(button);
730 // section:name -> button
731 map[section + ':' + name] = button;
734 isDefault && (this._defaultButton = button);
738 Adds the specified `buttons` to the buttons map (both name -> button and
739 section:name -> button), and set the a button as the default if one is
740 configured as the default button.
742 **Note:** This will clear all previous button mappings and null-out any
743 previous default button! If two or more buttons are configured with the same
744 `name` and/or configured to be the default button, the last one wins.
747 @param {Node[]} buttons The button nodes to map.
751 _mapButtons: function (buttons) {
752 this._buttonsMap = {};
753 this._defaultButton = null;
755 YObject.each(buttons, function (sectionButtons, section) {
758 for (i = 0, len = sectionButtons.length; i < len; i += 1) {
759 this._mapButton(sectionButtons[i], section);
765 Returns a copy of the specified `config` object merged with any defaults
766 provided by a `srcNode` and/or a predefined configuration for a button
767 with the same `name` on the `BUTTONS` property.
769 @method _mergeButtonConfig
770 @param {Object|String} config Button configuration object, or string name.
771 @return {Object} A copy of the button configuration object merged with any
776 _mergeButtonConfig: function (config) {
777 var buttonConfig, defConfig, name, button, tagName, label;
779 // Makes sure `config` is an Object and a copy of the specified value.
780 config = isString(config) ? {name: config} : Y.merge(config);
782 // Seeds default values from the button node, if there is one.
783 if (config.srcNode) {
784 button = config.srcNode;
785 tagName = button.get('tagName').toLowerCase();
786 label = button.get(tagName === 'input' ? 'value' : 'text');
788 // Makes sure the button's current values override any defaults.
790 disabled : !!button.get('disabled'),
791 isDefault: this._getButtonDefault(button),
792 name : this._getButtonName(button)
795 // Label should only be considered when not an empty string.
796 label && (buttonConfig.label = label);
798 // Merge `config` with `buttonConfig` values.
799 Y.mix(config, buttonConfig, false, null, 0, true);
802 name = this._getButtonName(config);
803 defConfig = this.BUTTONS && this.BUTTONS[name];
805 // Merge `config` with predefined default values.
807 Y.mix(config, defConfig, false, null, 0, true);
814 `HTML_PARSER` implementation for the `buttons` attribute.
816 **Note:** To determine a button node's name its `data-name` and `name`
817 attributes are examined. Whether the button should be the default is
818 determined by its `data-default` attribute.
820 @method _parseButtons
821 @param {Node} srcNode This widget's srcNode to search for buttons.
822 @return {null|Object} `buttons` Config object parsed from this widget's DOM.
826 _parseButtons: function (srcNode) {
827 var buttonSelector = '.' + WidgetButtons.CLASS_NAMES.button,
828 sections = ['header', 'body', 'footer'],
829 buttonsConfig = null;
831 YArray.each(sections, function (section) {
832 var container = this._getButtonContainer(section),
833 buttons = container && container.all(buttonSelector),
836 if (!buttons || buttons.isEmpty()) { return; }
840 // Creates a button config object for every button node found and
841 // adds it to the section. This way each button configuration can be
842 // merged with any defaults provided by predefined `BUTTONS`.
843 buttons.each(function (button) {
844 sectionButtons.push({srcNode: button});
847 buttonsConfig || (buttonsConfig = {});
848 buttonsConfig[section] = sectionButtons;
851 return buttonsConfig;
855 Setter for the `buttons` attribute. This processes the specified `config`
856 and returns a new `buttons` object which is stored as the new state; leaving
857 the original, specified `config` unmodified.
859 The button nodes will either be created via `Y.Plugin.Button.createNode()`,
860 or when a button is already a Node already, it will by `plug()`ed with
864 @param {Array|Object} config The `buttons` configuration to process.
865 @return {Object} The processed `buttons` object which represents the new
870 _setButtons: function (config) {
871 var defSection = this.DEFAULT_BUTTONS_SECTION,
874 function processButtons(buttonConfigs, currentSection) {
875 if (!isArray(buttonConfigs)) { return; }
877 var i, len, button, section;
879 for (i = 0, len = buttonConfigs.length; i < len; i += 1) {
880 button = buttonConfigs[i];
881 section = currentSection;
883 if (!isNode(button)) {
884 button = this._mergeButtonConfig(button);
885 section || (section = button.section);
888 // Always passes through `_createButton()` to make sure the node
889 // is decorated as a button.
890 button = this._createButton(button);
892 // Use provided `section` or fallback to the default section.
893 section || (section = defSection);
895 // Add button to the array of buttons for the specified section.
896 (buttons[section] || (buttons[section] = [])).push(button);
900 // Handle `config` being either an Array or Object of Arrays.
901 if (isArray(config)) {
902 processButtons.call(this, config);
904 YObject.each(config, processButtons, this);
911 Syncs this widget's current button-related state to its DOM. This method is
912 inserted via AOP, and will execute after `syncUI()`.
914 @method _syncUIButtons
918 _syncUIButtons: function () {
919 this._uiSetButtons(this.get('buttons'));
920 this._uiSetDefaultButton(this.get('defaultButton'));
921 this._uiSetVisibleButtons(this.get('visible'));
925 Inserts the specified `button` node into this widget's DOM at the specified
926 `section` and `index` and updates the section content.
928 The section and button container nodes will be created if they do not
931 @method _uiInsertButton
932 @param {Node} button The button node to insert into this widget's DOM.
933 @param {String} section The `WidgetStdMod` section (header/body/footer).
934 @param {Number} index Index at which the `button` should be positioned.
938 _uiInsertButton: function (button, section, index) {
939 var buttonsClassName = WidgetButtons.CLASS_NAMES.button,
940 buttonContainer = this._getButtonContainer(section, true),
941 sectionButtons = buttonContainer.all('.' + buttonsClassName);
943 // Inserts the button node at the correct index.
944 buttonContainer.insertBefore(button, sectionButtons.item(index));
946 // Adds the button container to the section content.
947 this.setStdModContent(section, buttonContainer, 'after');
951 Removes the button node from this widget's DOM and detaches any event
952 subscriptions on the button that were created by this widget. The section
953 content will be updated unless `{preserveContent: true}` is passed in the
956 By default the button container node will be removed when this removes the
957 last button of the specified `section`; and if no other content remains in
958 the section node, it will also be removed.
960 @method _uiRemoveButton
961 @param {Node} button The button to remove and destroy.
962 @param {String} section The `WidgetStdMod` section (header/body/footer).
963 @param {Object} [options] Additional options.
964 @param {Boolean} [options.preserveContent=false] Whether the section
965 content should be updated.
969 _uiRemoveButton: function (button, section, options) {
970 var yuid = Y.stamp(button, this),
971 handles = this._buttonsHandles,
972 handle = handles[yuid],
973 buttonContainer, buttonClassName;
975 handle && handle.detach();
976 delete handles[yuid];
980 options || (options = {});
982 // Remove the button container and section nodes if needed.
983 if (!options.preserveContent) {
984 buttonContainer = this._getButtonContainer(section);
985 buttonClassName = WidgetButtons.CLASS_NAMES.button;
987 // Only matters if we have a button container which is empty.
988 if (buttonContainer &&
989 buttonContainer.all('.' + buttonClassName).isEmpty()) {
991 buttonContainer.remove();
992 this._updateContentButtons(section);
998 Sets the current `buttons` state to this widget's DOM by rendering the
999 specified collection of `buttons` and updates the contents of each section
1002 Button nodes which already exist in the DOM will remain intact, or will be
1003 moved if they should be in a new position. Old button nodes which are no
1004 longer represented in the specified `buttons` collection will be removed,
1005 and any event subscriptions on the button which were created by this widget
1008 If the button nodes in this widget's DOM actually change, then each content
1009 section will be updated (or removed) appropriately.
1011 @method _uiSetButtons
1012 @param {Object} buttons The current `buttons` state to visually represent.
1016 _uiSetButtons: function (buttons) {
1017 var buttonClassName = WidgetButtons.CLASS_NAMES.button,
1018 sections = ['header', 'body', 'footer'];
1020 YArray.each(sections, function (section) {
1021 var sectionButtons = buttons[section] || [],
1022 numButtons = sectionButtons.length,
1023 buttonContainer = this._getButtonContainer(section, numButtons),
1024 buttonsUpdated = false,
1025 oldNodes, i, button, buttonIndex;
1027 // When there's no button container, there are no new buttons or old
1028 // buttons that we have to deal with for this section.
1029 if (!buttonContainer) { return; }
1031 oldNodes = buttonContainer.all('.' + buttonClassName);
1033 for (i = 0; i < numButtons; i += 1) {
1034 button = sectionButtons[i];
1035 buttonIndex = oldNodes ? oldNodes.indexOf(button) : -1;
1037 // Buttons already rendered in the Widget should remain there or
1038 // moved to their new index. New buttons will be added to the
1039 // current `buttonContainer`.
1040 if (buttonIndex > -1) {
1041 // Remove button from existing buttons nodeList since its in
1043 oldNodes.splice(buttonIndex, 1);
1045 // Check that the button is at the right position, if not,
1046 // move it to its new position.
1047 if (buttonIndex !== i) {
1048 // Using `i + 1` because the button should be at index
1049 // `i`; it's inserted before the node which comes after.
1050 buttonContainer.insertBefore(button, i + 1);
1051 buttonsUpdated = true;
1054 buttonContainer.appendChild(button);
1055 buttonsUpdated = true;
1059 // Safely removes the old button nodes which are no longer part of
1060 // this widget's `buttons`.
1061 oldNodes.each(function (button) {
1062 this._uiRemoveButton(button, section, {preserveContent: true});
1063 buttonsUpdated = true;
1066 // Remove leftover empty button containers and updated the StdMod
1068 if (numButtons === 0) {
1069 buttonContainer.remove();
1070 this._updateContentButtons(section);
1074 // Adds the button container to the section content.
1075 if (buttonsUpdated) {
1076 this.setStdModContent(section, buttonContainer, 'after');
1082 Adds the "yui3-button-primary" CSS class to the new `defaultButton` and
1083 removes it from the old default button.
1085 @method _uiSetDefaultButton
1086 @param {Node} newButton The new `defaultButton`.
1087 @param {Node} oldButton The old `defaultButton`.
1091 _uiSetDefaultButton: function (newButton, oldButton) {
1092 var primaryClassName = WidgetButtons.CLASS_NAMES.primary;
1094 newButton && newButton.addClass(primaryClassName);
1095 oldButton && oldButton.removeClass(primaryClassName);
1099 Focuses this widget's `defaultButton` if there is one and this widget is
1102 @method _uiSetVisibleButtons
1103 @param {Boolean} visible Whether this widget is visible.
1107 _uiSetVisibleButtons: function (visible) {
1108 if (!visible) { return; }
1110 var defaultButton = this.get('defaultButton');
1111 if (defaultButton) {
1112 defaultButton.focus();
1117 Removes the specified `button` from the buttons map (both name -> button and
1118 section:name -> button), and nulls-out the `defaultButton` if it is
1119 currently the default button.
1121 @method _unMapButton
1122 @param {Node} button The button node to remove from the buttons map.
1123 @param {String} section The `WidgetStdMod` section (header/body/footer).
1127 _unMapButton: function (button, section) {
1128 var map = this._buttonsMap,
1129 name = this._getButtonName(button),
1132 // Only delete the map entry if the specified `button` is mapped to it.
1135 if (map[name] === button) {
1139 // section:name -> button
1140 sectionName = section + ':' + name;
1141 if (map[sectionName] === button) {
1142 delete map[sectionName];
1146 // Clear the default button if its the specified `button`.
1147 if (this._defaultButton === button) {
1148 this._defaultButton = null;
1153 Updates the `defaultButton` attribute if it needs to be updated by comparing
1154 its current value with the protected `_defaultButton` property.
1156 @method _updateDefaultButton
1160 _updateDefaultButton: function () {
1161 var defaultButton = this._defaultButton;
1163 if (this.get('defaultButton') !== defaultButton) {
1164 this._set('defaultButton', defaultButton);
1169 Updates the content attribute which corresponds to the specified `section`.
1171 The method updates the section's content to its current `childNodes`
1172 (text and/or HTMLElement), or will null-out its contents if the section is
1173 empty. It also specifies a `src` of `buttons` on the change event facade.
1175 @method _updateContentButtons
1176 @param {String} section The `WidgetStdMod` section (header/body/footer) to
1181 _updateContentButtons: function (section) {
1182 // `childNodes` return text nodes and HTMLElements.
1183 var sectionContent = this.getStdModNode(section).get('childNodes');
1185 // Updates the section to its current contents, or null if it is empty.
1186 this.set(section + 'Content', sectionContent.isEmpty() ? null :
1187 sectionContent, {src: 'buttons'});
1190 // -- Protected Event Handlers ---------------------------------------------
1193 Handles this widget's `buttonsChange` event which fires anytime the
1194 `buttons` attribute is modified.
1196 **Note:** This method special-cases the `buttons` modifications caused by
1197 `addButton()` and `removeButton()`, both of which set the `src` property on
1198 the event facade to "add" and "remove" respectively.
1200 @method _afterButtonsChange
1201 @param {EventFacade} e
1205 _afterButtonsChange: function (e) {
1206 var buttons = e.newVal,
1207 section = e.section,
1212 // Special cases `addButton()` to only set and insert the new button.
1213 if (src === 'add') {
1214 // Make sure we have the button node.
1215 button = buttons[section][index];
1217 this._mapButton(button, section);
1218 this._updateDefaultButton();
1219 this._uiInsertButton(button, section, index);
1224 // Special cases `removeButton()` to only remove the specified button.
1225 if (src === 'remove') {
1226 // Button node already exists on the event facade.
1229 this._unMapButton(button, section);
1230 this._updateDefaultButton();
1231 this._uiRemoveButton(button, section);
1236 this._mapButtons(buttons);
1237 this._updateDefaultButton();
1238 this._uiSetButtons(buttons);
1242 Handles this widget's `headerContentChange`, `bodyContentChange`,
1243 `footerContentChange` events by making sure the `buttons` remain rendered
1244 after changes to the content areas.
1246 These events are very chatty, so extra caution is taken to avoid doing extra
1247 work or getting into an infinite loop.
1249 @method _afterContentChangeButtons
1250 @param {EventFacade} e
1254 _afterContentChangeButtons: function (e) {
1256 pos = e.stdModPosition,
1257 replace = !pos || pos === WidgetStdMod.REPLACE;
1259 // Only do work when absolutely necessary.
1260 if (replace && src !== 'buttons' && src !== Widget.UI_SRC) {
1261 this._uiSetButtons(this.get('buttons'));
1266 Handles this widget's `defaultButtonChange` event by adding the
1267 "yui3-button-primary" CSS class to the new `defaultButton` and removing it
1268 from the old default button.
1270 @method _afterDefaultButtonChange
1271 @param {EventFacade} e
1275 _afterDefaultButtonChange: function (e) {
1276 this._uiSetDefaultButton(e.newVal, e.prevVal);
1280 Handles this widget's `visibleChange` event by focusing the `defaultButton`
1283 @method _afterVisibleChangeButtons
1284 @param {EventFacade} e
1288 _afterVisibleChangeButtons: function (e) {
1289 this._uiSetVisibleButtons(e.newVal);
1293 Y.WidgetButtons = WidgetButtons;
1296 }, '3.5.1' ,{requires:['button-plugin', 'cssbutton', 'widget-stdmod']});