MDL-41987 Javascript: Tidy up YUI documentation
[moodle.git] / lib / yui / build / moodle-core-dock / moodle-core-dock.js
blob674039ff3b9599d2e3ede00a02209a153b531ef6
1 YUI.add('moodle-core-dock', function (Y, NAME) {
3 /**
4  * Dock JS.
5  *
6  * This file contains the DOCK object and all dock related global namespace methods and properties.
7  *
8  * @module moodle-core-dock
9  */
12 var LOGNS = 'moodle-core-dock',
13     BODY = Y.one(Y.config.doc.body),
14     CSS = {
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'
26     },
27     SELECTOR = {
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]'
33     },
34     DOCK,
35     DOCKPANEL,
36     TABHEIGHTMANAGER,
37     BLOCK,
38     DOCKEDITEM;
40 M.core = M.core || {};
41 M.core.dock = M.core.dock || {};
43 /**
44  * The dock - once initialised.
45  *
46  * @private
47  * @property _dock
48  * @type DOCK
49  */
50 M.core.dock._dock = null;
52 /**
53  * An associative array of dockable blocks.
54  * @property _dockableblocks
55  * @type {Array} An array of BLOCK objects organised by instanceid.
56  * @private
57  */
58 M.core.dock._dockableblocks = {};
60 /**
61  * Initialises the dock.
62  * This method registers dockable blocks, and creates delegations to dock them.
63  * @static
64  * @method init
65  */
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');
72 /**
73  * Returns an instance of the dock.
74  * Initialises one if one hasn't already being initialised.
75  *
76  * @static
77  * @method get
78  * @return DOCK
79  */
80 M.core.dock.get = function() {
81     if (this._dock === null) {
82         this._dock = new DOCK();
83     }
84     return this._dock;
87 /**
88  * Registers a dockable block with the dock.
89  *
90  * @static
91  * @method registerDockableBlock
92  * @param {int} id The block instance ID.
93  * @return void
94  */
95 M.core.dock.registerDockableBlock = function(id) {
96     if (typeof id === 'object' && typeof id.getData === 'function') {
97         id = id.getData('instanceid');
98     }
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.
104  * @static
105  * @method dockBlockByInstanceID
106  * @param id
107  * @return void
108  */
109 M.core.dock.dockBlock = function(id) {
110     if (typeof id === 'object' && id.target !== 'undefined') {
111         id = id.target;
112     }
113     if (typeof id === "object") {
114         if (!id.test(SELECTOR.dockableblock)) {
115             id = id.ancestor(SELECTOR.dockableblock);
116         }
117         if (typeof id === 'object' && typeof id.getData === 'function' && !id.ancestor('.'+CSS.dock)) {
118             id = id.getData('instanceid');
119         } else {
120             return;
121         }
122     }
123     var block = M.core.dock._dockableblocks[id];
124     if (block) {
125         block.moveToDock();
126     }
130  * Fixes the title orientation. Rotating it if required.
132  * @static
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.
137  */
138 M.core.dock.fixTitleOrientation = function(title, text) {
139     var dock = M.core.dock.get(),
140         fontsize = '11px',
141         transform = 'rotate(270deg)',
142         test,
143         width,
144         height,
145         container;
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);
151         return title;
152     }
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';
157     }
159     switch (M.str.langconfig.thisdirectionvertical) {
160         case 'ver':
161             // Stacked is easy
162             return title.set('innerHTML', text.split('').join('<br />'));
163         case 'ttb':
164             transform = 'rotate(90deg)';
165             break;
166         case 'btt':
167             // Nothing to do here. transform default is good.
168             break;
169     }
171     if (Y.UA.ie === 8) {
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');
176         return title;
177     }
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');
184     test.remove();
186     title.set('innerHTML', text);
187     title.addClass('css3transform');
189     // Move the title into position
190     title.setStyles({
191         'position' : 'relative',
192         'fontSize' : fontsize,
193         'width' : width,
194         'top' : (width - height)/2
195     });
197     // Positioning is different when in RTL mode.
198     if (right_to_left()) {
199         title.setStyle('left', width/2 - height);
200     } else {
201         title.setStyle('right', width/2 - height);
202     }
204     // Rotate the text
205     title.setStyles({
206         'transform' : transform,
207         '-ms-transform' : transform,
208         '-moz-transform' : transform,
209         '-webkit-transform' : transform,
210         '-o-transform' : transform
211     });
213     container = Y.Node.create('<div></div>');
214     container.append(title);
215     container.setStyles({
216         height : width + (width / 4),
217         position : 'relative'
218     });
219     return container;
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.
227  * @static
228  * @method notifyBlockChange
229  * @param {Number} instanceid
230  * @return void
231  */
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();
238         }
239     }
243  * The Dock.
245  * @namespace M.core.dock
246  * @class Dock
247  * @constructor
248  * @extends Base
249  * @uses EventTarget
250  */
251 DOCK = function() {
252     DOCK.superclass.constructor.apply(this, arguments);
254 DOCK.prototype = {
255     /**
256      * Tab height manager used to ensure tabs are always visible.
257      * @protected
258      * @property tabheightmanager
259      * @type TABHEIGHTMANAGER
260      */
261     tabheightmanager : null,
262     /**
263      * Will be an eventtype if there is an eventype to prevent.
264      * @protected
265      * @property preventevent
266      * @type String
267      */
268     preventevent : null,
269     /**
270      * Will be an object if there is a delayed event in effect.
271      * @protected
272      * @property delayedevent
273      * @type {Object}
274      */
275     delayedevent : null,
276     /**
277      * An array of currently docked items.
278      * @protected
279      * @property dockeditems
280      * @type Array
281      */
282     dockeditems : [],
283     /**
284      * Set to true once the dock has been drawn.
285      * @protected
286      * @property dockdrawn
287      * @type Boolean
288      */
289     dockdrawn : false,
290     /**
291      * The number of blocks that are currently docked.
292      * @protected
293      * @property count
294      * @type Number
295      */
296     count : 0,
297     /**
298      * The total number of blocks that have been docked.
299      * @protected
300      * @property totalcount
301      * @type Number
302      */
303     totalcount : 0,
304     /**
305      * A hidden node used as a holding area for DOM objects used by blocks that have been docked.
306      * @protected
307      * @property holdingareanode
308      * @type Node
309      */
310     holdingareanode : null,
311     /**
312      * Called during the initialisation process of the object.
313      * @method initializer
314      */
315     initializer : function() {
317         // Publish the events the dock has
318         /**
319          * Fired when the dock first starts initialising.
320          * @event dock:starting
321          */
322         this.publish('dock:starting', {prefix: 'dock',broadcast:  2,emitFacade: true, fireOnce:true});
323         /**
324          * Fired after the dock is initialised for the first time.
325          * @event dock:initialised
326          */
327         this.publish('dock:initialised', {prefix: 'dock',broadcast:  2,emitFacade: true, fireOnce:true});
328         /**
329          * Fired before the dock structure and content is first created.
330          * @event dock:beforedraw
331          */
332         this.publish('dock:beforedraw', {prefix:'dock', fireOnce:true});
333         /**
334          * Fired before the dock is changed from hidden to visible.
335          * @event dock:beforeshow
336          */
337         this.publish('dock:beforeshow', {prefix:'dock'});
338         /**
339          * Fires after the dock has been changed from hidden to visible.
340          * @event dock:shown
341          */
342         this.publish('dock:shown', {prefix:'dock'});
343         /**
344          * Fired after the dock has been changed from visible to hidden.
345          * @event dock:hidden
346          */
347         this.publish('dock:hidden', {prefix:'dock'});
348         /**
349          * Fires when an item is added to the dock.
350          * @event dock:itemadded
351          */
352         this.publish('dock:itemadded', {prefix:'dock'});
353         /**
354          * Fires when an item is removed from the dock.
355          * @event dock:itemremoved
356          */
357         this.publish('dock:itemremoved', {prefix:'dock'});
358         /**
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
362          */
363         this.publish('dock:itemschanged', {prefix:'dock'});
364         /**
365          * Fires once when the docks panel is first initialised.
366          * @event dock:panelgenerated
367          */
368         this.publish('dock:panelgenerated', {prefix:'dock', fireOnce:true});
369         /**
370          * Fires when the dock panel is about to be resized.
371          * @event dock:panelresizestart
372          */
373         this.publish('dock:panelresizestart', {prefix:'dock'});
374         /**
375          * Fires after the dock panel has been resized.
376          * @event dock:resizepanelcomplete
377          */
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');
387     },
388     /**
389      * Ensures that the dock has been drawn.
390      * @private
391      * @method _ensureDockDrawn
392      * @return {Boolean}
393      */
394     _ensureDockDrawn : function() {
395         if (this.dockdrawn === true) {
396             return true;
397         }
398         var dock = this._initialiseDockNode(),
399             clickargs = {
400                 cssselector:'.'+CSS.dockedtitle,
401                 delay:0
402             },
403             mouseenterargs = {
404                 cssselector:'.'+CSS.dockedtitle,
405                 delay:0.5,
406                 iscontained:true,
407                 preventevent:'click',
408                 preventdelay:3
409             };
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');
413         }
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;
437         return true;
438     },
439     /**
440      * Handles an actionkey event on the dock.
441      * @param {EventFacade} e
442      * @method handleDockedItemEvent
443      * @return {Boolean}
444      */
445     handleDockedItemEvent : function(e) {
446         if (e.type !== 'dock:actionkey') {
447             return false;
448         }
449         var target = e.target,
450             dockeditem = '.'+CSS.dockeditem;
451         if (!target.test(dockeditem)) {
452             target = target.ancestor(dockeditem);
453         }
454         if (!target) {
455             return false;
456         }
457         e.halt();
458         this.dockeditems[target.getAttribute('rel')].toggle(e.action);
459     },
460     /**
461      * Call the theme customisation method "customise_dock_for_theme" if it exists.
462      * @private
463      * @method _applyThemeCustomisation
464      */
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.
469             M.core_dock = this;
470             M.core_dock.cfg = {
471                 buffer : null,
472                 orientation : null,
473                 position : null,
474                 spacebeforefirstitem : null,
475                 removeallicon : null
476             };
477             M.core_dock.css = {
478                 dock : null,
479                 dockspacer : null,
480                 controls : null,
481                 body : null,
482                 buttonscontainer : null,
483                 dockeditem : null,
484                 dockeditemcontainer : null,
485                 dockedtitle : null,
486                 activeitem : null
487             };
488             try {
489                 // Run the customisation function
490                 customise_dock_for_theme(this);
491             } catch (exception) {
492                 // Do nothing at the moment.
493             }
494             // Now to work out what they did.
495             var key, value,
496                 warned = false,
497                 cfgmap = {
498                     buffer : 'bufferPanel',
499                     orientation : 'orientation',
500                     position : 'position',
501                     spacebeforefirstitem : 'bufferBeforeFirstItem',
502                     removeallicon : 'undockAllIconUrl'
503                 };
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) {
509                         continue;
510                     }
511                     if (!warned) {
512                         warned = true;
513                     }
514                     // Damn, the've set something.
515                     this.set(cfgmap[key], value);
516                 }
517             }
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) {
523                         continue;
524                     }
525                     if (!warned) {
526                         warned = true;
527                     }
528                     // Damn, they've set something.
529                     CSS[key] = value;
530                 }
531             }
532         }
533     },
534     /**
535      * Initialises the dock node, creating it and its content if required.
536      *
537      * @private
538      * @method _initialiseDockNode
539      * @return {Node} The dockNode
540      */
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');
548         if (!dock) {
549             dock = Y.one('#'+CSS.dock);
550         }
551         if (!dock) {
552             dock = Y.Node.create('<div id="'+CSS.dock+'"></div>');
553             BODY.append(dock);
554         }
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');
559         } else {
560             positionorientationclass = CSS.body+'_'+this.get('position')+'_'+this.get('orientation');
561             BODY.addClass(CSS.body).addClass();
562         }
564         if (!buttons) {
565             buttons = dock.one('.'+CSS.buttonscontainer);
566         }
567         if (!buttons) {
568             buttons = Y.Node.create('<div class="'+CSS.buttonscontainer+'"></div>');
569             dock.append(buttons);
570         }
572         if (!container) {
573             container = dock.one('.'+CSS.dockeditemcontainer);
574         }
575         if (!container) {
576             container = Y.Node.create('<div class="'+CSS.dockeditemcontainer+'"></div>');
577             buttons.append(container);
578         }
580         BODY.append(holdingarea);
581         this.holdingareanode = holdingarea;
583         this.set('dockNode', dock);
584         this.set('buttonsNode', buttons);
585         this.set('itemContainerNode', container);
587         return dock;
588     },
589     /**
590      * Initialises the dock controls.
591      *
592      * @private
593      * @method _initialiseDockControls
594      */
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));
604     },
605     /**
606      * Returns the dock panel. Initialising it if it hasn't already been initialised.
607      * @method getPanel
608      * @return {DOCKPANEL}
609      */
610     getPanel : function() {
611         var panel = this.get('panel');
612         if (!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');
619         }
620         return panel;
621     },
622     /**
623      * Resizes the dock panel if required.
624      * @method resizePanelIfRequired
625      */
626     resizePanelIfRequired : function() {
627         this.resize();
628         var panel = this.get('panel');
629         if (panel) {
630             panel.correctWidth();
631         }
632     },
633     /**
634      * Handles a dock event sending it to the right place.
635      *
636      * @method handleEvent
637      * @param {EventFacade} e
638      * @param {Object} options
639      * @return {Boolean}
640      */
641     handleEvent : function(e, options) {
642         var item = this.getActiveItem(),
643             target,
644             targetid,
645             regex = /^dock_item_(\d+)_title$/,
646             self = this;
647         if (options.cssselector === 'body') {
648             if (!this.get('dockNode').contains(e.target)) {
649                 if (item) {
650                     item.hide();
651                 }
652             }
653         } else {
654             if (e.target.test(options.cssselector)) {
655                 target = e.target;
656             } else {
657                 target = e.target.ancestor(options.cssselector);
658             }
659             if (!target) {
660                 return true;
661             }
662             if (this.preventevent !== null && e.type === this.preventevent) {
663                 return true;
664             }
665             if (options.preventevent) {
666                 this.preventevent = options.preventevent;
667                 if (options.preventdelay) {
668                     setTimeout(function(){
669                         self.preventevent = null;
670                     }, options.preventdelay * 1000);
671                 }
672             }
673             if (this.delayedevent && this.delayedevent.timeout) {
674                 clearTimeout(this.delayedevent.timeout);
675                 this.delayedevent.event.detach();
676                 this.delayedevent = null;
677             }
678             if (options.delay > 0) {
679                 return this.delayEvent(e, options, target);
680             }
681             targetid = target.get('id');
682             if (targetid.match(regex)) {
683                 item = this.dockeditems[targetid.replace(regex, '$1')];
684                 if (item.active) {
685                     item.hide();
686                 } else {
687                     item.show();
688                 }
689             } else if (item) {
690                 item.hide();
691             }
692         }
693         return true;
694     },
695     /**
696      * Delays an event.
697      *
698      * @method delayEvent
699      * @param {EventFacade} event
700      * @param {Object} options
701      * @param {Node} target
702      * @return {Boolean}
703      */
704     delayEvent : function(event, options, target) {
705         var self = this;
706         self.delayedevent = (function(){
707             return {
708                 target : target,
709                 event : BODY.on('mousemove', function(e){
710                     self.delayedevent.target = e.target;
711                 }),
712                 timeout : null
713             };
714         })(self);
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});
720             }
721         }, options.delay*1000);
722         return true;
723     },
724     /**
725      * Resizes block spaces.
726      * @method resizeBlockSpace
727      */
728     resizeBlockSpace : function() {
729         if (Y.all(SELECTOR.dockonload).size() > 0) {
730             // Do not resize during initial load
731             return;
732         }
733         var blockregions = [],
734             populatedblockregions = 0,
735             allnewregions = true,
736             showregions = false,
737             i;
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);
746             } else {
747                 BODY.addClass('empty-region-'+regionname);
748                 BODY.addClass('docked-region-'+regionname);
749                 BODY.removeClass('used-region-'+regionname);
750             }
751         });
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.
756                 return;
757             }
758             var hasblocks = (region.all('.block').size() > 0);
759             if (hasblocks) {
760                 populatedblockregions++;
761             }
762             allnewregions = false;
763             blockregions[region.get('id')] = {
764                 hasblocks : hasblocks,
765                 bodyclass : region.get('id').replace(/^region\-/, 'side-')+'-only'
766             };
767         });
768         if (BODY.hasClass('blocks-moving')) {
769             // open up blocks during blocks positioning
770             showregions = true;
771         }
772         if (populatedblockregions === 0 && showregions === false) {
773             BODY.addClass(CSS.contentonly);
774         } else {
775             BODY.removeClass(CSS.contentonly);
776         }
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);
783                     }
784                 }
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);
790                         } else {
791                             BODY.addClass(blockregions[i].bodyclass);
792                         }
793                     }
794                 }
795             } else {
796                 for (i in blockregions) {
797                     if (blockregions[i].bodyclass) {
798                         BODY.removeClass(blockregions[i].bodyclass);
799                     }
800                 }
801             }
802         }
803     },
804     /**
805      * Adds an item to the dock.
806      * @method add
807      * @param {DOCKEDITEM} item
808      */
809     add : function(item) {
810         // Set the dockitem id to the total count and then increment it.
811         item.set('id', this.totalcount);
812         this.count++;
813         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);
818     },
819     /**
820      * Appends an item to the dock (putting it in the item container.
821      * @method append
822      * @param {Node} docknode
823      */
824     append : function(docknode) {
825         this.get('itemContainerNode').append(docknode);
826     },
827     /**
828      * Handles events that require a docked block to be returned to the page./
829      * @method handleReturnToBlock
830      * @param {EventFacade} e
831      */
832     handleReturnToBlock : function(e) {
833         e.halt();
834         this.remove(this.getActiveItem().get('id'));
835     },
836     /**
837      * Removes a docked item from the dock.
838      * @method remove
839      * @param {Number} id The docked item id.
840      * @return {Boolean}
841      */
842     remove : function(id) {
843         if (!this.dockeditems[id]) {
844             return false;
845         }
846         this.dockeditems[id].remove();
847         delete this.dockeditems[id];
848         this.count--;
849         this.fire('dock:itemremoved', id);
850         this.fire('dock:itemschanged', id);
851         return true;
852     },
853     /**
854      * Ensures the the first item in the dock has the correct class.
855      * @method resetFirstItem
856      */
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');
861         }
862     },
863     /**
864      * Removes all docked blocks returning them to the page.
865      * @method removeAll
866      * @return {Boolean}
867      */
868     removeAll : function() {
869         var i;
870         for (i in this.dockeditems) {
871             if (Y.Lang.isNumber(i) || Y.Lang.isString(i)) {
872                 this.remove(i);
873             }
874         }
875         return true;
876     },
877     /**
878      * Hides the active item.
879      * @method hideActive
880      */
881     hideActive : function() {
882         var item = this.getActiveItem();
883         if (item) {
884             item.hide();
885         }
886     },
887     /**
888      * Checks wether the dock should be shown or hidden
889      * @method checkDockVisibility
890      */
891     checkDockVisibility : function() {
892         var bodyclass = CSS.body+'_'+this.get('position')+'_'+this.get('orientation');
893         if (!this.count) {
894             this.get('dockNode').addClass('nothingdocked');
895             BODY.removeClass(CSS.body).removeClass();
896             this.fire('dock:hidden');
897         } else {
898             this.fire('dock:beforeshow');
899             this.get('dockNode').removeClass('nothingdocked');
900             BODY.addClass(CSS.body).addClass(bodyclass);
901             this.fire('dock:shown');
902         }
903     },
904     /**
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.
907      * @method resize
908      * @return {Boolean}
909      */
910     resize : function() {
911         var panel = this.getPanel(),
912             item = this.getActiveItem(),
913             buffer,
914             screenh,
915             docky,
916             titletop,
917             containery,
918             containerheight,
919             scrolltop,
920             panelheight,
921             dockx,
922             titleleft;
923         if (!panel.get('visible') || !item) {
924             return true;
925         }
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);
948             } else {
949                 panel.setTop(titletop-containerheight+buffer);
950             }
952             if (scrolltop) {
953                 panel.get('bodyNode').set('scrollTop', scrolltop);
954             }
955         }
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');
964         }
966         this.fire('dock:resizepanelcomplete');
967         return true;
968     },
969     /**
970      * Returns the currently active dock item or false
971      * @method getActiveItem
972      * @return {DOCKEDITEM}
973      */
974     getActiveItem : function() {
975         var i;
976         for (i in this.dockeditems) {
977             if (this.dockeditems[i].active) {
978                 return this.dockeditems[i];
979             }
980         }
981         return false;
982     },
983     /**
984      * Adds an item to the holding area.
985      * @method addToHoldingArea
986      * @param {Node} node
987      */
988     addToHoldingArea : function(node) {
989         this.holdingareanode.append(node);
990     }
993 Y.extend(DOCK, Y.Base, DOCK.prototype, {
994     NAME : 'moodle-core-dock',
995     ATTRS : {
996         /**
997          * The dock itself. #dock.
998          * @attribute dockNode
999          * @type Node
1000          * @writeOnce
1001          */
1002         dockNode : {
1003             writeOnce : true
1004         },
1005         /**
1006          * The docks panel.
1007          * @attribute panel
1008          * @type DOCKPANEL
1009          * @writeOnce
1010          */
1011         panel : {
1012             writeOnce : true
1013         },
1014         /**
1015          * A container within the dock used for buttons.
1016          * @attribute buttonsNode
1017          * @type Node
1018          * @writeOnce
1019          */
1020         buttonsNode : {
1021             writeOnce : true
1022         },
1023         /**
1024          * A container within the dock used for docked blocks.
1025          * @attribute itemContainerNode
1026          * @type Node
1027          * @writeOnce
1028          */
1029         itemContainerNode : {
1030             writeOnce : true
1031         },
1033         /**
1034          * Buffer used when containing a panel.
1035          * @attribute bufferPanel
1036          * @type Number
1037          * @default 10
1038          */
1039         bufferPanel : {
1040             value : 10,
1041             validator : Y.Lang.isNumber
1042         },
1044         /**
1045          * Position of the dock.
1046          * @attribute position
1047          * @type String
1048          * @default left
1049          */
1050         position : {
1051             value : 'left',
1052             validator : Y.Lang.isString
1053         },
1055         /**
1056          * vertical || horizontal determines if we change the title
1057          * @attribute orientation
1058          * @type String
1059          * @default vertical
1060          */
1061         orientation : {
1062             value : 'vertical',
1063             validator : Y.Lang.isString,
1064             setter : function(value) {
1065                 if (value.match(/^vertical$/i)) {
1066                     return 'vertical';
1067                 }
1068                 return 'horizontal';
1069             }
1070         },
1072         /**
1073          * Space between the top of the dock and the first item.
1074          * @attribute bufferBeforeFirstItem
1075          * @type Number
1076          * @default 10
1077          */
1078         bufferBeforeFirstItem : {
1079             value : 10,
1080             validator : Y.Lang.isNumber
1081         },
1083         /**
1084          * Icon URL for the icon to undock all blocks
1085          * @attribute undockAllIconUrl
1086          * @type String
1087          * @default t/dock_to_block
1088          */
1089         undockAllIconUrl : {
1090             value : M.util.image_url('t/dock_to_block', 'moodle'),
1091             validator : Y.Lang.isString
1092         }
1093     }
1095 Y.augment(DOCK, Y.EventTarget);
1097  * Dock JS.
1099  * This file contains the panel class used by the dock to display the content of docked blocks.
1101  * @module moodle-core-dock
1102  */
1105  * Panel.
1107  * @namespace M.core.dock
1108  * @class Panel
1109  * @constructor
1110  * @extends Base
1111  * @uses EventTarget
1112  */
1113 DOCKPANEL = function() {
1114     DOCKPANEL.superclass.constructor.apply(this, arguments);
1116 DOCKPANEL.prototype = {
1117     /**
1118      * True once the panel has been created.
1119      * @property created
1120      * @protected
1121      * @type {Boolean}
1122      */
1123     created : false,
1124     /**
1125      * Called during the initialisation process of the object.
1126      * @method initializer
1127      */
1128     initializer : function() {
1129         /**
1130          * Fired before the panel is shown.
1131          * @event dockpane::beforeshow
1132          */
1133         this.publish('dockpanel:beforeshow', {prefix:'dockpanel'});
1134         /**
1135          * Fired after the panel is shown.
1136          * @event dockpanel:shown
1137          */
1138         this.publish('dockpanel:shown', {prefix:'dockpanel'});
1139         /**
1140          * Fired before the panel is hidden.
1141          * @event dockpane::beforehide
1142          */
1143         this.publish('dockpanel:beforehide', {prefix:'dockpanel'});
1144         /**
1145          * Fired after the panel is hidden.
1146          * @event dockpanel:hidden
1147          */
1148         this.publish('dockpanel:hidden', {prefix:'dockpanel'});
1149         /**
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
1153          */
1154         this.publish('dockpanel:visiblechange', {prefix:'dockpanel'});
1155     },
1156     /**
1157      * Creates the Panel if it has not already been created.
1158      * @method create
1159      * @return {Boolean}
1160      */
1161     create : function() {
1162         if (this.created) {
1163             return true;
1164         }
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>'));
1172         node.append(
1173             this.get('node').append(this.get('contentNode').append(this.get('headerNode')).append(this.get('bodyNode')))
1174         );
1175     },
1176     /**
1177      * Displays the panel.
1178      * @method show
1179      */
1180     show : function() {
1181         this.create();
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');
1187     },
1188     /**
1189      * Hides the panel
1190      * @method hide
1191      */
1192     hide : function() {
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');
1198     },
1199     /**
1200      * Sets the panel header.
1201      * @method setHeader
1202      * @param {Node|String} content
1203      */
1204     setHeader : function(content) {
1205         this.create();
1206         var header = this.get('headerNode'),
1207             i;
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]);
1213                 }
1214             }
1215         }
1216     },
1217     /**
1218      * Sets the panel body.
1219      * @method setBody
1220      * @param {Node|String} content
1221      */
1222     setBody : function(content) {
1223         this.create();
1224         this.get('bodyNode').setContent(content);
1225     },
1226     /**
1227      * Sets the new top mark of the panel.
1228      *
1229      * @method setTop
1230      * @param {Number} newtop
1231      */
1232     setTop : function(newtop) {
1233         if (Y.UA.ie > 0 && Y.UA.ie < 7) {
1234             this.get('node').setY(newtop);
1235         } else {
1236             this.get('node').setStyle('top', newtop.toString()+'px');
1237         }
1238     },
1239     /**
1240      * Corrects the width of the panel.
1241      * @method correctWidth
1242      */
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.
1252             newWidth = 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) {
1258             //   Content width
1259             // + the difference
1260             // + any rendering difference (borders, padding)
1261             // + 10px to make it look nice.
1262             newWidth = width + (scroll - width) + ((offsetWidth - width)*2) + 10;
1263         }
1265         // Make sure its not more then the maxwidth
1266         if (newWidth > maxWidth) {
1267             newWidth = maxWidth;
1268         }
1270         // Set the new width if its more than the old width.
1271         if (newWidth > offsetWidth) {
1272             this.get('node').setStyle('width', newWidth+'px');
1273         }
1274     }
1276 Y.extend(DOCKPANEL, Y.Base, DOCKPANEL.prototype, {
1277     NAME : 'moodle-core-dock-panel',
1278     ATTRS : {
1279         /**
1280          * The dock itself.
1281          * @attribute dock
1282          * @type DOCK
1283          * @writeonce
1284          */
1285         dock : {
1286             writeOnce : 'initOnly'
1287         },
1288         /**
1289          * The node that contains the whole panel.
1290          * @attribute node
1291          * @type Node
1292          */
1293         node : {
1294             value : null
1295         },
1296         /**
1297          * The node that contains the header, body and footer.
1298          * @attribute contentNode
1299          * @type Node
1300          */
1301         contentNode : {
1302             value : null
1303         },
1304         /**
1305          * The node that contains the header
1306          * @attribute headerNode
1307          * @type Node
1308          */
1309         headerNode : {
1310             value : null
1311         },
1312         /**
1313          * The node that contains the body
1314          * @attribute bodyNode
1315          * @type Node
1316          */
1317         bodyNode : {
1318             value : null
1319         },
1320         /**
1321          * True if the panel is currently visible.
1322          * @attribute visible
1323          * @type Boolean
1324          */
1325         visible : {
1326             value : false
1327         }
1328     }
1330 Y.augment(DOCKPANEL, Y.EventTarget);
1332  * Dock JS.
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
1338  */
1341  * Tab height manager.
1343  * @namespace M.core.dock
1344  * @class TabHeightManager
1345  * @constructor
1346  * @extends Base
1347  */
1348 TABHEIGHTMANAGER = function() {
1349     TABHEIGHTMANAGER.superclass.constructor.apply(this, arguments);
1351 TABHEIGHTMANAGER.prototype = {
1352     /**
1353      * Initialises the dock sizer which then attaches itself to the required
1354      * events in order to monitor the dock
1355      * @method initializer
1356      */
1357     initializer : function() {
1358         var dock = this.get('dock');
1359         dock.on('dock:itemschanged', this.checkSizing, this);
1360         Y.on('windowresize', this.checkSizing, this);
1361     },
1362     /**
1363      * Check if the size dock items needs to be adjusted
1364      * @method checkSizing
1365      */
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),
1375             totalheight = 0,
1376             id, dockedtitle;
1377         if (items.length > 0) {
1378             for (id in items) {
1379                 if (Y.Lang.isNumber(id) || Y.Lang.isString(id)) {
1380                     dockedtitle = Y.one(items[id].get('title')).ancestor('.'+CSS.dockedtitle);
1381                     if (dockedtitle) {
1382                         if (this.get('enabled')) {
1383                             dockedtitle.setStyle('height', 'auto');
1384                         }
1385                         totalheight += dockedtitle.get('offsetHeight') || 0;
1386                     }
1387                 }
1388             }
1389             if (totalheight > possibleheight) {
1390                 this.enable(possibleheight);
1391             }
1392         }
1393     },
1394     /**
1395      * Enables the dock sizer and resizes where required.
1396      * @method enable
1397      * @param {Number} possibleheight
1398      */
1399     enable : function(possibleheight) {
1400         var dock = this.get('dock'),
1401             items = dock.dockeditems,
1402             count = dock.count,
1403             runningcount = 0,
1404             usedheight = 0,
1405             id, itemtitle, itemheight, offsetheight;
1406         this.set('enabled', true);
1407         for (id in items) {
1408             if (Y.Lang.isNumber(id) || Y.Lang.isString(id)) {
1409                 itemtitle = Y.one(items[id].get('title')).ancestor('.'+CSS.dockedtitle);
1410                 if (!itemtitle) {
1411                     continue;
1412                 }
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;
1419                 } else {
1420                     usedheight += offsetheight;
1421                 }
1422                 runningcount++;
1423             }
1424         }
1425     }
1427 Y.extend(TABHEIGHTMANAGER, Y.Base, TABHEIGHTMANAGER.prototype, {
1428     NAME : 'moodle-core-tabheightmanager',
1429     ATTRS : {
1430         /**
1431          * The dock.
1432          * @attribute dock
1433          * @type DOCK
1434          * @writeOnce
1435          */
1436         dock : {
1437             writeOnce : 'initOnly'
1438         },
1439         /**
1440          * True if the item_sizer is being used, false otherwise.
1441          * @attribute enabled
1442          * @type Bool
1443          */
1444         enabled : {
1445             value : false
1446         }
1447     }
1450  * Dock JS.
1452  * This file contains the action key event definition that is used for accessibility handling within the Dock.
1454  * @module moodle-core-dock
1455  */
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
1469  * @class ActionKey
1470  */
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',
1478     /**
1479      * The keys to trigger on.
1480      * @property _keys
1481      */
1482     _keys: {
1483         //arrows
1484         '37': 'collapse',
1485         '39': 'expand',
1486         //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings)
1487         '32': 'toggle',
1488         '13': 'enter'
1489     },
1491     /**
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
1497      */
1498     _keyHandler: function (e, notifier, args) {
1499         var actObj;
1500         if (!args.actions) {
1501             actObj = {collapse: true, expand: true, toggle: true, enter: true};
1502         } else {
1503             actObj = args.actions;
1504         }
1505         if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) {
1506             e.action = this._keys[e.keyCode];
1507             notifier.fire(e);
1508         }
1509     },
1511     /**
1512      * Subscribes to events.
1513      * @method on
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
1517      */
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) {
1521             //no actions given
1522             sub._detacher = node.on(this._event, this._keyHandler, this, notifier, {actions: false});
1523         } else {
1524             sub._detacher = node.on(this._event, this._keyHandler, this, notifier, sub.args[0]);
1525         }
1526     },
1528     /**
1529      * Detaches an event listener
1530      * @method detach
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
1534      */
1535     detach: function (node, sub) {
1536         //detach our _detacher handle of the subscription made in on()
1537         sub._detacher.detach();
1538     },
1540     /**
1541      * Creates a delegated event listener.
1542      * @method delegate
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.
1547      */
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) {
1551             //no actions given
1552             sub._delegateDetacher = node.delegate(this._event, this._keyHandler, filter, this, notifier, {actions:false});
1553         } else {
1554             sub._delegateDetacher = node.delegate(this._event, this._keyHandler, filter, this, notifier, sub.args[0]);
1555         }
1556     },
1558     /**
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.
1565      */
1566     detachDelegate: function (node, sub) {
1567         sub._delegateDetacher.detach();
1568     }
1571  * Dock JS.
1573  * This file contains the block class used to manage blocks (both docked and not) for the dock.
1575  * @module moodle-core-dock
1576  */
1579  * Block.
1581  * @namespace M.core.dock
1582  * @class Block
1583  * @constructor
1584  * @extends Base
1585  */
1586 BLOCK = function() {
1587     BLOCK.superclass.constructor.apply(this, arguments);
1589 BLOCK.prototype = {
1590     /**
1591      * A content place holder used when the block has been docked.
1592      * @property contentplaceholder
1593      * @protected
1594      * @type Node
1595      */
1596     contentplaceholder : null,
1597     /**
1598      * The skip link associated with this block.
1599      * @property contentskipanchor
1600      * @protected
1601      * @type Node
1602      */
1603     contentskipanchor : null,
1604     /**
1605      * The cached content node for the actual block
1606      * @property cachedcontentnode
1607      * @protected
1608      * @type Node
1609      */
1610     cachedcontentnode : null,
1611     /**
1612      * If true the user preference isn't updated
1613      * @property skipsetposition
1614      * @protected
1615      * @type Boolean
1616      */
1617     skipsetposition : true,
1618     /**
1619      * The dock item associated with this block
1620      * @property dockitem
1621      * @protected
1622      * @type DOCKEDITEM
1623      */
1624     dockitem : null,
1625     /**
1626      * Called during the initialisation process of the object.
1627      * @method initializer
1628      */
1629     initializer : function() {
1630         var node = Y.one('#inst'+this.get('id')),
1631             commands;
1632         if (!node) {
1633             return false;
1634         }
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');
1643             if (!commands) {
1644                 commands = Y.Node.create('<div class="commands"></div>');
1645                 if (node.one('.header .title')) {
1646                     node.one('.header .title').append(commands);
1647                 }
1648             }
1649             this.moveToDock(null, commands);
1650         }
1651         this.skipsetposition = false;
1652         return true;
1653     },
1654     /**
1655      * Returns the class associated with this block.
1656      * @method _getBlockClass
1657      * @private
1658      * @param {Node} node
1659      * @return String
1660      */
1661     _getBlockClass : function(node) {
1662         var block = node.getData('block'),
1663             classes,
1664             matches;
1665         if (Y.Lang.isString(block) && block !== '') {
1666             return block;
1667         }
1668         classes = node.getAttribute('className').toString();
1669         matches = /(^| )block_([^ ]+)/.exec(classes);
1670         if (matches) {
1671             return matches[2];
1672         }
1673         return matches;
1674     },
1676     /**
1677      * This function is responsible for moving a block from the page structure onto the dock.
1678      * @method moveToDock
1679      * @param {EventFacade} e
1680      */
1681     moveToDock : function(e) {
1682         if (e) {
1683             e.halt(true);
1684         }
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(/\?/)) ? '&' : '?',
1691             blocktitle,
1692             blockcommands,
1693             movetoimg,
1694             moveto;
1696         if (!blockcontent) {
1697             return;
1698         }
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')
1710         });
1711         moveto = Y.Node.create('<a class="moveto customcommand requiresjs"></a>').setAttrs({
1712             href : Y.config.win.location.href + breakchar + 'dock='+id
1713         });
1714         moveto.append(movetoimg);
1715         blockcommands.append(moveto.append(movetoimg));
1717         // Create a new dock item for the block
1718         this.dockitem = new DOCKEDITEM({
1719             block : this,
1720             dock : dock,
1721             blockinstanceid : id,
1722             title : blocktitle,
1723             contents : blockcontent,
1724             commands : blockcommands,
1725             blockclass : this._getBlockClass(Y.one('#inst'+id))
1726         });
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);
1733         }
1735         this.set('isDocked', true);
1736     },
1737     /**
1738      * Records the block state and adds it to the docks holding area.
1739      * @method recordBlockState
1740      */
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();
1750         }
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);
1755         node = null;
1756         if (!this.cachedcontentnode.one('.title .commands')) {
1757             this.cachedcontentnode.one('.title').append(Y.Node.create('<div class="commands"></div>'));
1758         }
1759     },
1761     /**
1762      * This function removes a block from the dock and puts it back into the page structure.
1763      * @method returnToPage
1764      * @return {Boolean}
1765      */
1766     returnToPage : function() {
1767         var id = this.get('id'),
1768             commands;
1771         // Enable the skip anchor when going back to block mode
1772         if (this.contentskipanchor) {
1773             this.contentskipanchor.show();
1774         }
1776         if (this.cachedcontentnode.one('.header')) {
1777             this.cachedcontentnode.one('.header').insert(this.dockitem.get('contents'), 'after');
1778         } else {
1779             this.cachedcontentnode.insert(this.dockitem.get('contents'));
1780         }
1782         this.contentplaceholder.replace(this.cachedcontentnode);
1783         this.cachedcontentnode = Y.one('#'+this.cachedcontentnode.get('id'));
1785         commands = this.dockitem.get('commands');
1786         if (commands) {
1787             commands.all('.hidepanelicon').remove();
1788             commands.all('.moveto').remove();
1789             commands.remove();
1790         }
1791         this.cachedcontentnode = null;
1792         M.util.set_user_preference('docked_block_instance_'+id, 0);
1793         this.set('isDocked', false);
1794         return true;
1795     }
1797 Y.extend(BLOCK, Y.Base, BLOCK.prototype, {
1798     NAME : 'moodle-core-dock-block',
1799     ATTRS : {
1800         /**
1801          * The block instance ID
1802          * @attribute id
1803          * @writeOnce
1804          * @type Number
1805          */
1806         id : {
1807             writeOnce : 'initOnly',
1808             setter : function(value) {
1809                 return parseInt(value, 10);
1810             }
1811         },
1812         /**
1813          * True if the block has been docked.
1814          * @attribute isDocked
1815          * @default false
1816          * @type Boolean
1817          */
1818         isDocked : {
1819             value : false
1820         }
1821     }
1824  * Dock JS.
1826  * This file contains the docked item class.
1828  * @module moodle-core-dock
1829  */
1832  * Docked item.
1834  * @namespace M.core.dock
1835  * @class DockedItem
1836  * @constructor
1837  * @extends Base
1838  * @uses EventTarget
1839  */
1840 DOCKEDITEM = function() {
1841     DOCKEDITEM.superclass.constructor.apply(this, arguments);
1843 DOCKEDITEM.prototype = {
1844     /**
1845      * Set to true if this item is currently being displayed.
1846      * @property active
1847      * @protected
1848      * @type Boolean
1849      */
1850     active : false,
1851     /**
1852      * Called during the initialisation process of the object.
1853      * @method initializer
1854      */
1855     initializer : function() {
1856         var title = this.get('title'),
1857             titlestring,
1858             type;
1859         /**
1860          * Fired before the docked item has been drawn.
1861          * @event dockeditem:drawstart
1862          */
1863         this.publish('dockeditem:drawstart', {prefix:'dockeditem'});
1864         /**
1865          * Fired after the docked item has been drawn.
1866          * @event dockeditem:drawcomplete
1867          */
1868         this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'});
1869         /**
1870          * Fired before the docked item is to be shown.
1871          * @event dockeditem:showstart
1872          */
1873         this.publish('dockeditem:showstart', {prefix:'dockeditem'});
1874         /**
1875          * Fired after the docked item has been shown.
1876          * @event dockeditem:showcomplete
1877          */
1878         this.publish('dockeditem:showcomplete', {prefix:'dockeditem'});
1879         /**
1880          * Fired before the docked item has been hidden.
1881          * @event dockeditem:hidestart
1882          */
1883         this.publish('dockeditem:hidestart', {prefix:'dockeditem'});
1884         /**
1885          * Fired after the docked item has been hidden.
1886          * @event dockeditem:hidecomplete
1887          */
1888         this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'});
1889         /**
1890          * Fired when the docked item is removed from the dock.
1891          * @event dockeditem:itemremoved
1892          */
1893         this.publish('dockeditem:itemremoved', {prefix:'dockeditem'});
1894         if (title) {
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);
1901         }
1902     },
1903     /**
1904      * This function draws the item on the dock.
1905      * @method draw
1906      * @return Boolean
1907      */
1908     draw : function() {
1909         var create = Y.Node.create,
1910             dock = this.get('dock'),
1911             count = dock.count,
1912             docktitle,
1913             dockitem,
1914             closeicon,
1915             closeiconimg,
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>');
1923         if (count === 1) {
1924             dockitem.addClass('firstdockitem');
1925         }
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');
1940         return true;
1941     },
1942     /**
1943      * This function toggles makes the item active and shows it.
1944      * @method show
1945      * @return Boolean
1946      */
1947     show : function() {
1948         var dock = this.get('dock'),
1949             panel = dock.getPanel(),
1950             docktitle = this.get('dockTitleNode');
1952         dock.hideActive();
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')));
1956         panel.show();
1957         panel.correctWidth();
1959         this.active = true;
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');
1965         dock.resize();
1966         return true;
1967     },
1968     /**
1969      * This function hides the item and makes it inactive.
1970      * @method hide
1971      */
1972     hide : function() {
1973         this.fire('dockeditem:hidestart');
1974         if (this.active) {
1975             // No longer active
1976             this.active = false;
1977             // Hide the panel
1978             this.get('dock').getPanel().hide();
1979         }
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');
1984     },
1985     /**
1986      * A toggle between calling show and hide functions based on css.activeitem
1987      * Applies rules to key press events (dock:actionkey)
1988      * @method toggle
1989      * @param {String} action
1990      */
1991     toggle : function(action) {
1992         var docktitle = this.get('dockTitleNode');
1993         if (docktitle.hasClass(CSS.activeitem) && action !== 'expand') {
1994             this.hide();
1995         } else if (!docktitle.hasClass(CSS.activeitem) && action !== 'collapse')  {
1996             this.show();
1997         }
1998     },
1999     /**
2000      * This function removes the node and destroys it's bits.
2001      * @method remove.
2002      */
2003     remove : function () {
2004         this.hide();
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');
2010     },
2011     /**
2012      * Returns the description of this item to use for log calls.
2013      * @method _getLogDescription
2014      * @private
2015      * @return {String}
2016      */
2017     _getLogDescription : function() {
2018         return this.get('titlestring').get('innerHTML')+' ('+this.get('blockinstanceid')+')';
2019     }
2021 Y.extend(DOCKEDITEM, Y.Base, DOCKEDITEM.prototype, {
2022     NAME : 'moodle-core-dock-dockeditem',
2023     ATTRS : {
2024         /**
2025          * The block this docked item is associated with.
2026          * @attribute block
2027          * @type BLOCK
2028          * @writeOnce
2029          * @required
2030          */
2031         block : {
2032             writeOnce : 'initOnly'
2033         },
2034         /**
2035          * The dock itself.
2036          * @attribute dock
2037          * @type DOCK
2038          * @writeOnce
2039          * @required
2040          */
2041         dock : {
2042             writeOnce : 'initOnly'
2043         },
2044         /**
2045          * The docked item ID. This will be given by the dock.
2046          * @attribute id
2047          * @type Number
2048          */
2049         id : {},
2050         /**
2051          * Block instance id.Taken from the associated block.
2052          * @attribute blockinstanceid
2053          * @type Number
2054          * @writeOnce
2055          */
2056         blockinstanceid : {
2057             writeOnce : 'initOnly',
2058             setter : function(value) {
2059                 return parseInt(value, 10);
2060             }
2061         },
2062         /**
2063          * The title  nodeof the docked item.
2064          * @attribute title
2065          * @type Node
2066          * @default null
2067          */
2068         title : {
2069             value : null
2070         },
2071         /**
2072          * The title string.
2073          * @attribute titlestring
2074          * @type String
2075          */
2076         titlestring : {
2077             value : null
2078         },
2079         /**
2080          * The contents of the docked item
2081          * @attribute contents
2082          * @type Node
2083          * @writeOnce
2084          * @required
2085          */
2086         contents : {
2087             writeOnce : 'initOnly'
2088         },
2089         /**
2090          * Commands associated with the block.
2091          * @attribute commands
2092          * @type Node
2093          * @writeOnce
2094          * @required
2095          */
2096         commands : {
2097             writeOnce : 'initOnly'
2098         },
2099         /**
2100          * The block class.
2101          * @attribute blockclass
2102          * @type String
2103          * @writeOnce
2104          * @required
2105          */
2106         blockclass : {
2107             writeOnce : 'initOnly'
2108         },
2109         /**
2110          * The title node for the docked block.
2111          * @attribute dockTitleNode
2112          * @type Node
2113          */
2114         dockTitleNode : {
2115             value : null
2116         },
2117         /**
2118          * The item node for the docked block.
2119          * @attribute dockItemNode
2120          * @type Node
2121          */
2122         dockItemNode : {
2123             value : null
2124         },
2125         /**
2126          * The container for the docked item (will contain the block contents when visible)
2127          * @attribute dockcontainerNode
2128          * @type Node
2129          */
2130         dockcontainerNode : {
2131             value : null
2132         }
2133     }
2135 Y.augment(DOCKEDITEM, Y.EventTarget);
2138 }, '@VERSION@', {
2139     "requires": [
2140         "base",
2141         "node",
2142         "event-custom",
2143         "event-mouseenter",
2144         "event-resize",
2145         "escape",
2146         "moodle-core-dock-loader"
2147     ]