Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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