NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / widget-parent / widget-parent-debug.js
blobd93283e25b7f13180e8afedf66b038d38965cd18
1 /*
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/
6 */
8 YUI.add('widget-parent', function (Y, NAME) {
10 /**
11  * Extension enabling a Widget to be a parent of another Widget.
12  *
13  * @module widget-parent
14  */
16 var Lang = Y.Lang,
17     RENDERED = "rendered",
18     BOUNDING_BOX = "boundingBox";
20 /**
21  * Widget extension providing functionality enabling a Widget to be a
22  * parent of another Widget.
23  *
24  * <p>In addition to the set of attributes supported by WidgetParent, the constructor
25  * configuration object can also contain a <code>children</code> which can be used
26  * to add child widgets to the parent during construction. The <code>children</code>
27  * property is an array of either child widget instances or child widget configuration
28  * objects, and is sugar for the <a href="#method_add">add</a> method. See the
29  * <a href="#method_add">add</a> for details on the structure of the child widget
30  * configuration object.
31  * @class WidgetParent
32  * @constructor
33  * @uses ArrayList
34  * @param {Object} config User configuration object.
35  */
36 function Parent(config) {
38     /**
39     * Fires when a Widget is add as a child.  The event object will have a
40     * 'child' property that returns a reference to the child Widget, as well
41     * as an 'index' property that returns a reference to the index specified
42     * when the add() method was called.
43     * <p>
44     * Subscribers to the "on" moment of this event, will be notified
45     * before a child is added.
46     * </p>
47     * <p>
48     * Subscribers to the "after" moment of this event, will be notified
49     * after a child is added.
50     * </p>
51     *
52     * @event addChild
53     * @preventable _defAddChildFn
54     * @param {EventFacade} e The Event Facade
55     */
56     this.publish("addChild", {
57         defaultTargetOnly: true,
58         defaultFn: this._defAddChildFn
59     });
62     /**
63     * Fires when a child Widget is removed.  The event object will have a
64     * 'child' property that returns a reference to the child Widget, as well
65     * as an 'index' property that returns a reference child's ordinal position.
66     * <p>
67     * Subscribers to the "on" moment of this event, will be notified
68     * before a child is removed.
69     * </p>
70     * <p>
71     * Subscribers to the "after" moment of this event, will be notified
72     * after a child is removed.
73     * </p>
74     *
75     * @event removeChild
76     * @preventable _defRemoveChildFn
77     * @param {EventFacade} e The Event Facade
78     */
79     this.publish("removeChild", {
80         defaultTargetOnly: true,
81         defaultFn: this._defRemoveChildFn
82     });
84     this._items = [];
86     var children,
87         handle;
89     if (config && config.children) {
91         children = config.children;
93         handle = this.after("initializedChange", function (e) {
94             this._add(children);
95             handle.detach();
96         });
98     }
100     //  Widget method overlap
101     Y.after(this._renderChildren, this, "renderUI");
102     Y.after(this._bindUIParent, this, "bindUI");
104     this.after("selectionChange", this._afterSelectionChange);
105     this.after("selectedChange", this._afterParentSelectedChange);
106     this.after("activeDescendantChange", this._afterActiveDescendantChange);
108     this._hDestroyChild = this.after("*:destroy", this._afterDestroyChild);
109     this.after("*:focusedChange", this._updateActiveDescendant);
113 Parent.ATTRS = {
115     /**
116      * @attribute defaultChildType
117      * @type {String|Object}
118      *
119      * @description String representing the default type of the children
120      * managed by this Widget.  Can also supply default type as a constructor
121      * reference.
122      */
123     defaultChildType: {
124         setter: function (val) {
126             var returnVal = Y.Attribute.INVALID_VALUE,
127                 FnConstructor = Lang.isString(val) ? Y[val] : val;
129             if (Lang.isFunction(FnConstructor)) {
130                 returnVal = FnConstructor;
131             }
133             return returnVal;
134         }
135     },
137     /**
138      * @attribute activeDescendant
139      * @type Widget
140      * @readOnly
141      *
142      * @description Returns the Widget's currently focused descendant Widget.
143      */
144     activeDescendant: {
145         readOnly: true
146     },
148     /**
149      * @attribute multiple
150      * @type Boolean
151      * @default false
152      * @writeOnce
153      *
154      * @description Boolean indicating if multiple children can be selected at
155      * once.  Whether or not multiple selection is enabled is always delegated
156      * to the value of the <code>multiple</code> attribute of the root widget
157      * in the object hierarchy.
158      */
159     multiple: {
160         value: false,
161         validator: Lang.isBoolean,
162         writeOnce: true,
163         getter: function (value) {
164             var root = this.get("root");
165             return (root && root != this) ? root.get("multiple") : value;
166         }
167     },
170     /**
171      * @attribute selection
172      * @type {ArrayList|Widget}
173      * @readOnly
174      *
175      * @description Returns the currently selected child Widget.  If the
176      * <code>mulitple</code> attribte is set to <code>true</code> will
177      * return an Y.ArrayList instance containing the currently selected
178      * children.  If no children are selected, will return null.
179      */
180     selection: {
181         readOnly: true,
182         setter: "_setSelection",
183         getter: function (value) {
184             var selection = Lang.isArray(value) ?
185                     (new Y.ArrayList(value)) : value;
186             return selection;
187         }
188     },
190     selected: {
191         setter: function (value) {
193             //  Enforces selection behavior on for parent Widgets.  Parent's
194             //  selected attribute can be set to the following:
195             //  0 - Not selected
196             //  1 - Fully selected (all children are selected).  In order for
197             //  all children to be selected, multiple selection must be
198             //  enabled.  Therefore, you cannot set the "selected" attribute
199             //  on a parent Widget to 1 unless multiple selection is enabled.
200             //  2 - Partially selected, meaning one ore more (but not all)
201             //  children are selected.
203             var returnVal = value;
205             if (value === 1 && !this.get("multiple")) {
206                 Y.log('The selected attribute can only be set to 1 if the "multiple" attribute is set to true.', "error", "widget");
207                 returnVal = Y.Attribute.INVALID_VALUE;
208             }
210             return returnVal;
211         }
212     }
216 Parent.prototype = {
218     /**
219      * The destructor implementation for Parent widgets. Destroys all children.
220      * @method destructor
221      */
222     destructor: function() {
223         this._destroyChildren();
224     },
226     /**
227      * Destroy event listener for each child Widget, responsible for removing
228      * the destroyed child Widget from the parent's internal array of children
229      * (_items property).
230      *
231      * @method _afterDestroyChild
232      * @protected
233      * @param {EventFacade} event The event facade for the attribute change.
234      */
235     _afterDestroyChild: function (event) {
236         var child = event.target;
238         if (child.get("parent") == this) {
239             child.remove();
240         }
241     },
243     /**
244      * Attribute change listener for the <code>selection</code>
245      * attribute, responsible for setting the value of the
246      * parent's <code>selected</code> attribute.
247      *
248      * @method _afterSelectionChange
249      * @protected
250      * @param {EventFacade} event The event facade for the attribute change.
251      */
252     _afterSelectionChange: function (event) {
254         if (event.target == this && event.src != this) {
256             var selection = event.newVal,
257                 selectedVal = 0;    //  Not selected
260             if (selection) {
262                 selectedVal = 2;    //  Assume partially selected, confirm otherwise
265                 if (Y.instanceOf(selection, Y.ArrayList) &&
266                     (selection.size() === this.size())) {
268                     selectedVal = 1;    //  Fully selected
270                 }
272             }
274             this.set("selected", selectedVal, { src: this });
276         }
277     },
280     /**
281      * Attribute change listener for the <code>activeDescendant</code>
282      * attribute, responsible for setting the value of the
283      * parent's <code>activeDescendant</code> attribute.
284      *
285      * @method _afterActiveDescendantChange
286      * @protected
287      * @param {EventFacade} event The event facade for the attribute change.
288      */
289     _afterActiveDescendantChange: function (event) {
290         var parent = this.get("parent");
292         if (parent) {
293             parent._set("activeDescendant", event.newVal);
294         }
295     },
297     /**
298      * Attribute change listener for the <code>selected</code>
299      * attribute, responsible for syncing the selected state of all children to
300      * match that of their parent Widget.
301      *
302      *
303      * @method _afterParentSelectedChange
304      * @protected
305      * @param {EventFacade} event The event facade for the attribute change.
306      */
307     _afterParentSelectedChange: function (event) {
309         var value = event.newVal;
311         if (this == event.target && event.src != this &&
312             (value === 0 || value === 1)) {
314             this.each(function (child) {
316                 //  Specify the source of this change as the parent so that
317                 //  value of the parent's "selection" attribute isn't
318                 //  recalculated
320                 child.set("selected", value, { src: this });
322             }, this);
324         }
326     },
329     /**
330      * Default setter for <code>selection</code> attribute changes.
331      *
332      * @method _setSelection
333      * @protected
334      * @param child {Widget|Array} Widget or Array of Widget instances.
335      * @return {Widget|Array} Widget or Array of Widget instances.
336      */
337     _setSelection: function (child) {
339         var selection = null,
340             selected;
342         if (this.get("multiple") && !this.isEmpty()) {
344             selected = [];
346             this.each(function (v) {
348                if (v.get("selected") > 0) {
349                    selected.push(v);
350                }
352             });
354             if (selected.length > 0) {
355                 selection = selected;
356             }
358         }
359         else {
361             if (child.get("selected") > 0) {
362                 selection = child;
363             }
365         }
367         return selection;
369     },
372     /**
373      * Attribute change listener for the <code>selected</code>
374      * attribute of child Widgets, responsible for setting the value of the
375      * parent's <code>selection</code> attribute.
376      *
377      * @method _updateSelection
378      * @protected
379      * @param {EventFacade} event The event facade for the attribute change.
380      */
381     _updateSelection: function (event) {
383         var child = event.target,
384             selection;
386         if (child.get("parent") == this) {
388             if (event.src != "_updateSelection") {
390                 selection = this.get("selection");
392                 if (!this.get("multiple") && selection && event.newVal > 0) {
394                     //  Deselect the previously selected child.
395                     //  Set src equal to the current context to prevent
396                     //  unnecessary re-calculation of the selection.
398                     selection.set("selected", 0, { src: "_updateSelection" });
400                 }
402                 this._set("selection", child);
404             }
406             if (event.src == this) {
407                 this._set("selection", child, { src: this });
408             }
410         }
412     },
414     /**
415      * Attribute change listener for the <code>focused</code>
416      * attribute of child Widgets, responsible for setting the value of the
417      * parent's <code>activeDescendant</code> attribute.
418      *
419      * @method _updateActiveDescendant
420      * @protected
421      * @param {EventFacade} event The event facade for the attribute change.
422      */
423     _updateActiveDescendant: function (event) {
424         var activeDescendant = (event.newVal === true) ? event.target : null;
425         this._set("activeDescendant", activeDescendant);
426     },
428     /**
429      * Creates an instance of a child Widget using the specified configuration.
430      * By default Widget instances will be created of the type specified
431      * by the <code>defaultChildType</code> attribute.  Types can be explicitly
432      * defined via the <code>childType</code> property of the configuration object
433      * literal. The use of the <code>type</code> property has been deprecated, but
434      * will still be used as a fallback, if <code>childType</code> is not defined,
435      * for backwards compatibility.
436      *
437      * @method _createChild
438      * @protected
439      * @param config {Object} Object literal representing the configuration
440      * used to create an instance of a Widget.
441      */
442     _createChild: function (config) {
444         var defaultType = this.get("defaultChildType"),
445             altType = config.childType || config.type,
446             child,
447             Fn,
448             FnConstructor;
450         if (altType) {
451             Fn = Lang.isString(altType) ? Y[altType] : altType;
452         }
454         if (Lang.isFunction(Fn)) {
455             FnConstructor = Fn;
456         } else if (defaultType) {
457             // defaultType is normalized to a function in it's setter
458             FnConstructor = defaultType;
459         }
461         if (FnConstructor) {
462             child = new FnConstructor(config);
463         } else {
464             Y.error("Could not create a child instance because its constructor is either undefined or invalid.");
465         }
467         return child;
469     },
471     /**
472      * Default addChild handler
473      *
474      * @method _defAddChildFn
475      * @protected
476      * @param event {EventFacade} The Event object
477      * @param child {Widget} The Widget instance, or configuration
478      * object for the Widget to be added as a child.
479      * @param index {Number} Number representing the position at
480      * which the child will be inserted.
481      */
482     _defAddChildFn: function (event) {
484         var child = event.child,
485             index = event.index,
486             children = this._items;
488         if (child.get("parent")) {
489             child.remove();
490         }
492         if (Lang.isNumber(index)) {
493             children.splice(index, 0, child);
494         }
495         else {
496             children.push(child);
497         }
499         child._set("parent", this);
500         child.addTarget(this);
502         // Update index in case it got normalized after addition
503         // (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).
504         event.index = child.get("index");
506         //  TO DO: Remove in favor of using event bubbling
507         child.after("selectedChange", Y.bind(this._updateSelection, this));
508     },
511     /**
512      * Default removeChild handler
513      *
514      * @method _defRemoveChildFn
515      * @protected
516      * @param event {EventFacade} The Event object
517      * @param child {Widget} The Widget instance to be removed.
518      * @param index {Number} Number representing the index of the Widget to
519      * be removed.
520      */
521     _defRemoveChildFn: function (event) {
523         var child = event.child,
524             index = event.index,
525             children = this._items;
527         if (child.get("focused")) {
528             child.blur(); // focused is readOnly, so use the public i/f to unset it
529         }
531         if (child.get("selected")) {
532             child.set("selected", 0);
533         }
535         children.splice(index, 1);
537         child.removeTarget(this);
538         child._oldParent = child.get("parent");
539         child._set("parent", null);
540     },
542     /**
543     * @method _add
544     * @protected
545     * @param child {Widget|Object} The Widget instance, or configuration
546     * object for the Widget to be added as a child.
547     * @param child {Array} Array of Widget instances, or configuration
548     * objects for the Widgets to be added as a children.
549     * @param index {Number} (Optional.)  Number representing the position at
550     * which the child should be inserted.
551     * @description Adds a Widget as a child.  If the specified Widget already
552     * has a parent it will be removed from its current parent before
553     * being added as a child.
554     * @return {Widget|Array} Successfully added Widget or Array containing the
555     * successfully added Widget instance(s). If no children where added, will
556     * will return undefined.
557     */
558     _add: function (child, index) {
560         var children,
561             oChild,
562             returnVal;
565         if (Lang.isArray(child)) {
567             children = [];
569             Y.each(child, function (v, k) {
571                 oChild = this._add(v, (index + k));
573                 if (oChild) {
574                     children.push(oChild);
575                 }
577             }, this);
580             if (children.length > 0) {
581                 returnVal = children;
582             }
584         }
585         else {
587             if (Y.instanceOf(child, Y.Widget)) {
588                 oChild = child;
589             }
590             else {
591                 oChild = this._createChild(child);
592             }
594             if (oChild && this.fire("addChild", { child: oChild, index: index })) {
595                 returnVal = oChild;
596             }
598         }
600         return returnVal;
602     },
605     /**
606     * @method add
607     * @param child {Widget|Object} The Widget instance, or configuration
608     * object for the Widget to be added as a child. The configuration object
609     * for the child can include a <code>childType</code> property, which is either
610     * a constructor function or a string which names a constructor function on the
611     * Y instance (e.g. "Tab" would refer to Y.Tab) (<code>childType</code> used to be
612     * named <code>type</code>, support for which has been deprecated, but is still
613     * maintained for backward compatibility. <code>childType</code> takes precedence
614     * over <code>type</code> if both are defined.
615     * @param child {Array} Array of Widget instances, or configuration
616     * objects for the Widgets to be added as a children.
617     * @param index {Number} (Optional.)  Number representing the position at
618     * which the child should be inserted.
619     * @description Adds a Widget as a child.  If the specified Widget already
620     * has a parent it will be removed from its current parent before
621     * being added as a child.
622     * @return {ArrayList} Y.ArrayList containing the successfully added
623     * Widget instance(s).  If no children where added, will return an empty
624     * Y.ArrayList instance.
625     */
626     add: function () {
628         var added = this._add.apply(this, arguments),
629             children = added ? (Lang.isArray(added) ? added : [added]) : [];
631         return (new Y.ArrayList(children));
633     },
636     /**
637     * @method remove
638     * @param index {Number} (Optional.)  Number representing the index of the
639     * child to be removed.
640     * @description Removes the Widget from its parent.  Optionally, can remove
641     * a child by specifying its index.
642     * @return {Widget} Widget instance that was successfully removed, otherwise
643     * undefined.
644     */
645     remove: function (index) {
647         var child = this._items[index],
648             returnVal;
650         if (child && this.fire("removeChild", { child: child, index: index })) {
651             returnVal = child;
652         }
654         return returnVal;
656     },
659     /**
660     * @method removeAll
661     * @description Removes all of the children from the Widget.
662     * @return {ArrayList} Y.ArrayList instance containing Widgets that were
663     * successfully removed.  If no children where removed, will return an empty
664     * Y.ArrayList instance.
665     */
666     removeAll: function () {
668         var removed = [],
669             child;
671         Y.each(this._items.concat(), function () {
673             child = this.remove(0);
675             if (child) {
676                 removed.push(child);
677             }
679         }, this);
681         return (new Y.ArrayList(removed));
683     },
685     /**
686      * Selects the child at the given index (zero-based).
687      *
688      * @method selectChild
689      * @param {Number} i the index of the child to be selected
690      */
691     selectChild: function(i) {
692         this.item(i).set('selected', 1);
693     },
695     /**
696      * Selects all children.
697      *
698      * @method selectAll
699      */
700     selectAll: function () {
701         this.set("selected", 1);
702     },
704     /**
705      * Deselects all children.
706      *
707      * @method deselectAll
708      */
709     deselectAll: function () {
710         this.set("selected", 0);
711     },
713     /**
714      * Updates the UI in response to a child being added.
715      *
716      * @method _uiAddChild
717      * @protected
718      * @param child {Widget} The child Widget instance to render.
719      * @param parentNode {Object} The Node under which the
720      * child Widget is to be rendered.
721      */
722     _uiAddChild: function (child, parentNode) {
724         child.render(parentNode);
726         // TODO: Ideally this should be in Child's render UI.
728         var childBB = child.get("boundingBox"),
729             siblingBB,
730             nextSibling = child.next(false),
731             prevSibling;
733         // Insert or Append to last child.
735         // Avoiding index, and using the current sibling
736         // state (which should be accurate), means we don't have
737         // to worry about decorator elements which may be added
738         // to the _childContainer node.
740         if (nextSibling && nextSibling.get(RENDERED)) {
742             siblingBB = nextSibling.get(BOUNDING_BOX);
743             siblingBB.insert(childBB, "before");
745         } else {
747             prevSibling = child.previous(false);
749             if (prevSibling && prevSibling.get(RENDERED)) {
751                 siblingBB = prevSibling.get(BOUNDING_BOX);
752                 siblingBB.insert(childBB, "after");
754             } else if (!parentNode.contains(childBB)) {
756                 // Based on pull request from andreas-karlsson
757                 // https://github.com/yui/yui3/pull/25#issuecomment-2103536
759                 // Account for case where a child was rendered independently of the
760                 // parent-child framework, to a node outside of the parentNode,
761                 // and there are no siblings.
763                 parentNode.appendChild(childBB);
764             }
765         }
767     },
769     /**
770      * Updates the UI in response to a child being removed.
771      *
772      * @method _uiRemoveChild
773      * @protected
774      * @param child {Widget} The child Widget instance to render.
775      */
776     _uiRemoveChild: function (child) {
777         child.get("boundingBox").remove();
778     },
780     _afterAddChild: function (event) {
781         var child = event.child;
783         if (child.get("parent") == this) {
784             this._uiAddChild(child, this._childrenContainer);
785         }
786     },
788     _afterRemoveChild: function (event) {
789         var child = event.child;
791         if (child._oldParent == this) {
792             this._uiRemoveChild(child);
793         }
794     },
796     /**
797      * Sets up DOM and CustomEvent listeners for the parent widget.
798      * <p>
799      * This method in invoked after bindUI is invoked for the Widget class
800      * using YUI's aop infrastructure.
801      * </p>
802      *
803      * @method _bindUIParent
804      * @protected
805      */
806     _bindUIParent: function () {
807         this.after("addChild", this._afterAddChild);
808         this.after("removeChild", this._afterRemoveChild);
809     },
811     /**
812      * Renders all child Widgets for the parent.
813      * <p>
814      * This method in invoked after renderUI is invoked for the Widget class
815      * using YUI's aop infrastructure.
816      * </p>
817      * @method _renderChildren
818      * @protected
819      */
820     _renderChildren: function () {
822         /**
823          * <p>By default WidgetParent will render it's children to the parent's content box.</p>
824          *
825          * <p>If the children need to be rendered somewhere else, the _childrenContainer property
826          * can be set to the Node which the children should be rendered to. This property should be
827          * set before the _renderChildren method is invoked, ideally in your renderUI method,
828          * as soon as you create the element to be rendered to.</p>
829          *
830          * @protected
831          * @property _childrenContainer
832          * @value The content box
833          * @type Node
834          */
835         var renderTo = this._childrenContainer || this.get("contentBox");
837         this._childrenContainer = renderTo;
839         this.each(function (child) {
840             child.render(renderTo);
841         });
842     },
844     /**
845      * Destroys all child Widgets for the parent.
846      * <p>
847      * This method is invoked before the destructor is invoked for the Widget
848      * class using YUI's aop infrastructure.
849      * </p>
850      * @method _destroyChildren
851      * @protected
852      */
853     _destroyChildren: function () {
855         //  Detach the handler responsible for removing children in
856         //  response to destroying them since:
857         //  1)  It is unnecessary/inefficient at this point since we are doing
858         //      a batch destroy of all children.
859         //  2)  Removing each child will affect our ability to iterate the
860         //      children since the size of _items will be changing as we
861         //      iterate.
862         this._hDestroyChild.detach();
864         //  Need to clone the _items array since
865         this.each(function (child) {
866             child.destroy();
867         });
868     }
872 Y.augment(Parent, Y.ArrayList);
874 Y.WidgetParent = Parent;
877 }, '3.13.0', {"requires": ["arraylist", "base-build", "widget"]});