MDL-32843 import YUI 3.5.1
[moodle.git] / lib / yui / 3.5.1 / build / widget-parent / widget-parent.js
blob61f1f09d88c86ee4c9f285df47f59caa528e9503
1 /*
2 YUI 3.5.1 (build 22)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('widget-parent', function(Y) {
9 /**
10  * Extension enabling a Widget to be a parent of another Widget.
11  *
12  * @module widget-parent
13  */
15 var Lang = Y.Lang,
16     RENDERED = "rendered",
17     BOUNDING_BOX = "boundingBox";
19 /**
20  * Widget extension providing functionality enabling a Widget to be a 
21  * parent of another Widget.
22  *
23  * <p>In addition to the set of attributes supported by WidgetParent, the constructor
24  * configuration object can also contain a <code>children</code> which can be used
25  * to add child widgets to the parent during construction. The <code>children</code>
26  * property is an array of either child widget instances or child widget configuration 
27  * objects, and is sugar for the <a href="#method_add">add</a> method. See the 
28  * <a href="#method_add">add</a> for details on the structure of the child widget 
29  * configuration object.
30  * @class WidgetParent
31  * @constructor
32  * @uses ArrayList
33  * @param {Object} config User configuration object.
34  */
35 function Parent(config) {
37     /**
38     * Fires when a Widget is add as a child.  The event object will have a 
39     * 'child' property that returns a reference to the child Widget, as well 
40     * as an 'index' property that returns a reference to the index specified 
41     * when the add() method was called.
42     * <p>
43     * Subscribers to the "on" moment of this event, will be notified 
44     * before a child is added.
45     * </p>
46     * <p>
47     * Subscribers to the "after" moment of this event, will be notified
48     * after a child is added.
49     * </p>
50     *
51     * @event addChild
52     * @preventable _defAddChildFn
53     * @param {EventFacade} e The Event Facade
54     */
55     this.publish("addChild", { 
56         defaultTargetOnly: true,
57         defaultFn: this._defAddChildFn 
58     });
61     /**
62     * Fires when a child Widget is removed.  The event object will have a 
63     * 'child' property that returns a reference to the child Widget, as well 
64     * as an 'index' property that returns a reference child's ordinal position.
65     * <p>
66     * Subscribers to the "on" moment of this event, will be notified 
67     * before a child is removed.
68     * </p>
69     * <p>
70     * Subscribers to the "after" moment of this event, will be notified
71     * after a child is removed.
72     * </p>
73     *
74     * @event removeChild
75     * @preventable _defRemoveChildFn
76     * @param {EventFacade} e The Event Facade
77     */
78     this.publish("removeChild", { 
79         defaultTargetOnly: true,
80         defaultFn: this._defRemoveChildFn 
81     });
83     this._items = [];
85     var children,
86         handle;
88     if (config && config.children) {
90         children = config.children;
91         
92         handle = this.after("initializedChange", function (e) {
93             this._add(children);
94             handle.detach();
95         });
97     }
99     //  Widget method overlap
100     Y.after(this._renderChildren, this, "renderUI");
101     Y.after(this._bindUIParent, this, "bindUI");
103     this.after("selectionChange", this._afterSelectionChange);
104     this.after("selectedChange", this._afterParentSelectedChange);
105     this.after("activeDescendantChange", this._afterActiveDescendantChange);
107     this._hDestroyChild = this.after("*:destroy", this._afterDestroyChild);
108     this.after("*:focusedChange", this._updateActiveDescendant);
112 Parent.ATTRS = {
114     /**
115      * @attribute defaultChildType
116      * @type {String|Object}
117      *
118      * @description String representing the default type of the children 
119      * managed by this Widget.  Can also supply default type as a constructor
120      * reference.
121      */
122     defaultChildType: {
123         setter: function (val) {
124             
125             var returnVal = Y.Attribute.INVALID_VALUE,
126                 FnConstructor = Lang.isString(val) ? Y[val] : val;
127             
128             if (Lang.isFunction(FnConstructor)) {
129                 returnVal = FnConstructor;
130             }
131             
132             return returnVal;
133         }
134     },
136     /**
137      * @attribute activeDescendant
138      * @type Widget
139      * @readOnly
140      *
141      * @description Returns the Widget's currently focused descendant Widget.
142      */
143     activeDescendant: {    
144         readOnly: true
145     },
147     /**
148      * @attribute multiple
149      * @type Boolean
150      * @default false
151      * @writeOnce 
152      *
153      * @description Boolean indicating if multiple children can be selected at 
154      * once.  Whether or not multiple selection is enabled is always delegated
155      * to the value of the <code>multiple</code> attribute of the root widget
156      * in the object hierarchy.
157      */
158     multiple: {
159         value: false,
160         validator: Lang.isBoolean,
161         writeOnce: true,
162         getter: function (value) {
163             var root = this.get("root");
164             return (root && root != this) ? root.get("multiple") : value;
165         }
166     },
169     /**
170      * @attribute selection
171      * @type {ArrayList|Widget}
172      * @readOnly  
173      *
174      * @description Returns the currently selected child Widget.  If the 
175      * <code>mulitple</code> attribte is set to <code>true</code> will 
176      * return an Y.ArrayList instance containing the currently selected 
177      * children.  If no children are selected, will return null.
178      */
179     selection: {
180         readOnly: true,
181         setter: "_setSelection",
182         getter: function (value) {
183             var selection = Lang.isArray(value) ? 
184                     (new Y.ArrayList(value)) : value;
185             return selection;
186         }
187     },
189     selected: {
190         setter: function (value) {
192             //  Enforces selection behavior on for parent Widgets.  Parent's 
193             //  selected attribute can be set to the following:
194             //  0 - Not selected
195             //  1 - Fully selected (all children are selected).  In order for 
196             //  all children to be selected, multiple selection must be 
197             //  enabled.  Therefore, you cannot set the "selected" attribute 
198             //  on a parent Widget to 1 unless multiple selection is enabled.
199             //  2 - Partially selected, meaning one ore more (but not all) 
200             //  children are selected.
202             var returnVal = value;
204             if (value === 1 && !this.get("multiple")) {
205                 returnVal = Y.Attribute.INVALID_VALUE;
206             }
207             
208             return returnVal;
209         }
210     }
214 Parent.prototype = {
216     /**
217      * The destructor implementation for Parent widgets. Destroys all children.
218      * @method destructor
219      */
220     destructor: function() {
221         this._destroyChildren();
222     },
224     /**
225      * Destroy event listener for each child Widget, responsible for removing 
226      * the destroyed child Widget from the parent's internal array of children
227      * (_items property).
228      *
229      * @method _afterDestroyChild
230      * @protected
231      * @param {EventFacade} event The event facade for the attribute change.
232      */
233     _afterDestroyChild: function (event) {
234         var child = event.target;
236         if (child.get("parent") == this) {
237             child.remove();
238         }        
239     },
241     /**
242      * Attribute change listener for the <code>selection</code> 
243      * attribute, responsible for setting the value of the 
244      * parent's <code>selected</code> attribute.
245      *
246      * @method _afterSelectionChange
247      * @protected
248      * @param {EventFacade} event The event facade for the attribute change.
249      */
250     _afterSelectionChange: function (event) {
252         if (event.target == this && event.src != this) {
254             var selection = event.newVal,
255                 selectedVal = 0;    //  Not selected
258             if (selection) {
260                 selectedVal = 2;    //  Assume partially selected, confirm otherwise
263                 if (Y.instanceOf(selection, Y.ArrayList) && 
264                     (selection.size() === this.size())) {
266                     selectedVal = 1;    //  Fully selected
268                 }
269                 
270             }
272             this.set("selected", selectedVal, { src: this });
273         
274         }
275     },
278     /**
279      * Attribute change listener for the <code>activeDescendant</code> 
280      * attribute, responsible for setting the value of the 
281      * parent's <code>activeDescendant</code> attribute.
282      *
283      * @method _afterActiveDescendantChange
284      * @protected
285      * @param {EventFacade} event The event facade for the attribute change.
286      */
287     _afterActiveDescendantChange: function (event) {
288         var parent = this.get("parent");
290         if (parent) {
291             parent._set("activeDescendant", event.newVal);
292         }
293     },
295     /**
296      * Attribute change listener for the <code>selected</code> 
297      * attribute, responsible for syncing the selected state of all children to 
298      * match that of their parent Widget.
299      * 
300      *
301      * @method _afterParentSelectedChange
302      * @protected
303      * @param {EventFacade} event The event facade for the attribute change.
304      */
305     _afterParentSelectedChange: function (event) {
307         var value = event.newVal;
309         if (this == event.target && event.src != this && 
310             (value === 0 || value === 1)) {
312             this.each(function (child) {
314                 //  Specify the source of this change as the parent so that 
315                 //  value of the parent's "selection" attribute isn't 
316                 //  recalculated
318                 child.set("selected", value, { src: this });
320             }, this);
321             
322         }
323         
324     },
327     /**
328      * Default setter for <code>selection</code> attribute changes.
329      *
330      * @method _setSelection
331      * @protected
332      * @param child {Widget|Array} Widget or Array of Widget instances.     
333      * @return {Widget|Array} Widget or Array of Widget instances.
334      */
335     _setSelection: function (child) {
337         var selection = null,
338             selected;
340         if (this.get("multiple") && !this.isEmpty()) {
342             selected = [];
343             
344             this.each(function (v) {
346                if (v.get("selected") > 0) {
347                    selected.push(v);
348                }
350             });
352             if (selected.length > 0) {
353                 selection = selected;
354             }
356         }
357         else {
359             if (child.get("selected") > 0) {
360                 selection = child;
361             }
363         }
364         
365         return selection;
366             
367     },
370     /**
371      * Attribute change listener for the <code>selected</code> 
372      * attribute of child Widgets, responsible for setting the value of the 
373      * parent's <code>selection</code> attribute.
374      *
375      * @method _updateSelection
376      * @protected
377      * @param {EventFacade} event The event facade for the attribute change.
378      */
379     _updateSelection: function (event) {
381         var child = event.target,
382             selection;
384         if (child.get("parent") == this) {
386             if (event.src != "_updateSelection") {
388                 selection = this.get("selection");
390                 if (!this.get("multiple") && selection && event.newVal > 0) {
392                     //  Deselect the previously selected child.
393                     //  Set src equal to the current context to prevent
394                     //  unnecessary re-calculation of the selection.
396                     selection.set("selected", 0, { src: "_updateSelection" });
398                 }
400                 this._set("selection", child);
402             }
404             if (event.src == this) {
405                 this._set("selection", child, { src: this });
406             }
407             
408         }
410     },
412     /**
413      * Attribute change listener for the <code>focused</code> 
414      * attribute of child Widgets, responsible for setting the value of the 
415      * parent's <code>activeDescendant</code> attribute.
416      *
417      * @method _updateActiveDescendant
418      * @protected
419      * @param {EventFacade} event The event facade for the attribute change.
420      */
421     _updateActiveDescendant: function (event) {
422         var activeDescendant = (event.newVal === true) ? event.target : null;
423         this._set("activeDescendant", activeDescendant);
424     },
426     /**
427      * Creates an instance of a child Widget using the specified configuration.
428      * By default Widget instances will be created of the type specified 
429      * by the <code>defaultChildType</code> attribute.  Types can be explicitly
430      * defined via the <code>childType</code> property of the configuration object
431      * literal. The use of the <code>type</code> property has been deprecated, but 
432      * will still be used as a fallback, if <code>childType</code> is not defined,
433      * for backwards compatibility. 
434      *
435      * @method _createChild
436      * @protected
437      * @param config {Object} Object literal representing the configuration 
438      * used to create an instance of a Widget.
439      */
440     _createChild: function (config) {
442         var defaultType = this.get("defaultChildType"),
443             altType = config.childType || config.type,
444             child,
445             Fn,
446             FnConstructor;
448         if (altType) {
449             Fn = Lang.isString(altType) ? Y[altType] : altType;
450         }
452         if (Lang.isFunction(Fn)) {
453             FnConstructor = Fn;
454         } else if (defaultType) {
455             // defaultType is normalized to a function in it's setter 
456             FnConstructor = defaultType;
457         }
459         if (FnConstructor) {
460             child = new FnConstructor(config);
461         } else {
462             Y.error("Could not create a child instance because its constructor is either undefined or invalid.");
463         }
465         return child;
466         
467     },
469     /**
470      * Default addChild handler
471      *
472      * @method _defAddChildFn
473      * @protected
474      * @param event {EventFacade} The Event object
475      * @param child {Widget} The Widget instance, or configuration 
476      * object for the Widget to be added as a child.
477      * @param index {Number} Number representing the position at 
478      * which the child will be inserted.
479      */
480     _defAddChildFn: function (event) {
482         var child = event.child,
483             index = event.index,
484             children = this._items;
486         if (child.get("parent")) {
487             child.remove();
488         }
490         if (Lang.isNumber(index)) {
491             children.splice(index, 0, child);
492         }
493         else {
494             children.push(child);
495         }
497         child._set("parent", this);
498         child.addTarget(this);
500         // Update index in case it got normalized after addition
501         // (e.g. user passed in 10, and there are only 3 items, the actual index would be 3. We don't want to pass 10 around in the event facade).
502         event.index = child.get("index");
504         //  TO DO: Remove in favor of using event bubbling
505         child.after("selectedChange", Y.bind(this._updateSelection, this));
506     },
509     /**
510      * Default removeChild handler
511      *
512      * @method _defRemoveChildFn
513      * @protected
514      * @param event {EventFacade} The Event object
515      * @param child {Widget} The Widget instance to be removed.
516      * @param index {Number} Number representing the index of the Widget to 
517      * be removed.
518      */    
519     _defRemoveChildFn: function (event) {
521         var child = event.child,
522             index = event.index,
523             children = this._items;
525         if (child.get("focused")) {
526             child.blur(); // focused is readOnly, so use the public i/f to unset it
527         }
529         if (child.get("selected")) {
530             child.set("selected", 0);
531         }
533         children.splice(index, 1);
535         child.removeTarget(this);
536         child._oldParent = child.get("parent");
537         child._set("parent", null);
538     },
540     /**
541     * @method _add
542     * @protected
543     * @param child {Widget|Object} The Widget instance, or configuration 
544     * object for the Widget to be added as a child.
545     * @param child {Array} Array of Widget instances, or configuration 
546     * objects for the Widgets to be added as a children.
547     * @param index {Number} (Optional.)  Number representing the position at 
548     * which the child should be inserted.
549     * @description Adds a Widget as a child.  If the specified Widget already
550     * has a parent it will be removed from its current parent before
551     * being added as a child.
552     * @return {Widget|Array} Successfully added Widget or Array containing the 
553     * successfully added Widget instance(s). If no children where added, will 
554     * will return undefined.
555     */
556     _add: function (child, index) {   
558         var children,
559             oChild,
560             returnVal;
563         if (Lang.isArray(child)) {
565             children = [];
567             Y.each(child, function (v, k) {
569                 oChild = this._add(v, (index + k));
571                 if (oChild) {
572                     children.push(oChild);
573                 }
574                 
575             }, this);
576             
578             if (children.length > 0) {
579                 returnVal = children;
580             }
582         }
583         else {
585             if (Y.instanceOf(child, Y.Widget)) {
586                 oChild = child;
587             }
588             else {
589                 oChild = this._createChild(child);
590             }
592             if (oChild && this.fire("addChild", { child: oChild, index: index })) {
593                 returnVal = oChild;
594             }
596         }
598         return returnVal;
600     },
603     /**
604     * @method add
605     * @param child {Widget|Object} The Widget instance, or configuration 
606     * object for the Widget to be added as a child. The configuration object
607     * for the child can include a <code>childType</code> property, which is either
608     * a constructor function or a string which names a constructor function on the 
609     * Y instance (e.g. "Tab" would refer to Y.Tab) (<code>childType</code> used to be 
610     * named <code>type</code>, support for which has been deprecated, but is still
611     * maintained for backward compatibility. <code>childType</code> takes precedence
612     * over <code>type</code> if both are defined.
613     * @param child {Array} Array of Widget instances, or configuration 
614     * objects for the Widgets to be added as a children.
615     * @param index {Number} (Optional.)  Number representing the position at 
616     * which the child should be inserted.
617     * @description Adds a Widget as a child.  If the specified Widget already
618     * has a parent it will be removed from its current parent before
619     * being added as a child.
620     * @return {ArrayList} Y.ArrayList containing the successfully added 
621     * Widget instance(s).  If no children where added, will return an empty 
622     * Y.ArrayList instance.
623     */
624     add: function () {
626         var added = this._add.apply(this, arguments),
627             children = added ? (Lang.isArray(added) ? added : [added]) : [];
629         return (new Y.ArrayList(children));
631     },
634     /**
635     * @method remove
636     * @param index {Number} (Optional.)  Number representing the index of the 
637     * child to be removed.
638     * @description Removes the Widget from its parent.  Optionally, can remove
639     * a child by specifying its index.
640     * @return {Widget} Widget instance that was successfully removed, otherwise
641     * undefined.
642     */
643     remove: function (index) {
645         var child = this._items[index],
646             returnVal;
648         if (child && this.fire("removeChild", { child: child, index: index })) {
649             returnVal = child;
650         }
651         
652         return returnVal;
654     },
657     /**
658     * @method removeAll
659     * @description Removes all of the children from the Widget.
660     * @return {ArrayList} Y.ArrayList instance containing Widgets that were 
661     * successfully removed.  If no children where removed, will return an empty 
662     * Y.ArrayList instance.
663     */
664     removeAll: function () {
666         var removed = [],
667             child;
669         Y.each(this._items.concat(), function () {
671             child = this.remove(0);
673             if (child) {
674                 removed.push(child);
675             }
677         }, this);
679         return (new Y.ArrayList(removed));
681     },
682     
683     /**
684      * Selects the child at the given index (zero-based).
685      *
686      * @method selectChild
687      * @param {Number} i the index of the child to be selected
688      */
689     selectChild: function(i) {
690         this.item(i).set('selected', 1);
691     },
693     /**
694      * Selects all children.
695      *
696      * @method selectAll
697      */
698     selectAll: function () {
699         this.set("selected", 1);
700     },
702     /**
703      * Deselects all children.
704      *
705      * @method deselectAll
706      */
707     deselectAll: function () {
708         this.set("selected", 0);
709     },
711     /**
712      * Updates the UI in response to a child being added.
713      *
714      * @method _uiAddChild
715      * @protected
716      * @param child {Widget} The child Widget instance to render.
717      * @param parentNode {Object} The Node under which the 
718      * child Widget is to be rendered.
719      */    
720     _uiAddChild: function (child, parentNode) {
722         child.render(parentNode);
724         // TODO: Ideally this should be in Child's render UI. 
726         var childBB = child.get("boundingBox"),
727             siblingBB,
728             nextSibling = child.next(false),
729             prevSibling;
731         // Insert or Append to last child.
733         // Avoiding index, and using the current sibling 
734         // state (which should be accurate), means we don't have 
735         // to worry about decorator elements which may be added 
736         // to the _childContainer node.
737     
738         if (nextSibling && nextSibling.get(RENDERED)) {
740             siblingBB = nextSibling.get(BOUNDING_BOX);
741             siblingBB.insert(childBB, "before");
743         } else {
745             prevSibling = child.previous(false);
747             if (prevSibling && prevSibling.get(RENDERED)) {
749                 siblingBB = prevSibling.get(BOUNDING_BOX);
750                 siblingBB.insert(childBB, "after");
752             } else if (!parentNode.contains(childBB)) {
754                 // Based on pull request from andreas-karlsson
755                 // https://github.com/yui/yui3/pull/25#issuecomment-2103536
757                 // Account for case where a child was rendered independently of the 
758                 // parent-child framework, to a node outside of the parentNode,
759                 // and there are no siblings.
761                 parentNode.appendChild(childBB);
762             }
763         }
765     },
767     /**
768      * Updates the UI in response to a child being removed.
769      *
770      * @method _uiRemoveChild
771      * @protected
772      * @param child {Widget} The child Widget instance to render.
773      */        
774     _uiRemoveChild: function (child) {
775         child.get("boundingBox").remove();
776     },
778     _afterAddChild: function (event) {
779         var child = event.child;
781         if (child.get("parent") == this) {
782             this._uiAddChild(child, this._childrenContainer);
783         }
784     },
786     _afterRemoveChild: function (event) {
787         var child = event.child;
789         if (child._oldParent == this) {
790             this._uiRemoveChild(child);
791         }
792     },
794     /**
795      * Sets up DOM and CustomEvent listeners for the parent widget.
796      * <p>
797      * This method in invoked after bindUI is invoked for the Widget class
798      * using YUI's aop infrastructure.
799      * </p>
800      *
801      * @method _bindUIParent
802      * @protected
803      */
804     _bindUIParent: function () {
805         this.after("addChild", this._afterAddChild);
806         this.after("removeChild", this._afterRemoveChild);
807     },
809     /**
810      * Renders all child Widgets for the parent.
811      * <p>
812      * This method in invoked after renderUI is invoked for the Widget class
813      * using YUI's aop infrastructure.
814      * </p>
815      * @method _renderChildren
816      * @protected
817      */
818     _renderChildren: function () {
820         /**
821          * <p>By default WidgetParent will render it's children to the parent's content box.</p>
822          *
823          * <p>If the children need to be rendered somewhere else, the _childrenContainer property
824          * can be set to the Node which the children should be rendered to. This property should be
825          * set before the _renderChildren method is invoked, ideally in your renderUI method, 
826          * as soon as you create the element to be rendered to.</p>
827          *
828          * @protected
829          * @property _childrenContainer
830          * @value The content box
831          * @type Node
832          */
833         var renderTo = this._childrenContainer || this.get("contentBox");
835         this._childrenContainer = renderTo;
837         this.each(function (child) {
838             child.render(renderTo);
839         });
840     },
842     /**
843      * Destroys all child Widgets for the parent.
844      * <p>
845      * This method is invoked before the destructor is invoked for the Widget 
846      * class using YUI's aop infrastructure.
847      * </p>
848      * @method _destroyChildren
849      * @protected
850      */
851     _destroyChildren: function () {
853         //  Detach the handler responsible for removing children in 
854         //  response to destroying them since:
855         //  1)  It is unnecessary/inefficient at this point since we are doing 
856         //      a batch destroy of all children.
857         //  2)  Removing each child will affect our ability to iterate the 
858         //      children since the size of _items will be changing as we 
859         //      iterate.
860         this._hDestroyChild.detach();
862         //  Need to clone the _items array since 
863         this.each(function (child) {
864             child.destroy();
865         });
866     }
867     
870 Y.augment(Parent, Y.ArrayList);
872 Y.WidgetParent = Parent;
875 }, '3.5.1' ,{requires:['base-build', 'arraylist', 'widget']});