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/
8 YUI.add('widget-buttons', function (Y, NAME) {
11 Provides header/body/footer button support for Widgets that use the
12 `WidgetStdMod` extension.
14 @module widget-buttons
22 ButtonPlugin = Y.Plugin.Button,
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;
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.
53 function WidgetButtons() {
54 // Has to be setup before the `initializer()`.
55 this._buttonsHandles = {};
58 WidgetButtons.ATTRS = {
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
70 All specified values will be normalized to this type of structure:
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.
90 // Uses predefined "close" button by string name.
106 click: function (e) {
120 getter: '_getButtons',
121 setter: '_setButtons',
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,
142 @attribute defaultButton
155 CSS classes used by `WidgetButtons`.
157 @property CLASS_NAMES
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);
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
183 WidgetButtons.NON_BUTTON_NODE_CFG = [
184 'action', 'classNames', 'context', 'events', 'isDefault', 'section'
187 WidgetButtons.prototype = {
188 // -- Public Properties ----------------------------------------------------
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.
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
215 BUTTONS_TEMPLATE: '<span />',
218 The default section to render buttons in when no section is specified.
220 @property DEFAULT_BUTTONS_SECTION
222 @default Y.WidgetStdMod.FOOTER
225 DEFAULT_BUTTONS_SECTION: WidgetStdMod.FOOTER,
227 // -- Protected Properties -------------------------------------------------
230 A map of button node `_yuid` -> event-handle for all button nodes which were
231 created by this widget.
233 @property _buttonsHandles
240 A map of this widget's `buttons`, both name -> button and
241 section:name -> button.
243 @property _buttonsMap
250 Internal reference to this widget's default button.
252 @property _defaultButton
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.');
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.
272 buttonsChange : Y.bind('_afterButtonsChange', this),
273 defaultButtonChange: Y.bind('_afterDefaultButtonChange', this)
276 Y.after(this._bindUIButtons, this, 'bindUI');
277 Y.after(this._syncUIButtons, this, 'syncUI');
280 destructor: function () {
281 // Detach all event subscriptions this widget added to its `buttons`.
282 YObject.each(this._buttonsHandles, function (handle) {
286 delete this._buttonsHandles;
287 delete this._buttonsMap;
288 delete this._defaultButton;
291 // -- Public Methods -------------------------------------------------------
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
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.
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.
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
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
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
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
342 @param {String} [button.label] The visible text/value displayed in the
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 <button>
352 node will be created.
353 @param {String} [button.template] A specific template to use when creating
354 a new button node (e.g. "<a />"). **Note:** Specifying a `srcNode`
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.
364 @see Plugin.Button.createNode()
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);
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, {
398 Returns a button node from this widget's `buttons`.
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
406 @return {Node} The button node.
409 getButton: function (name, section) {
410 if (!isValue(name)) { return; }
412 var map = this._buttonsMap,
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];
423 // Looks up button by name or section:name.
424 return arguments.length > 1 ? map[section + ':' + name] : map[name];
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
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.
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
457 removeButton: function (button, section) {
458 if (!isValue(button)) { return this; }
460 var buttons = this.get('buttons'),
463 // Shortcut if `button` is already an index which is needed for slicing.
464 if (isNumber(button)) {
465 section || (section = this.DEFAULT_BUTTONS_SECTION);
467 button = buttons[section][index];
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);
476 // Determines the `section` and `index` at which the button exists.
477 YObject.some(buttons, function (sectionButtons, currentSection) {
478 index = YArray.indexOf(sectionButtons, button);
481 section = currentSection;
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, {
503 // -- Protected Methods ----------------------------------------------------
506 Binds UI event listeners. This method is inserted via AOP, and will execute
509 @method _bindUIButtons
513 _bindUIButtons: function () {
514 // Event handlers are bound with `bind()` to make them more extensible.
515 var afterContentChange = Y.bind('_afterContentChangeButtons', this);
518 visibleChange : Y.bind('_afterVisibleChangeButtons', this),
519 headerContentChange: afterContentChange,
520 bodyContentChange : afterContentChange,
521 footerContentChange: afterContentChange
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
532 @method _createButton
533 @param {Node|Object} button Button node or configuration object.
534 @return {Node} The button node.
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);
548 // Merge `button` config with defaults and back-compat.
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]];
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`
571 if (isString(action)) {
572 action = Y.bind(action, context);
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);
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`.
603 @see BUTTONS_TEMPLATE
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);
626 Returns whether or not the specified `button` is configured to be the
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.
643 _getButtonDefault: function (button) {
644 var isDefault = isNode(button) ?
645 button.getData('default') : button.isDefault;
647 if (isString(isDefault)) {
648 return isDefault.toLowerCase() === 'true';
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.
671 _getButtonName: function (button) {
674 if (isNode(button)) {
675 name = button.getData('name') || button.get('name');
677 name = button && (button.name || button.type);
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
688 This will recreate a copy of the `buttons` object, and each section array
689 (the button nodes are *not* copied/cloned.)
692 @param {Object} buttons The widget's current `buttons` state.
693 @return {Object} A copy of the widget's current `buttons` state.
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();
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.
718 @param {Node} button The button node to map.
719 @param {String} section The `WidgetStdMod` section (header/body/footer).
723 _mapButton: function (button, section) {
724 var map = this._buttonsMap,
725 name = this._getButtonName(button),
726 isDefault = this._getButtonDefault(button);
732 // section:name -> button
733 map[section + ':' + name] = button;
736 isDefault && (this._defaultButton = button);
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.
749 @param {Node[]} buttons The button nodes to map.
753 _mapButtons: function (buttons) {
754 this._buttonsMap = {};
755 this._defaultButton = null;
757 YObject.each(buttons, function (sectionButtons, section) {
760 for (i = 0, len = sectionButtons.length; i < len; i += 1) {
761 this._mapButton(sectionButtons[i], section);
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
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.
792 disabled : !!button.get('disabled'),
793 isDefault: this._getButtonDefault(button),
794 name : this._getButtonName(button)
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);
804 name = this._getButtonName(config);
805 defConfig = this.BUTTONS && this.BUTTONS[name];
807 // Merge `config` with predefined default values.
809 Y.mix(config, defConfig, false, null, 0, true);
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.
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),
838 if (!buttons || buttons.isEmpty()) { return; }
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});
849 buttonsConfig || (buttonsConfig = {});
850 buttonsConfig[section] = sectionButtons;
853 return buttonsConfig;
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
866 @param {Array|Object} config The `buttons` configuration to process.
867 @return {Object} The processed `buttons` object which represents the new
872 _setButtons: function (config) {
873 var defSection = this.DEFAULT_BUTTONS_SECTION,
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);
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);
902 // Handle `config` being either an Array or Object of Arrays.
903 if (isArray(config)) {
904 processButtons.call(this, config);
906 YObject.each(config, processButtons, this);
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
920 _syncUIButtons: function () {
921 this._uiSetButtons(this.get('buttons'));
922 this._uiSetDefaultButton(this.get('defaultButton'));
923 this._uiSetVisibleButtons(this.get('visible'));
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
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.
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');
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
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.
971 _uiRemoveButton: function (button, section, options) {
972 var yuid = Y.stamp(button, this),
973 handles = this._buttonsHandles,
974 handle = handles[yuid],
975 buttonContainer, buttonClassName;
981 delete handles[yuid];
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);
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
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
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.
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
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;
1059 buttonContainer.appendChild(button);
1060 buttonsUpdated = true;
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;
1071 // Remove leftover empty button containers and updated the StdMod
1073 if (numButtons === 0) {
1074 buttonContainer.remove();
1075 this._updateContentButtons(section);
1079 // Adds the button container to the section content.
1080 if (buttonsUpdated) {
1081 this.setStdModContent(section, buttonContainer, 'after');
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`.
1096 _uiSetDefaultButton: function (newButton, oldButton) {
1097 var primaryClassName = WidgetButtons.CLASS_NAMES.primary;
1099 if (newButton) { newButton.addClass(primaryClassName); }
1100 if (oldButton) { oldButton.removeClass(primaryClassName); }
1104 Focuses this widget's `defaultButton` if there is one and this widget is
1107 @method _uiSetVisibleButtons
1108 @param {Boolean} visible Whether this widget is visible.
1112 _uiSetVisibleButtons: function (visible) {
1113 if (!visible) { return; }
1115 var defaultButton = this.get('defaultButton');
1116 if (defaultButton) {
1117 defaultButton.focus();
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).
1132 _unMapButton: function (button, section) {
1133 var map = this._buttonsMap,
1134 name = this._getButtonName(button),
1137 // Only delete the map entry if the specified `button` is mapped to it.
1140 if (map[name] === button) {
1144 // section:name -> button
1145 sectionName = section + ':' + name;
1146 if (map[sectionName] === button) {
1147 delete map[sectionName];
1151 // Clear the default button if its the specified `button`.
1152 if (this._defaultButton === button) {
1153 this._defaultButton = null;
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
1165 _updateDefaultButton: function () {
1166 var defaultButton = this._defaultButton;
1168 if (this.get('defaultButton') !== defaultButton) {
1169 this._set('defaultButton', defaultButton);
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
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'});
1195 // -- Protected Event Handlers ---------------------------------------------
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
1210 _afterButtonsChange: function (e) {
1211 var buttons = e.newVal,
1212 section = e.section,
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);
1229 // Special cases `removeButton()` to only remove the specified button.
1230 if (src === 'remove') {
1231 // Button node already exists on the event facade.
1234 this._unMapButton(button, section);
1235 this._updateDefaultButton();
1236 this._uiRemoveButton(button, section);
1241 this._mapButtons(buttons);
1242 this._updateDefaultButton();
1243 this._uiSetButtons(buttons);
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
1259 _afterContentChangeButtons: function (e) {
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'));
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
1280 _afterDefaultButtonChange: function (e) {
1281 this._uiSetDefaultButton(e.newVal, e.prevVal);
1285 Handles this widget's `visibleChange` event by focusing the `defaultButton`
1288 @method _afterVisibleChangeButtons
1289 @param {EventFacade} e
1293 _afterVisibleChangeButtons: function (e) {
1294 this._uiSetVisibleButtons(e.newVal);
1298 Y.WidgetButtons = WidgetButtons;
1301 }, '3.13.0', {"requires": ["button-plugin", "cssbutton", "widget-stdmod"]});