1 YUI.add('moodle-core-dock', function (Y, NAME) {
6 * This file contains the DOCK object and all dock related global namespace methods and properties.
8 * @module moodle-core-dock
12 var LOGNS = 'moodle-core-dock',
13 BODY = Y.one(Y.config.doc.body),
15 dock: 'dock', // CSS Class applied to the dock box
16 dockspacer: 'dockspacer', // CSS class applied to the dockspacer
17 controls: 'controls', // CSS class applied to the controls box
18 body: 'has_dock', // CSS class added to the body when there is a dock
19 buttonscontainer: 'buttons_container',
20 dockeditem: 'dockeditem', // CSS class added to each item in the dock
21 dockeditemcontainer: 'dockeditem_container',
22 dockedtitle: 'dockedtitle', // CSS class added to the item's title in each dock
23 activeitem: 'activeitem', // CSS class added to the active item
24 contentonly: 'content-only',
25 dockonload: 'dock_on_load'
28 dockableblock: '.block[data-instanceid][data-dockable]',
29 blockmoveto: '.block[data-instanceid][data-dockable] .moveto',
30 panelmoveto: '#dockeditempanel .commands a.moveto',
31 dockonload: '.block.'+CSS.dockonload,
32 blockregion: '[data-blockregion]'
40 M.core = M.core || {};
41 M.core.dock = M.core.dock || {};
44 * The dock - once initialised.
50 M.core.dock._dock = null;
53 * An associative array of dockable blocks.
54 * @property _dockableblocks
55 * @type {Array} An array of BLOCK objects organised by instanceid.
58 M.core.dock._dockableblocks = {};
61 * Initialises the dock.
62 * This method registers dockable blocks, and creates delegations to dock them.
66 M.core.dock.init = function() {
67 Y.all(SELECTOR.dockableblock).each(M.core.dock.registerDockableBlock);
68 BODY.delegate('click', M.core.dock.dockBlock, SELECTOR.blockmoveto);
69 BODY.delegate('key', M.core.dock.dockBlock, SELECTOR.blockmoveto, 'enter');
73 * Returns an instance of the dock.
74 * Initialises one if one hasn't already being initialised.
80 M.core.dock.get = function() {
81 if (this._dock === null) {
82 this._dock = new DOCK();
88 * Registers a dockable block with the dock.
91 * @method registerDockableBlock
92 * @param {int} id The block instance ID.
95 M.core.dock.registerDockableBlock = function(id) {
96 if (typeof id === 'object' && typeof id.getData === 'function') {
97 id = id.getData('instanceid');
99 M.core.dock._dockableblocks[id] = new BLOCK({id : id});
103 * Docks a block given either its instanceid, its node, or an event fired from within the block.
105 * @method dockBlockByInstanceID
109 M.core.dock.dockBlock = function(id) {
110 if (typeof id === 'object' && id.target !== 'undefined') {
113 if (typeof id === "object") {
114 if (!id.test(SELECTOR.dockableblock)) {
115 id = id.ancestor(SELECTOR.dockableblock);
117 if (typeof id === 'object' && typeof id.getData === 'function' && !id.ancestor('.'+CSS.dock)) {
118 id = id.getData('instanceid');
123 var block = M.core.dock._dockableblocks[id];
130 * Fixes the title orientation. Rotating it if required.
133 * @method fixTitleOrientation
134 * @param {Node} title The title node we are looking at.
135 * @param {String} text The string to use as the title.
136 * @return {Node} The title node to use.
138 M.core.dock.fixTitleOrientation = function(title, text) {
139 var dock = M.core.dock.get(),
141 transform = 'rotate(270deg)',
146 title = Y.one(title);
148 if (dock.get('orientation') !== 'vertical') {
149 // If the dock isn't vertical don't adjust it!
150 title.set('innerHTML', text);
154 if (Y.UA.ie > 0 && Y.UA.ie < 8) {
155 // IE 6/7 can't rotate text so force ver
156 M.str.langconfig.thisdirectionvertical = 'ver';
159 switch (M.str.langconfig.thisdirectionvertical) {
162 return title.set('innerHTML', text.split('').join('<br />'));
164 transform = 'rotate(90deg)';
167 // Nothing to do here. transform default is good.
172 // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute.
173 title.set('innerHTML', text);
174 title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;');
175 title.addClass('filterrotate');
179 // We need to fix a font-size - sorry theme designers.
180 test = Y.Node.create('<h2 class="transform-test-heading"><span class="transform-test-node" style="font-size:'+fontsize+';">'+text+'</span></h2>');
181 BODY.insert(test, 0);
182 width = test.one('span').get('offsetWidth') * 1.2;
183 height = test.one('span').get('offsetHeight');
186 title.set('innerHTML', text);
187 title.addClass('css3transform');
189 // Move the title into position
191 'position' : 'relative',
192 'fontSize' : fontsize,
194 'top' : (width - height)/2
197 // Positioning is different when in RTL mode.
198 if (right_to_left()) {
199 title.setStyle('left', width/2 - height);
201 title.setStyle('right', width/2 - height);
206 'transform' : transform,
207 '-ms-transform' : transform,
208 '-moz-transform' : transform,
209 '-webkit-transform' : transform,
210 '-o-transform' : transform
213 container = Y.Node.create('<div></div>');
214 container.append(title);
215 container.setStyles({
216 height : width + (width / 4),
217 position : 'relative'
223 * Informs the dock that the content of the block has changed.
224 * This should be called by the blocks JS code if its content has been updated dynamically.
225 * This method ensure the dock resizes if need be.
228 * @method notifyBlockChange
229 * @param {Number} instanceid
232 M.core.dock.notifyBlockChange = function(instanceid) {
233 if (this._dock !== null) {
234 var dock = M.core.dock.get(),
235 activeitem = dock.getActiveItem();
236 if (activeitem && activeitem.get('blockinstanceid') === parseInt(instanceid, 10)) {
237 dock.resizePanelIfRequired();
245 * @namespace M.core.dock
252 DOCK.superclass.constructor.apply(this, arguments);
256 * Tab height manager used to ensure tabs are always visible.
258 * @property tabheightmanager
259 * @type TABHEIGHTMANAGER
261 tabheightmanager : null,
263 * Will be an eventtype if there is an eventype to prevent.
265 * @property preventevent
270 * Will be an object if there is a delayed event in effect.
272 * @property delayedevent
277 * An array of currently docked items.
279 * @property dockeditems
284 * Set to true once the dock has been drawn.
286 * @property dockdrawn
291 * The number of blocks that are currently docked.
298 * The total number of blocks that have been docked.
300 * @property totalcount
305 * A hidden node used as a holding area for DOM objects used by blocks that have been docked.
307 * @property holdingareanode
310 holdingareanode : null,
312 * Called during the initialisation process of the object.
313 * @method initializer
315 initializer : function() {
317 // Publish the events the dock has
319 * Fired when the dock first starts initialising.
320 * @event dock:starting
322 this.publish('dock:starting', {prefix: 'dock',broadcast: 2,emitFacade: true, fireOnce:true});
324 * Fired after the dock is initialised for the first time.
325 * @event dock:initialised
327 this.publish('dock:initialised', {prefix: 'dock',broadcast: 2,emitFacade: true, fireOnce:true});
329 * Fired before the dock structure and content is first created.
330 * @event dock:beforedraw
332 this.publish('dock:beforedraw', {prefix:'dock', fireOnce:true});
334 * Fired before the dock is changed from hidden to visible.
335 * @event dock:beforeshow
337 this.publish('dock:beforeshow', {prefix:'dock'});
339 * Fires after the dock has been changed from hidden to visible.
342 this.publish('dock:shown', {prefix:'dock'});
344 * Fired after the dock has been changed from visible to hidden.
347 this.publish('dock:hidden', {prefix:'dock'});
349 * Fires when an item is added to the dock.
350 * @event dock:itemadded
352 this.publish('dock:itemadded', {prefix:'dock'});
354 * Fires when an item is removed from the dock.
355 * @event dock:itemremoved
357 this.publish('dock:itemremoved', {prefix:'dock'});
359 * Fires when a block is added or removed from the dock.
360 * This happens after the itemadded and itemremoved events have been called.
361 * @event dock:itemschanged
363 this.publish('dock:itemschanged', {prefix:'dock'});
365 * Fires once when the docks panel is first initialised.
366 * @event dock:panelgenerated
368 this.publish('dock:panelgenerated', {prefix:'dock', fireOnce:true});
370 * Fires when the dock panel is about to be resized.
371 * @event dock:panelresizestart
373 this.publish('dock:panelresizestart', {prefix:'dock'});
375 * Fires after the dock panel has been resized.
376 * @event dock:resizepanelcomplete
378 this.publish('dock:resizepanelcomplete', {prefix:'dock'});
380 // Apply theme customisations here before we do any real work.
381 this._applyThemeCustomisation();
382 // Inform everyone we are now about to initialise.
383 this.fire('dock:starting');
384 this._ensureDockDrawn();
385 // Inform everyone the dock has been initialised
386 this.fire('dock:initialised');
389 * Ensures that the dock has been drawn.
391 * @method _ensureDockDrawn
394 _ensureDockDrawn : function() {
395 if (this.dockdrawn === true) {
398 var dock = this._initialiseDockNode(),
400 cssselector:'.'+CSS.dockedtitle,
404 cssselector:'.'+CSS.dockedtitle,
407 preventevent:'click',
410 if (Y.UA.ie > 0 && Y.UA.ie < 7) {
411 // Adjust for IE 6 (can't handle fixed pos)
412 dock.setStyle('height', dock.get('winHeight')+'px');
415 this.fire('dock:beforedraw');
417 this._initialiseDockControls();
419 this.tabheightmanager = new TABHEIGHTMANAGER({dock:this});
421 // Attach the required event listeners
422 // We use delegate here as that way a handful of events are created for the dock
423 // and all items rather than the same number for the dock AND every item individually
424 Y.delegate('click', this.handleEvent, this.get('dockNode'), '.'+CSS.dockedtitle, this, clickargs);
425 Y.delegate('mouseenter', this.handleEvent, this.get('dockNode'), '.'+CSS.dockedtitle, this, mouseenterargs);
426 this.get('dockNode').on('mouseleave', this.handleEvent, this, {cssselector:'#dock', delay:0.5, iscontained:false});
428 Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this);
429 Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this);
430 Y.delegate('dock:actionkey', this.handleDockedItemEvent, this.get('dockNode'), '.'+CSS.dockeditem, this);
432 BODY.on('click', this.handleEvent, this, {cssselector:'body', delay:0});
433 this.on('dock:itemschanged', this.resizeBlockSpace, this);
434 this.on('dock:itemschanged', this.checkDockVisibility, this);
435 this.on('dock:itemschanged', this.resetFirstItem, this);
436 this.dockdrawn = true;
440 * Handles an actionkey event on the dock.
441 * @param {EventFacade} e
442 * @method handleDockedItemEvent
445 handleDockedItemEvent : function(e) {
446 if (e.type !== 'dock:actionkey') {
449 var target = e.target,
450 dockeditem = '.'+CSS.dockeditem;
451 if (!target.test(dockeditem)) {
452 target = target.ancestor(dockeditem);
458 this.dockeditems[target.getAttribute('rel')].toggle(e.action);
461 * Call the theme customisation method "customise_dock_for_theme" if it exists.
463 * @method _applyThemeCustomisation
465 _applyThemeCustomisation : function() {
466 // Check if there is a customisation function
467 if (typeof(customise_dock_for_theme) === 'function') {
468 // First up pre the legacy object.
474 spacebeforefirstitem : null,
482 buttonscontainer : null,
484 dockeditemcontainer : null,
489 // Run the customisation function
490 customise_dock_for_theme(this);
491 } catch (exception) {
492 // Do nothing at the moment.
494 // Now to work out what they did.
498 buffer : 'bufferPanel',
499 orientation : 'orientation',
500 position : 'position',
501 spacebeforefirstitem : 'bufferBeforeFirstItem',
502 removeallicon : 'undockAllIconUrl'
504 // Check for and apply any legacy configuration.
505 for (key in M.core_dock.cfg) {
506 if (Y.Lang.isString(key) && cfgmap[key]) {
507 value = M.core_dock.cfg[key];
508 if (value === null) {
514 // Damn, the've set something.
515 this.set(cfgmap[key], value);
518 // Check for and apply any legacy CSS changes..
519 for (key in M.core_dock.css) {
520 if (Y.Lang.isString(key)) {
521 value = M.core_dock.css[key];
522 if (value === null) {
528 // Damn, they've set something.
535 * Initialises the dock node, creating it and its content if required.
538 * @method _initialiseDockNode
539 * @return {Node} The dockNode
541 _initialiseDockNode : function() {
542 var dock = this.get('dockNode'),
543 positionorientationclass = CSS.dock+'_'+this.get('position')+'_'+this.get('orientation'),
544 holdingarea = Y.Node.create('<div></div>').setStyles({display:'none'}),
545 buttons = this.get('buttonsNode'),
546 container = this.get('itemContainerNode');
549 dock = Y.one('#'+CSS.dock);
552 dock = Y.Node.create('<div id="'+CSS.dock+'"></div>');
555 dock.setAttribute('role', 'menubar').addClass(positionorientationclass);
556 if (Y.all(SELECTOR.dockonload).size() === 0) {
557 // Nothing on the dock... hide it using CSS
558 dock.addClass('nothingdocked');
560 positionorientationclass = CSS.body+'_'+this.get('position')+'_'+this.get('orientation');
561 BODY.addClass(CSS.body).addClass();
565 buttons = dock.one('.'+CSS.buttonscontainer);
568 buttons = Y.Node.create('<div class="'+CSS.buttonscontainer+'"></div>');
569 dock.append(buttons);
573 container = dock.one('.'+CSS.dockeditemcontainer);
576 container = Y.Node.create('<div class="'+CSS.dockeditemcontainer+'"></div>');
577 buttons.append(container);
580 BODY.append(holdingarea);
581 this.holdingareanode = holdingarea;
583 this.set('dockNode', dock);
584 this.set('buttonsNode', buttons);
585 this.set('itemContainerNode', container);
590 * Initialises the dock controls.
593 * @method _initialiseDockControls
595 _initialiseDockControls : function() {
596 // Add a removeall button
597 // Must set the image src seperatly of we get an error with XML strict headers
599 var removeall = Y.Node.create('<img alt="'+M.util.get_string('undockall', 'block')+'" tabindex="0" />');
600 removeall.setAttribute('src',this.get('undockAllIconUrl'));
601 removeall.on('removeall|click', this.removeAll, this);
602 removeall.on('dock:actionkey', this.removeAll, this, {actions:{enter:true}});
603 this.get('buttonsNode').append(Y.Node.create('<div class="'+CSS.controls+'"></div>').append(removeall));
606 * Returns the dock panel. Initialising it if it hasn't already been initialised.
608 * @return {DOCKPANEL}
610 getPanel : function() {
611 var panel = this.get('panel');
613 panel = new DOCKPANEL({dock:this});
614 panel.on('panel:visiblechange', this.resize, this);
615 Y.on('windowresize', this.resize, this);
616 // Initialise the dockpanel .. should only happen once
617 this.set('panel', panel);
618 this.fire('dock:panelgenerated');
623 * Resizes the dock panel if required.
624 * @method resizePanelIfRequired
626 resizePanelIfRequired : function() {
628 var panel = this.get('panel');
630 panel.correctWidth();
634 * Handles a dock event sending it to the right place.
636 * @method handleEvent
637 * @param {EventFacade} e
638 * @param {Object} options
641 handleEvent : function(e, options) {
642 var item = this.getActiveItem(),
645 regex = /^dock_item_(\d+)_title$/,
647 if (options.cssselector === 'body') {
648 if (!this.get('dockNode').contains(e.target)) {
654 if (e.target.test(options.cssselector)) {
657 target = e.target.ancestor(options.cssselector);
662 if (this.preventevent !== null && e.type === this.preventevent) {
665 if (options.preventevent) {
666 this.preventevent = options.preventevent;
667 if (options.preventdelay) {
668 setTimeout(function(){
669 self.preventevent = null;
670 }, options.preventdelay * 1000);
673 if (this.delayedevent && this.delayedevent.timeout) {
674 clearTimeout(this.delayedevent.timeout);
675 this.delayedevent.event.detach();
676 this.delayedevent = null;
678 if (options.delay > 0) {
679 return this.delayEvent(e, options, target);
681 targetid = target.get('id');
682 if (targetid.match(regex)) {
683 item = this.dockeditems[targetid.replace(regex, '$1')];
699 * @param {EventFacade} event
700 * @param {Object} options
701 * @param {Node} target
704 delayEvent : function(event, options, target) {
706 self.delayedevent = (function(){
709 event : BODY.on('mousemove', function(e){
710 self.delayedevent.target = e.target;
715 self.delayedevent.timeout = setTimeout(function(){
716 self.delayedevent.timeout = null;
717 self.delayedevent.event.detach();
718 if (options.iscontained === self.get('dockNode').contains(self.delayedevent.target)) {
719 self.handleEvent(event, {cssselector:options.cssselector, delay:0, iscontained:options.iscontained});
721 }, options.delay*1000);
725 * Resizes block spaces.
726 * @method resizeBlockSpace
728 resizeBlockSpace : function() {
729 if (Y.all(SELECTOR.dockonload).size() > 0) {
730 // Do not resize during initial load
733 var blockregions = [],
734 populatedblockregions = 0,
735 allnewregions = true,
738 // First look for understood regions.
739 Y.all(SELECTOR.blockregion).each(function(region){
740 var regionname = region.getData('blockregion');
741 if (region.all('.block').size() > 0) {
742 populatedblockregions++;
743 BODY.addClass('used-region-'+regionname);
744 BODY.removeClass('empty-region-'+regionname);
745 BODY.removeClass('docked-region-'+regionname);
747 BODY.addClass('empty-region-'+regionname);
748 BODY.addClass('docked-region-'+regionname);
749 BODY.removeClass('used-region-'+regionname);
752 // Next check for legacy regions.
753 Y.all('.block-region').each(function(region){
754 if (region.test(SELECTOR.blockregion)) {
755 // This is a new region, we've already processed it.
758 var hasblocks = (region.all('.block').size() > 0);
760 populatedblockregions++;
762 allnewregions = false;
763 blockregions[region.get('id')] = {
764 hasblocks : hasblocks,
765 bodyclass : region.get('id').replace(/^region\-/, 'side-')+'-only'
768 if (BODY.hasClass('blocks-moving')) {
769 // open up blocks during blocks positioning
772 if (populatedblockregions === 0 && showregions === false) {
773 BODY.addClass(CSS.contentonly);
775 BODY.removeClass(CSS.contentonly);
778 if (!allnewregions) {
779 if (populatedblockregions === 0 && showregions === false) {
780 for (i in blockregions) {
781 if (blockregions[i].bodyclass) {
782 BODY.removeClass(blockregions[i].bodyclass);
785 } else if (populatedblockregions === 1 && showregions === false) {
786 for (i in blockregions) {
787 if (blockregions[i].bodyclass) {
788 if (!blockregions[i].hasblocks) {
789 BODY.removeClass(blockregions[i].bodyclass);
791 BODY.addClass(blockregions[i].bodyclass);
796 for (i in blockregions) {
797 if (blockregions[i].bodyclass) {
798 BODY.removeClass(blockregions[i].bodyclass);
805 * Adds an item to the dock.
807 * @param {DOCKEDITEM} item
809 add : function(item) {
810 // Set the dockitem id to the total count and then increment it.
811 item.set('id', this.totalcount);
814 this.dockeditems[item.get('id')] = item;
815 this.dockeditems[item.get('id')].draw();
816 this.fire('dock:itemadded', item);
817 this.fire('dock:itemschanged', item);
820 * Appends an item to the dock (putting it in the item container.
822 * @param {Node} docknode
824 append : function(docknode) {
825 this.get('itemContainerNode').append(docknode);
828 * Handles events that require a docked block to be returned to the page./
829 * @method handleReturnToBlock
830 * @param {EventFacade} e
832 handleReturnToBlock : function(e) {
834 this.remove(this.getActiveItem().get('id'));
837 * Removes a docked item from the dock.
839 * @param {Number} id The docked item id.
842 remove : function(id) {
843 if (!this.dockeditems[id]) {
846 this.dockeditems[id].remove();
847 delete this.dockeditems[id];
849 this.fire('dock:itemremoved', id);
850 this.fire('dock:itemschanged', id);
854 * Ensures the the first item in the dock has the correct class.
855 * @method resetFirstItem
857 resetFirstItem : function() {
858 this.get('dockNode').all('.'+CSS.dockeditem+'.firstdockitem').removeClass('firstdockitem');
859 if (this.get('dockNode').one('.'+CSS.dockeditem)) {
860 this.get('dockNode').one('.'+CSS.dockeditem).addClass('firstdockitem');
864 * Removes all docked blocks returning them to the page.
868 removeAll : function() {
870 for (i in this.dockeditems) {
871 if (Y.Lang.isNumber(i) || Y.Lang.isString(i)) {
878 * Hides the active item.
881 hideActive : function() {
882 var item = this.getActiveItem();
888 * Checks wether the dock should be shown or hidden
889 * @method checkDockVisibility
891 checkDockVisibility : function() {
892 var bodyclass = CSS.body+'_'+this.get('position')+'_'+this.get('orientation');
894 this.get('dockNode').addClass('nothingdocked');
895 BODY.removeClass(CSS.body).removeClass();
896 this.fire('dock:hidden');
898 this.fire('dock:beforeshow');
899 this.get('dockNode').removeClass('nothingdocked');
900 BODY.addClass(CSS.body).addClass(bodyclass);
901 this.fire('dock:shown');
905 * This function checks the size and position of the panel and moves/resizes if
906 * required to keep it within the bounds of the window.
910 resize : function() {
911 var panel = this.getPanel(),
912 item = this.getActiveItem(),
923 if (!panel.get('visible') || !item) {
927 this.fire('dock:panelresizestart');
928 if (this.get('orientation') === 'vertical') {
929 buffer = this.get('bufferPanel');
930 screenh = parseInt(BODY.get('winHeight'), 10)-(buffer*2);
931 docky = this.get('dockNode').getY();
932 titletop = item.get('dockTitleNode').getY()-docky-buffer;
933 containery = this.get('itemContainerNode').getY();
934 containerheight = containery-docky+this.get('buttonsNode').get('offsetHeight');
935 scrolltop = panel.get('bodyNode').get('scrollTop');
936 panel.get('bodyNode').setStyle('height', 'auto');
937 panel.get('node').removeClass('oversized_content');
938 panelheight = panel.get('node').get('offsetHeight');
940 if (Y.UA.ie > 0 && Y.UA.ie < 7) {
941 panel.setTop(item.get('dockTitleNode').getY());
942 } else if (panelheight > screenh) {
943 panel.setTop(buffer-containerheight);
944 panel.get('bodyNode').setStyle('height', (screenh-panel.get('headerNode').get('offsetHeight'))+'px');
945 panel.get('node').addClass('oversized_content');
946 } else if (panelheight > (screenh-(titletop-buffer))) {
947 panel.setTop(titletop-containerheight-(panelheight - (screenh-titletop))+buffer);
949 panel.setTop(titletop-containerheight+buffer);
953 panel.get('bodyNode').set('scrollTop', scrolltop);
957 if (this.get('position') === 'right') {
958 panel.get('node').setStyle('left', -panel.get('offsetWidth')+'px');
960 } else if (this.get('position') === 'top') {
961 dockx = this.get('dockNode').getX();
962 titleleft = item.get('dockTitleNode').getX()-dockx;
963 panel.get('node').setStyle('left', titleleft+'px');
966 this.fire('dock:resizepanelcomplete');
970 * Returns the currently active dock item or false
971 * @method getActiveItem
972 * @return {DOCKEDITEM}
974 getActiveItem : function() {
976 for (i in this.dockeditems) {
977 if (this.dockeditems[i].active) {
978 return this.dockeditems[i];
984 * Adds an item to the holding area.
985 * @method addToHoldingArea
988 addToHoldingArea : function(node) {
989 this.holdingareanode.append(node);
993 Y.extend(DOCK, Y.Base, DOCK.prototype, {
994 NAME : 'moodle-core-dock',
997 * The dock itself. #dock.
998 * @attribute dockNode
1015 * A container within the dock used for buttons.
1016 * @attribute buttonsNode
1024 * A container within the dock used for docked blocks.
1025 * @attribute itemContainerNode
1029 itemContainerNode : {
1034 * Buffer used when containing a panel.
1035 * @attribute bufferPanel
1041 validator : Y.Lang.isNumber
1045 * Position of the dock.
1046 * @attribute position
1052 validator : Y.Lang.isString
1056 * vertical || horizontal determines if we change the title
1057 * @attribute orientation
1063 validator : Y.Lang.isString,
1064 setter : function(value) {
1065 if (value.match(/^vertical$/i)) {
1068 return 'horizontal';
1073 * Space between the top of the dock and the first item.
1074 * @attribute bufferBeforeFirstItem
1078 bufferBeforeFirstItem : {
1080 validator : Y.Lang.isNumber
1084 * Icon URL for the icon to undock all blocks
1085 * @attribute undockAllIconUrl
1087 * @default t/dock_to_block
1089 undockAllIconUrl : {
1090 value : M.util.image_url('t/dock_to_block', 'moodle'),
1091 validator : Y.Lang.isString
1095 Y.augment(DOCK, Y.EventTarget);
1099 * This file contains the panel class used by the dock to display the content of docked blocks.
1101 * @module moodle-core-dock
1107 * @namespace M.core.dock
1113 DOCKPANEL = function() {
1114 DOCKPANEL.superclass.constructor.apply(this, arguments);
1116 DOCKPANEL.prototype = {
1118 * True once the panel has been created.
1125 * Called during the initialisation process of the object.
1126 * @method initializer
1128 initializer : function() {
1130 * Fired before the panel is shown.
1131 * @event dockpane::beforeshow
1133 this.publish('dockpanel:beforeshow', {prefix:'dockpanel'});
1135 * Fired after the panel is shown.
1136 * @event dockpanel:shown
1138 this.publish('dockpanel:shown', {prefix:'dockpanel'});
1140 * Fired before the panel is hidden.
1141 * @event dockpane::beforehide
1143 this.publish('dockpanel:beforehide', {prefix:'dockpanel'});
1145 * Fired after the panel is hidden.
1146 * @event dockpanel:hidden
1148 this.publish('dockpanel:hidden', {prefix:'dockpanel'});
1150 * Fired when ever the dock panel is either hidden or shown.
1151 * Always fired after the shown or hidden events.
1152 * @event dockpanel:visiblechange
1154 this.publish('dockpanel:visiblechange', {prefix:'dockpanel'});
1157 * Creates the Panel if it has not already been created.
1161 create : function() {
1165 this.created = true;
1166 var dock = this.get('dock'),
1167 node = dock.get('dockNode');
1168 this.set('node', Y.Node.create('<div id="dockeditempanel" class="dockitempanel_hidden"></div>'));
1169 this.set('contentNode', Y.Node.create('<div class="dockeditempanel_content"></div>'));
1170 this.set('headerNode', Y.Node.create('<div class="dockeditempanel_hd"></div>'));
1171 this.set('bodyNode', Y.Node.create('<div class="dockeditempanel_bd"></div>'));
1173 this.get('node').append(this.get('contentNode').append(this.get('headerNode')).append(this.get('bodyNode')))
1177 * Displays the panel.
1182 this.fire('dockpanel:beforeshow');
1183 this.set('visible', true);
1184 this.get('node').removeClass('dockitempanel_hidden');
1185 this.fire('dockpanel:shown');
1186 this.fire('dockpanel:visiblechange');
1193 this.fire('dockpanel:beforehide');
1194 this.set('visible', false);
1195 this.get('node').addClass('dockitempanel_hidden');
1196 this.fire('dockpanel:hidden');
1197 this.fire('dockpanel:visiblechange');
1200 * Sets the panel header.
1202 * @param {Node|String} content
1204 setHeader : function(content) {
1206 var header = this.get('headerNode'),
1208 header.setContent(content);
1209 if (arguments.length > 1) {
1210 for (i = 1; i < arguments.length; i++) {
1211 if (Y.Lang.isNumber(i) || Y.Lang.isString(i)) {
1212 header.append(arguments[i]);
1218 * Sets the panel body.
1220 * @param {Node|String} content
1222 setBody : function(content) {
1224 this.get('bodyNode').setContent(content);
1227 * Sets the new top mark of the panel.
1230 * @param {Number} newtop
1232 setTop : function(newtop) {
1233 if (Y.UA.ie > 0 && Y.UA.ie < 7) {
1234 this.get('node').setY(newtop);
1236 this.get('node').setStyle('top', newtop.toString()+'px');
1240 * Corrects the width of the panel.
1241 * @method correctWidth
1243 correctWidth : function() {
1244 var bodyNode = this.get('bodyNode'),
1245 // Width of content.
1246 width = bodyNode.get('clientWidth'),
1247 // Scrollable width of content.
1248 scroll = bodyNode.get('scrollWidth'),
1249 // Width of content container with overflow.
1250 offsetWidth = bodyNode.get('offsetWidth'),
1251 // The new width - defaults to the current width.
1253 // The max width (80% of screen).
1254 maxWidth = Math.round(bodyNode.get('winWidth') * 0.8);
1256 // If the scrollable width is more than the visible width
1257 if (scroll > width) {
1260 // + any rendering difference (borders, padding)
1261 // + 10px to make it look nice.
1262 newWidth = width + (scroll - width) + ((offsetWidth - width)*2) + 10;
1265 // Make sure its not more then the maxwidth
1266 if (newWidth > maxWidth) {
1267 newWidth = maxWidth;
1270 // Set the new width if its more than the old width.
1271 if (newWidth > offsetWidth) {
1272 this.get('node').setStyle('width', newWidth+'px');
1276 Y.extend(DOCKPANEL, Y.Base, DOCKPANEL.prototype, {
1277 NAME : 'moodle-core-dock-panel',
1286 writeOnce : 'initOnly'
1289 * The node that contains the whole panel.
1297 * The node that contains the header, body and footer.
1298 * @attribute contentNode
1305 * The node that contains the header
1306 * @attribute headerNode
1313 * The node that contains the body
1314 * @attribute bodyNode
1321 * True if the panel is currently visible.
1322 * @attribute visible
1330 Y.augment(DOCKPANEL, Y.EventTarget);
1334 * This file contains the tab height manager.
1335 * The tab height manager is responsible for ensure all tabs are visible all the time.
1337 * @module moodle-core-dock
1341 * Tab height manager.
1343 * @namespace M.core.dock
1344 * @class TabHeightManager
1348 TABHEIGHTMANAGER = function() {
1349 TABHEIGHTMANAGER.superclass.constructor.apply(this, arguments);
1351 TABHEIGHTMANAGER.prototype = {
1353 * Initialises the dock sizer which then attaches itself to the required
1354 * events in order to monitor the dock
1355 * @method initializer
1357 initializer : function() {
1358 var dock = this.get('dock');
1359 dock.on('dock:itemschanged', this.checkSizing, this);
1360 Y.on('windowresize', this.checkSizing, this);
1363 * Check if the size dock items needs to be adjusted
1364 * @method checkSizing
1366 checkSizing : function() {
1367 var dock = this.get('dock'),
1368 node = dock.get('dockNode'),
1369 items = dock.dockeditems,
1370 containermargin = parseInt(node.one('.dockeditem_container').getStyle('marginTop').replace('/[^0-9]+$/', ''), 10),
1371 dockheight = node.get('offsetHeight') - containermargin,
1372 controlheight = node.one('.controls').get('offsetHeight'),
1373 buffer = (dock.get('bufferPanel') * 3),
1374 possibleheight = dockheight - controlheight - buffer - (items.length*2),
1377 if (items.length > 0) {
1379 if (Y.Lang.isNumber(id) || Y.Lang.isString(id)) {
1380 dockedtitle = Y.one(items[id].get('title')).ancestor('.'+CSS.dockedtitle);
1382 if (this.get('enabled')) {
1383 dockedtitle.setStyle('height', 'auto');
1385 totalheight += dockedtitle.get('offsetHeight') || 0;
1389 if (totalheight > possibleheight) {
1390 this.enable(possibleheight);
1395 * Enables the dock sizer and resizes where required.
1397 * @param {Number} possibleheight
1399 enable : function(possibleheight) {
1400 var dock = this.get('dock'),
1401 items = dock.dockeditems,
1405 id, itemtitle, itemheight, offsetheight;
1406 this.set('enabled', true);
1408 if (Y.Lang.isNumber(id) || Y.Lang.isString(id)) {
1409 itemtitle = Y.one(items[id].get('title')).ancestor('.'+CSS.dockedtitle);
1413 itemheight = Math.floor((possibleheight-usedheight) / (count - runningcount));
1414 offsetheight = itemtitle.get('offsetHeight');
1415 itemtitle.setStyle('overflow', 'hidden');
1416 if (offsetheight > itemheight) {
1417 itemtitle.setStyle('height', itemheight+'px');
1418 usedheight += itemheight;
1420 usedheight += offsetheight;
1427 Y.extend(TABHEIGHTMANAGER, Y.Base, TABHEIGHTMANAGER.prototype, {
1428 NAME : 'moodle-core-tabheightmanager',
1437 writeOnce : 'initOnly'
1440 * True if the item_sizer is being used, false otherwise.
1441 * @attribute enabled
1452 * This file contains the action key event definition that is used for accessibility handling within the Dock.
1454 * @module moodle-core-dock
1458 * A 'dock:actionkey' Event.
1459 * The event consists of the left arrow, right arrow, enter and space keys.
1460 * More keys can be mapped to action meanings.
1461 * actions: collapse , expand, toggle, enter.
1463 * This event is subscribed to by dockitems.
1464 * The on() method to subscribe allows specifying the desired trigger actions as JSON.
1466 * This event can also be delegated if needed.
1468 * @namespace M.core.dock
1471 Y.Event.define("dock:actionkey", {
1472 // Webkit and IE repeat keydown when you hold down arrow keys.
1473 // Opera links keypress to page scroll; others keydown.
1474 // Firefox prevents page scroll via preventDefault() on either
1475 // keydown or keypress.
1476 _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
1479 * The keys to trigger on.
1486 //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings)
1492 * Handles key events
1493 * @method _keyHandler
1494 * @param {EventFacade} e
1495 * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
1496 * @param {Object} args
1498 _keyHandler: function (e, notifier, args) {
1500 if (!args.actions) {
1501 actObj = {collapse: true, expand: true, toggle: true, enter: true};
1503 actObj = args.actions;
1505 if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) {
1506 e.action = this._keys[e.keyCode];
1512 * Subscribes to events.
1514 * @param {Node} node The node this subscription was applied to.
1515 * @param {Subscription} sub The object tracking this subscription.
1516 * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
1518 on: function (node, sub, notifier) {
1519 // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
1520 if (sub.args === null) {
1522 sub._detacher = node.on(this._event, this._keyHandler, this, notifier, {actions: false});
1524 sub._detacher = node.on(this._event, this._keyHandler, this, notifier, sub.args[0]);
1529 * Detaches an event listener
1531 * @param {Node} node The node this subscription was applied to.
1532 * @param {Subscription} sub The object tracking this subscription.
1533 * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
1535 detach: function (node, sub) {
1536 //detach our _detacher handle of the subscription made in on()
1537 sub._detacher.detach();
1541 * Creates a delegated event listener.
1543 * @param {Node} node The node this subscription was applied to.
1544 * @param {Subscription} sub The object tracking this subscription.
1545 * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
1546 * @param {String|function} filter Selector string or function that accpets an event object and returns null.
1548 delegate: function (node, sub, notifier, filter) {
1549 // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
1550 if (sub.args === null) {
1552 sub._delegateDetacher = node.delegate(this._event, this._keyHandler, filter, this, notifier, {actions:false});
1554 sub._delegateDetacher = node.delegate(this._event, this._keyHandler, filter, this, notifier, sub.args[0]);
1559 * Detaches a delegated event listener.
1560 * @method detachDelegate
1561 * @param {Node} node The node this subscription was applied to.
1562 * @param {Subscription} sub The object tracking this subscription.
1563 * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
1564 * @param {String|function} filter Selector string or function that accpets an event object and returns null.
1566 detachDelegate: function (node, sub) {
1567 sub._delegateDetacher.detach();
1573 * This file contains the block class used to manage blocks (both docked and not) for the dock.
1575 * @module moodle-core-dock
1581 * @namespace M.core.dock
1586 BLOCK = function() {
1587 BLOCK.superclass.constructor.apply(this, arguments);
1591 * A content place holder used when the block has been docked.
1592 * @property contentplaceholder
1596 contentplaceholder : null,
1598 * The skip link associated with this block.
1599 * @property contentskipanchor
1603 contentskipanchor : null,
1605 * The cached content node for the actual block
1606 * @property cachedcontentnode
1610 cachedcontentnode : null,
1612 * If true the user preference isn't updated
1613 * @property skipsetposition
1617 skipsetposition : true,
1619 * The dock item associated with this block
1620 * @property dockitem
1626 * Called during the initialisation process of the object.
1627 * @method initializer
1629 initializer : function() {
1630 var node = Y.one('#inst'+this.get('id')),
1637 M.core.dock.ensureMoveToIconExists(node);
1639 // Move the block straight to the dock if required
1640 if (node.hasClass(CSS.dockonload)) {
1641 node.removeClass(CSS.dockonload);
1642 commands = node.one('.header .title .commands');
1644 commands = Y.Node.create('<div class="commands"></div>');
1645 if (node.one('.header .title')) {
1646 node.one('.header .title').append(commands);
1649 this.moveToDock(null, commands);
1651 this.skipsetposition = false;
1655 * Returns the class associated with this block.
1656 * @method _getBlockClass
1658 * @param {Node} node
1661 _getBlockClass : function(node) {
1662 var block = node.getData('block'),
1665 if (Y.Lang.isString(block) && block !== '') {
1668 classes = node.getAttribute('className').toString();
1669 matches = /(^| )block_([^ ]+)/.exec(classes);
1677 * This function is responsible for moving a block from the page structure onto the dock.
1678 * @method moveToDock
1679 * @param {EventFacade} e
1681 moveToDock : function(e) {
1686 var dock = M.core.dock.get(),
1687 id = this.get('id'),
1688 blockcontent = Y.one('#inst'+id).one('.content'),
1689 icon = (right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block',
1690 breakchar = (location.href.match(/\?/)) ? '&' : '?',
1696 if (!blockcontent) {
1701 this.recordBlockState();
1703 blocktitle = this.cachedcontentnode.one('.title h2').cloneNode(true);
1704 blockcommands = this.cachedcontentnode.one('.title .commands').cloneNode(true);
1706 movetoimg = Y.Node.create('<img />').setAttrs({
1707 alt : Y.Escape.html(M.str.block.undockitem),
1708 title : Y.Escape.html(M.util.get_string('undockblock', 'block', blocktitle.get('innerHTML'))),
1709 src : M.util.image_url(icon, 'moodle')
1711 moveto = Y.Node.create('<a class="moveto customcommand requiresjs"></a>').setAttrs({
1712 href : Y.config.win.location.href + breakchar + 'dock='+id
1714 moveto.append(movetoimg);
1715 blockcommands.append(moveto.append(movetoimg));
1717 // Create a new dock item for the block
1718 this.dockitem = new DOCKEDITEM({
1721 blockinstanceid : id,
1723 contents : blockcontent,
1724 commands : blockcommands,
1725 blockclass : this._getBlockClass(Y.one('#inst'+id))
1727 // Register an event so that when it is removed we can put it back as a block
1728 dock.add(this.dockitem);
1730 if (!this.skipsetposition) {
1731 // save the users preference
1732 M.util.set_user_preference('docked_block_instance_'+id, 1);
1735 this.set('isDocked', true);
1738 * Records the block state and adds it to the docks holding area.
1739 * @method recordBlockState
1741 recordBlockState : function() {
1742 var id = this.get('id'),
1743 dock = M.core.dock.get(),
1744 node = Y.one('#inst'+id),
1745 skipanchor = node.previous();
1746 // Disable the skip anchor when docking
1747 if (skipanchor.hasClass('skip-block')) {
1748 this.contentskipanchor = skipanchor;
1749 this.contentskipanchor.hide();
1751 this.cachedcontentnode = node;
1752 this.contentplaceholder = Y.Node.create('<div class="block_dock_placeholder"></div>');
1753 node.replace(this.contentplaceholder);
1754 dock.addToHoldingArea(node);
1756 if (!this.cachedcontentnode.one('.title .commands')) {
1757 this.cachedcontentnode.one('.title').append(Y.Node.create('<div class="commands"></div>'));
1762 * This function removes a block from the dock and puts it back into the page structure.
1763 * @method returnToPage
1766 returnToPage : function() {
1767 var id = this.get('id'),
1771 // Enable the skip anchor when going back to block mode
1772 if (this.contentskipanchor) {
1773 this.contentskipanchor.show();
1776 if (this.cachedcontentnode.one('.header')) {
1777 this.cachedcontentnode.one('.header').insert(this.dockitem.get('contents'), 'after');
1779 this.cachedcontentnode.insert(this.dockitem.get('contents'));
1782 this.contentplaceholder.replace(this.cachedcontentnode);
1783 this.cachedcontentnode = Y.one('#'+this.cachedcontentnode.get('id'));
1785 commands = this.dockitem.get('commands');
1787 commands.all('.hidepanelicon').remove();
1788 commands.all('.moveto').remove();
1791 this.cachedcontentnode = null;
1792 M.util.set_user_preference('docked_block_instance_'+id, 0);
1793 this.set('isDocked', false);
1797 Y.extend(BLOCK, Y.Base, BLOCK.prototype, {
1798 NAME : 'moodle-core-dock-block',
1801 * The block instance ID
1807 writeOnce : 'initOnly',
1808 setter : function(value) {
1809 return parseInt(value, 10);
1813 * True if the block has been docked.
1814 * @attribute isDocked
1826 * This file contains the docked item class.
1828 * @module moodle-core-dock
1834 * @namespace M.core.dock
1840 DOCKEDITEM = function() {
1841 DOCKEDITEM.superclass.constructor.apply(this, arguments);
1843 DOCKEDITEM.prototype = {
1845 * Set to true if this item is currently being displayed.
1852 * Called during the initialisation process of the object.
1853 * @method initializer
1855 initializer : function() {
1856 var title = this.get('title'),
1860 * Fired before the docked item has been drawn.
1861 * @event dockeditem:drawstart
1863 this.publish('dockeditem:drawstart', {prefix:'dockeditem'});
1865 * Fired after the docked item has been drawn.
1866 * @event dockeditem:drawcomplete
1868 this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'});
1870 * Fired before the docked item is to be shown.
1871 * @event dockeditem:showstart
1873 this.publish('dockeditem:showstart', {prefix:'dockeditem'});
1875 * Fired after the docked item has been shown.
1876 * @event dockeditem:showcomplete
1878 this.publish('dockeditem:showcomplete', {prefix:'dockeditem'});
1880 * Fired before the docked item has been hidden.
1881 * @event dockeditem:hidestart
1883 this.publish('dockeditem:hidestart', {prefix:'dockeditem'});
1885 * Fired after the docked item has been hidden.
1886 * @event dockeditem:hidecomplete
1888 this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'});
1890 * Fired when the docked item is removed from the dock.
1891 * @event dockeditem:itemremoved
1893 this.publish('dockeditem:itemremoved', {prefix:'dockeditem'});
1895 type = title.get('nodeName');
1896 titlestring = title.cloneNode(true);
1897 title = Y.Node.create('<'+type+'></'+type+'>');
1898 title = M.core.dock.fixTitleOrientation(title, titlestring.get('text'));
1899 this.set('title', title);
1900 this.set('titlestring', titlestring);
1904 * This function draws the item on the dock.
1909 var create = Y.Node.create,
1910 dock = this.get('dock'),
1916 id = this.get('id');
1918 this.fire('dockeditem:drawstart');
1920 docktitle = create('<div id="dock_item_'+id+'_title" role="menu" aria-haspopup="true" class="'+CSS.dockedtitle+'"></div>');
1921 docktitle.append(this.get('title'));
1922 dockitem = create('<div id="dock_item_'+id+'" class="'+CSS.dockeditem+'" tabindex="0" rel="'+id+'"></div>');
1924 dockitem.addClass('firstdockitem');
1926 dockitem.append(docktitle);
1927 dock.append(dockitem);
1929 closeiconimg = create('<img alt="'+M.str.block.hidepanel+'" title="'+M.str.block.hidedockpanel+'" />');
1930 closeiconimg.setAttribute('src', M.util.image_url('t/dockclose', 'moodle'));
1931 closeicon = create('<span class="hidepanelicon" tabindex="0"></span>').append(closeiconimg);
1932 closeicon.on('forceclose|click', this.hide, this);
1933 closeicon.on('dock:actionkey',this.hide, this, {actions:{enter:true,toggle:true}});
1934 this.get('commands').append(closeicon);
1936 this.set('dockTitleNode', docktitle);
1937 this.set('dockItemNode', dockitem);
1939 this.fire('dockeditem:drawcomplete');
1943 * This function toggles makes the item active and shows it.
1948 var dock = this.get('dock'),
1949 panel = dock.getPanel(),
1950 docktitle = this.get('dockTitleNode');
1953 this.fire('dockeditem:showstart');
1954 panel.setHeader(this.get('titlestring'), this.get('commands'));
1955 panel.setBody(Y.Node.create('<div class="block_'+this.get('blockclass')+' block_docked"></div>').append(this.get('contents')));
1957 panel.correctWidth();
1960 // Add active item class first up
1961 docktitle.addClass(CSS.activeitem);
1962 // Set aria-exapanded property to true.
1963 docktitle.set('aria-expanded', "true");
1964 this.fire('dockeditem:showcomplete');
1969 * This function hides the item and makes it inactive.
1973 this.fire('dockeditem:hidestart');
1976 this.active = false;
1978 this.get('dock').getPanel().hide();
1980 // Remove the active class
1981 // Set aria-exapanded property to false
1982 this.get('dockTitleNode').removeClass(CSS.activeitem).set('aria-expanded', "false");
1983 this.fire('dockeditem:hidecomplete');
1986 * A toggle between calling show and hide functions based on css.activeitem
1987 * Applies rules to key press events (dock:actionkey)
1989 * @param {String} action
1991 toggle : function(action) {
1992 var docktitle = this.get('dockTitleNode');
1993 if (docktitle.hasClass(CSS.activeitem) && action !== 'expand') {
1995 } else if (!docktitle.hasClass(CSS.activeitem) && action !== 'collapse') {
2000 * This function removes the node and destroys it's bits.
2003 remove : function () {
2005 // Return the block to its original position.
2006 this.get('block').returnToPage();
2007 // Remove the dock item node.
2008 this.get('dockItemNode').remove();
2009 this.fire('dockeditem:itemremoved');
2012 * Returns the description of this item to use for log calls.
2013 * @method _getLogDescription
2017 _getLogDescription : function() {
2018 return this.get('titlestring').get('innerHTML')+' ('+this.get('blockinstanceid')+')';
2021 Y.extend(DOCKEDITEM, Y.Base, DOCKEDITEM.prototype, {
2022 NAME : 'moodle-core-dock-dockeditem',
2025 * The block this docked item is associated with.
2032 writeOnce : 'initOnly'
2042 writeOnce : 'initOnly'
2045 * The docked item ID. This will be given by the dock.
2051 * Block instance id.Taken from the associated block.
2052 * @attribute blockinstanceid
2057 writeOnce : 'initOnly',
2058 setter : function(value) {
2059 return parseInt(value, 10);
2063 * The title nodeof the docked item.
2073 * @attribute titlestring
2080 * The contents of the docked item
2081 * @attribute contents
2087 writeOnce : 'initOnly'
2090 * Commands associated with the block.
2091 * @attribute commands
2097 writeOnce : 'initOnly'
2101 * @attribute blockclass
2107 writeOnce : 'initOnly'
2110 * The title node for the docked block.
2111 * @attribute dockTitleNode
2118 * The item node for the docked block.
2119 * @attribute dockItemNode
2126 * The container for the docked item (will contain the block contents when visible)
2127 * @attribute dockcontainerNode
2130 dockcontainerNode : {
2135 Y.augment(DOCKEDITEM, Y.EventTarget);
2146 "moodle-core-dock-loader"