29e85e5203a2d92a5d24baa40e955ec46146f68b
[moodle.git] / lib / yui / src / dock / js / dock.js
1 /**
2  * Dock JS.
3  *
4  * This file contains the DOCK object and all dock related global namespace methods and properties.
5  *
6  * @module moodle-core-dock
7  */
9 var LOGNS = 'moodle-core-dock',
10     BODY = Y.one(Y.config.doc.body),
11     CSS = {
12         dock: 'dock',                    // CSS Class applied to the dock box
13         dockspacer: 'dockspacer',        // CSS class applied to the dockspacer
14         controls: 'controls',            // CSS class applied to the controls box
15         body: 'has_dock',                // CSS class added to the body when there is a dock
16         buttonscontainer: 'buttons_container',
17         dockeditem: 'dockeditem',        // CSS class added to each item in the dock
18         dockeditemcontainer: 'dockeditem_container',
19         dockedtitle: 'dockedtitle',      // CSS class added to the item's title in each dock
20         activeitem: 'activeitem',        // CSS class added to the active item
21         dockonload: 'dock_on_load'
22     },
23     SELECTOR = {
24         dockableblock: '.block[data-instanceid][data-dockable]',
25         blockmoveto: '.block[data-instanceid][data-dockable] .moveto',
26         panelmoveto: '#dockeditempanel .commands a.moveto',
27         dockonload: '.block.' + CSS.dockonload,
28         blockregion: '[data-blockregion]'
29     },
30     DOCK,
31     DOCKPANEL,
32     TABHEIGHTMANAGER,
33     BLOCK,
34     DOCKEDITEM; // eslint-disable-line no-unused-vars
36 M.core = M.core || {};
37 M.core.dock = M.core.dock || {};
39 /**
40  * The dock - once initialised.
41  *
42  * @private
43  * @property _dock
44  * @type DOCK
45  */
46 M.core.dock._dock = null;
48 /**
49  * An associative array of dockable blocks.
50  * @property _dockableblocks
51  * @type {Array} An array of BLOCK objects organised by instanceid.
52  * @private
53  */
54 M.core.dock._dockableblocks = {};
56 /**
57  * Initialises the dock.
58  * This method registers dockable blocks, and creates delegations to dock them.
59  * @static
60  * @method init
61  */
62 M.core.dock.init = function() {
63     Y.all(SELECTOR.dockableblock).each(M.core.dock.registerDockableBlock);
64     Y.Global.on(M.core.globalEvents.BLOCK_CONTENT_UPDATED, function(e) {
65         M.core.dock.notifyBlockChange(e.instanceid);
66     }, this);
67     BODY.delegate('click', M.core.dock.dockBlock, SELECTOR.blockmoveto);
68     BODY.delegate('key', M.core.dock.dockBlock, SELECTOR.blockmoveto, 'enter');
69 };
71 /**
72  * Returns an instance of the dock.
73  * Initialises one if one hasn't already being initialised.
74  *
75  * @static
76  * @method get
77  * @return DOCK
78  */
79 M.core.dock.get = function() {
80     if (this._dock === null) {
81         this._dock = new DOCK();
82     }
83     return this._dock;
84 };
86 /**
87  * Registers a dockable block with the dock.
88  *
89  * @static
90  * @method registerDockableBlock
91  * @param {int} id The block instance ID.
92  * @return void
93  */
94 M.core.dock.registerDockableBlock = function(id) {
95     if (typeof id === 'object' && typeof id.getData === 'function') {
96         id = id.getData('instanceid');
97     }
98     M.core.dock._dockableblocks[id] = new BLOCK({id: id});
99 };
101 /**
102  * Docks a block given either its instanceid, its node, or an event fired from within the block.
103  * @static
104  * @method dockBlockByInstanceID
105  * @param id
106  * @return void
107  */
108 M.core.dock.dockBlock = function(id) {
109     if (typeof id === 'object' && id.target !== 'undefined') {
110         id = id.target;
111     }
112     if (typeof id === "object") {
113         if (!id.test(SELECTOR.dockableblock)) {
114             id = id.ancestor(SELECTOR.dockableblock);
115         }
116         if (typeof id === 'object' && typeof id.getData === 'function' && !id.ancestor('.' + CSS.dock)) {
117             id = id.getData('instanceid');
118         } else {
119             Y.log('Invalid instanceid given to dockBlockByInstanceID', 'warn', LOGNS);
120             return;
121         }
122     }
123     var block = M.core.dock._dockableblocks[id];
124     if (block) {
125         block.moveToDock();
126     }
127 };
129 /**
130  * Fixes the title orientation. Rotating it if required.
131  *
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         verticaldirection = M.util.get_string('thisdirectionvertical', 'langconfig');
147     title = Y.one(title);
149     if (dock.get('orientation') !== 'vertical') {
150         // If the dock isn't vertical don't adjust it!
151         title.set('innerHTML', text);
152         return title;
153     }
155     if (Y.UA.ie > 0 && Y.UA.ie < 8) {
156         // IE 6/7 can't rotate text so force ver
157         verticaldirection = 'ver';
158     }
160     switch (verticaldirection) {
161         case 'ver':
162             // Stacked is easy
163             return title.set('innerHTML', text.split('').join('<br />'));
164         case 'ttb':
165             transform = 'rotate(90deg)';
166             break;
167         case 'btt':
168             // Nothing to do here. transform default is good.
169             break;
170     }
172     if (Y.UA.ie === 8) {
173         // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute.
174         title.set('innerHTML', text);
175         title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;');
176         title.addClass('filterrotate');
177         return title;
178     }
180     // We need to fix a font-size - sorry theme designers.
181     test = Y.Node.create('<h2 class="transform-test-heading"><span class="transform-test-node" style="font-size:' +
182             fontsize + ';">' + text + '</span></h2>');
183     BODY.insert(test, 0);
184     width = test.one('span').get('offsetWidth') * 1.2;
185     height = test.one('span').get('offsetHeight');
186     test.remove();
188     title.set('innerHTML', text);
189     title.addClass('css3transform');
191     // Move the title into position
192     title.setStyles({
193         'position': 'relative',
194         'fontSize': fontsize,
195         'width': width,
196         'top': (width - height) / 2
197     });
199     // Positioning is different when in RTL mode.
200     if (window.right_to_left()) {
201         title.setStyle('left', width / 2 - height);
202     } else {
203         title.setStyle('right', width / 2 - height);
204     }
206     // Rotate the text
207     title.setStyles({
208         'transform': transform,
209         '-ms-transform': transform,
210         '-moz-transform': transform,
211         '-webkit-transform': transform,
212         '-o-transform': transform
213     });
215     container = Y.Node.create('<div></div>');
216     container.append(title);
217     container.setStyles({
218         height: width + (width / 4),
219         position: 'relative'
220     });
221     return container;
222 };
224 /**
225  * Informs the dock that the content of the block has changed.
226  * This should be called by the blocks JS code if its content has been updated dynamically.
227  * This method ensure the dock resizes if need be.
228  *
229  * @static
230  * @method notifyBlockChange
231  * @param {Number} instanceid
232  * @return void
233  */
234 M.core.dock.notifyBlockChange = function(instanceid) {
235     if (this._dock !== null) {
236         var dock = M.core.dock.get(),
237             activeitem = dock.getActiveItem();
238         if (activeitem && activeitem.get('blockinstanceid') === parseInt(instanceid, 10)) {
239             dock.resizePanelIfRequired();
240         }
241     }
242 };
244 /**
245  * The Dock.
246  *
247  * @namespace M.core.dock
248  * @class Dock
249  * @constructor
250  * @extends Base
251  * @uses EventTarget
252  */
253 DOCK = function() {
254     DOCK.superclass.constructor.apply(this, arguments);
255 };
256 DOCK.prototype = {
257     /**
258      * Tab height manager used to ensure tabs are always visible.
259      * @protected
260      * @property tabheightmanager
261      * @type TABHEIGHTMANAGER
262      */
263     tabheightmanager: null,
264     /**
265      * Will be an eventtype if there is an eventype to prevent.
266      * @protected
267      * @property preventevent
268      * @type String
269      */
270     preventevent: null,
271     /**
272      * Will be an object if there is a delayed event in effect.
273      * @protected
274      * @property delayedevent
275      * @type {Object}
276      */
277     delayedevent: null,
278     /**
279      * An array of currently docked items.
280      * @protected
281      * @property dockeditems
282      * @type Array
283      */
284     dockeditems: [],
285     /**
286      * Set to true once the dock has been drawn.
287      * @protected
288      * @property dockdrawn
289      * @type Boolean
290      */
291     dockdrawn: false,
292     /**
293      * The number of blocks that are currently docked.
294      * @protected
295      * @property count
296      * @type Number
297      */
298     count: 0,
299     /**
300      * The total number of blocks that have been docked.
301      * @protected
302      * @property totalcount
303      * @type Number
304      */
305     totalcount: 0,
306     /**
307      * A hidden node used as a holding area for DOM objects used by blocks that have been docked.
308      * @protected
309      * @property holdingareanode
310      * @type Node
311      */
312     holdingareanode: null,
313     /**
314      * Called during the initialisation process of the object.
315      * @method initializer
316      */
317     initializer: function() {
318         Y.log('Dock initialising', 'debug', LOGNS);
320         // Publish the events the dock has
321         /**
322          * Fired when the dock first starts initialising.
323          * @event dock:starting
324          */
325         this.publish('dock:starting', {prefix: 'dock', broadcast:  2, emitFacade: true, fireOnce: true});
326         /**
327          * Fired after the dock is initialised for the first time.
328          * @event dock:initialised
329          */
330         this.publish('dock:initialised', {prefix: 'dock', broadcast:  2, emitFacade: true, fireOnce: true});
331         /**
332          * Fired before the dock structure and content is first created.
333          * @event dock:beforedraw
334          */
335         this.publish('dock:beforedraw', {prefix: 'dock', fireOnce: true});
336         /**
337          * Fired before the dock is changed from hidden to visible.
338          * @event dock:beforeshow
339          */
340         this.publish('dock:beforeshow', {prefix: 'dock'});
341         /**
342          * Fires after the dock has been changed from hidden to visible.
343          * @event dock:shown
344          */
345         this.publish('dock:shown', {prefix: 'dock', broadcast: 2});
346         /**
347          * Fired after the dock has been changed from visible to hidden.
348          * @event dock:hidden
349          */
350         this.publish('dock:hidden', {prefix: 'dock', broadcast: 2});
351         /**
352          * Fires when an item is added to the dock.
353          * @event dock:itemadded
354          */
355         this.publish('dock:itemadded', {prefix: 'dock'});
356         /**
357          * Fires when an item is removed from the dock.
358          * @event dock:itemremoved
359          */
360         this.publish('dock:itemremoved', {prefix: 'dock'});
361         /**
362          * Fires when a block is added or removed from the dock.
363          * This happens after the itemadded and itemremoved events have been called.
364          * @event dock:itemschanged
365          */
366         this.publish('dock:itemschanged', {prefix: 'dock', broadcast: 2});
367         /**
368          * Fires once when the docks panel is first initialised.
369          * @event dock:panelgenerated
370          */
371         this.publish('dock:panelgenerated', {prefix: 'dock', fireOnce: true});
372         /**
373          * Fires when the dock panel is about to be resized.
374          * @event dock:panelresizestart
375          */
376         this.publish('dock:panelresizestart', {prefix: 'dock'});
377         /**
378          * Fires after the dock panel has been resized.
379          * @event dock:resizepanelcomplete
380          */
381         this.publish('dock:resizepanelcomplete', {prefix: 'dock'});
383         // Apply theme customisations here before we do any real work.
384         this._applyThemeCustomisation();
385         // Inform everyone we are now about to initialise.
386         this.fire('dock:starting');
387         this._ensureDockDrawn();
388         // Inform everyone the dock has been initialised
389         this.fire('dock:initialised');
390     },
391     /**
392      * Ensures that the dock has been drawn.
393      * @private
394      * @method _ensureDockDrawn
395      * @return {Boolean}
396      */
397     _ensureDockDrawn: function() {
398         if (this.dockdrawn === true) {
399             return true;
400         }
401         var dock = this._initialiseDockNode(),
402             clickargs = {
403                 cssselector: '.' + CSS.dockedtitle,
404                 delay: 0
405             },
406             mouseenterargs = {
407                 cssselector: '.' + CSS.dockedtitle,
408                 delay: 0.5,
409                 iscontained: true,
410                 preventevent: 'click',
411                 preventdelay: 3
412             };
413         if (Y.UA.ie > 0 && Y.UA.ie < 7) {
414             // Adjust for IE 6 (can't handle fixed pos)
415             dock.setStyle('height', dock.get('winHeight') + 'px');
416         }
418         this.fire('dock:beforedraw');
420         this._initialiseDockControls();
422         this.tabheightmanager = new TABHEIGHTMANAGER({dock: this});
424         // Attach the required event listeners
425         // We use delegate here as that way a handful of events are created for the dock
426         // and all items rather than the same number for the dock AND every item individually
427         Y.delegate('click', this.handleEvent, this.get('dockNode'), '.' + CSS.dockedtitle, this, clickargs);
428         Y.delegate('mouseenter', this.handleEvent, this.get('dockNode'), '.' + CSS.dockedtitle, this, mouseenterargs);
429         this.get('dockNode').on('mouseleave', this.handleEvent, this, {cssselector: '#dock', delay: 0.5, iscontained: false});
431         Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this);
432         Y.delegate('dock:actionkey', this.handleDockedItemEvent, this.get('dockNode'), '.' + CSS.dockeditem, this);
434         BODY.on('click', this.handleEvent, this, {cssselector: 'body', delay: 0});
435         this.on('dock:itemschanged', this.resizeBlockSpace, this);
436         this.on('dock:itemschanged', this.checkDockVisibility, this);
437         this.on('dock:itemschanged', this.resetFirstItem, this);
438         this.dockdrawn = true;
439         return true;
440     },
441     /**
442      * Handles an actionkey event on the dock.
443      * @param {EventFacade} e
444      * @method handleDockedItemEvent
445      * @return {Boolean}
446      */
447     handleDockedItemEvent: function(e) {
448         if (e.type !== 'dock:actionkey') {
449             return false;
450         }
451         var target = e.target,
452             dockeditem = '.' + CSS.dockeditem;
453         if (!target.test(dockeditem)) {
454             target = target.ancestor(dockeditem);
455         }
456         if (!target) {
457             return false;
458         }
459         e.halt();
460         this.dockeditems[target.getAttribute('rel')].toggle(e.action);
461     },
462     /**
463      * Call the theme customisation method "customise_dock_for_theme" if it exists.
464      * @private
465      * @method _applyThemeCustomisation
466      */
467     _applyThemeCustomisation: function() {
468         // Check if there is a customisation function
469         if (typeof (customise_dock_for_theme) === 'function') {
470             // First up pre the legacy object.
471             M.core_dock = this;
472             M.core_dock.cfg = {
473                 buffer: null,
474                 orientation: null,
475                 position: null,
476                 spacebeforefirstitem: null,
477                 removeallicon: null
478             };
479             M.core_dock.css = {
480                 dock: null,
481                 dockspacer: null,
482                 controls: null,
483                 body: null,
484                 buttonscontainer: null,
485                 dockeditem: null,
486                 dockeditemcontainer: null,
487                 dockedtitle: null,
488                 activeitem: null
489             };
490             try {
491                 // Run the customisation function
492                 window.customise_dock_for_theme(this);
493             } catch (exception) {
494                 // Do nothing at the moment.
495                 Y.log('Exception while attempting to apply theme customisations.', 'error', LOGNS);
496             }
497             // Now to work out what they did.
498             var key, value,
499                 warned = false,
500                 cfgmap = {
501                     buffer: 'bufferPanel',
502                     orientation: 'orientation',
503                     position: 'position',
504                     spacebeforefirstitem: 'bufferBeforeFirstItem',
505                     removeallicon: 'undockAllIconUrl'
506                 };
507             // Check for and apply any legacy configuration.
508             for (key in M.core_dock.cfg) {
509                 if (Y.Lang.isString(key) && cfgmap[key]) {
510                     value = M.core_dock.cfg[key];
511                     if (value === null) {
512                         continue;
513                     }
514                     if (!warned) {
515                         Y.log('Warning: customise_dock_for_theme has changed. Please update your code.', 'warn', LOGNS);
516                         warned = true;
517                     }
518                     // Damn, the've set something.
519                     Y.log('Note for customise_dock_for_theme code: M.core_dock.cfg.' + key +
520                             ' is now dock.set(\'' + key + '\', value)',
521                             'debug', LOGNS);
522                     this.set(cfgmap[key], value);
523                 }
524             }
525             // Check for and apply any legacy CSS changes..
526             for (key in M.core_dock.css) {
527                 if (Y.Lang.isString(key)) {
528                     value = M.core_dock.css[key];
529                     if (value === null) {
530                         continue;
531                     }
532                     if (!warned) {
533                         Y.log('Warning: customise_dock_for_theme has changed. Please update your code.', 'warn', LOGNS);
534                         warned = true;
535                     }
536                     // Damn, they've set something.
537                     Y.log('Note for customise_dock_for_theme code: M.core_dock.css.' + key + ' is now CSS.' + key + ' = value',
538                             'debug', LOGNS);
539                     CSS[key] = value;
540                 }
541             }
542         }
543     },
544     /**
545      * Initialises the dock node, creating it and its content if required.
546      *
547      * @private
548      * @method _initialiseDockNode
549      * @return {Node} The dockNode
550      */
551     _initialiseDockNode: function() {
552         var dock = this.get('dockNode'),
553             positionorientationclass = CSS.dock + '_' + this.get('position') + '_' + this.get('orientation'),
554             holdingarea = Y.Node.create('<div></div>').setStyles({display: 'none'}),
555             buttons = this.get('buttonsNode'),
556             container = this.get('itemContainerNode');
558         if (!dock) {
559             dock = Y.one('#' + CSS.dock);
560         }
561         if (!dock) {
562             dock = Y.Node.create('<div id="' + CSS.dock + '"></div>');
563             BODY.append(dock);
564         }
565         dock.setAttribute('role', 'menubar').addClass(positionorientationclass);
566         if (Y.all(SELECTOR.dockonload).size() === 0) {
567             // Nothing on the dock... hide it using CSS
568             dock.addClass('nothingdocked');
569         } else {
570             positionorientationclass = CSS.body + '_' + this.get('position') + '_' + this.get('orientation');
571             BODY.addClass(CSS.body).addClass();
572         }
574         if (!buttons) {
575             buttons = dock.one('.' + CSS.buttonscontainer);
576         }
577         if (!buttons) {
578             buttons = Y.Node.create('<div class="' + CSS.buttonscontainer + '"></div>');
579             dock.append(buttons);
580         }
582         if (!container) {
583             container = dock.one('.' + CSS.dockeditemcontainer);
584         }
585         if (!container) {
586             container = Y.Node.create('<div class="' + CSS.dockeditemcontainer + '"></div>');
587             buttons.append(container);
588         }
590         BODY.append(holdingarea);
591         this.holdingareanode = holdingarea;
593         this.set('dockNode', dock);
594         this.set('buttonsNode', buttons);
595         this.set('itemContainerNode', container);
597         return dock;
598     },
599     /**
600      * Initialises the dock controls.
601      *
602      * @private
603      * @method _initialiseDockControls
604      */
605     _initialiseDockControls: function() {
606         // Add a removeall button
607         // Must set the image src seperatly of we get an error with XML strict headers
609         var removeall = Y.Node.create('<img alt="' + M.util.get_string('undockall', 'block') + '" tabindex="0" />');
610         removeall.setAttribute('src', this.get('undockAllIconUrl'));
611         removeall.on('removeall|click', this.removeAll, this);
612         removeall.on('dock:actionkey', this.removeAll, this, {actions: {enter: true}});
613         this.get('buttonsNode').append(Y.Node.create('<div class="' + CSS.controls + '"></div>').append(removeall));
614     },
615     /**
616      * Returns the dock panel. Initialising it if it hasn't already been initialised.
617      * @method getPanel
618      * @return {DOCKPANEL}
619      */
620     getPanel: function() {
621         var panel = this.get('panel');
622         if (!panel) {
623             panel = new DOCKPANEL({dock: this});
624             panel.on('panel:visiblechange', this.resize, this);
625             Y.on('windowresize', this.resize, this);
626             // Initialise the dockpanel .. should only happen once
627             this.set('panel', panel);
628             this.fire('dock:panelgenerated');
629         }
630         return panel;
631     },
632     /**
633      * Resizes the dock panel if required.
634      * @method resizePanelIfRequired
635      */
636     resizePanelIfRequired: function() {
637         this.resize();
638         var panel = this.get('panel');
639         if (panel) {
640             panel.correctWidth();
641         }
642     },
643     /**
644      * Handles a dock event sending it to the right place.
645      *
646      * @method handleEvent
647      * @param {EventFacade} e
648      * @param {Object} options
649      * @return {Boolean}
650      */
651     handleEvent: function(e, options) {
652         var item = this.getActiveItem(),
653             target,
654             targetid,
655             regex = /^dock_item_(\d+)_title$/,
656             self = this;
657         if (options.cssselector === 'body') {
658             if (!this.get('dockNode').contains(e.target)) {
659                 if (item) {
660                     item.hide();
661                 }
662             }
663         } else {
664             if (e.target.test(options.cssselector)) {
665                 target = e.target;
666             } else {
667                 target = e.target.ancestor(options.cssselector);
668             }
669             if (!target) {
670                 return true;
671             }
672             if (this.preventevent !== null && e.type === this.preventevent) {
673                 return true;
674             }
675             if (options.preventevent) {
676                 this.preventevent = options.preventevent;
677                 if (options.preventdelay) {
678                     setTimeout(function() {
679                         self.preventevent = null;
680                     }, options.preventdelay * 1000);
681                 }
682             }
683             if (this.delayedevent && this.delayedevent.timeout) {
684                 clearTimeout(this.delayedevent.timeout);
685                 this.delayedevent.event.detach();
686                 this.delayedevent = null;
687             }
688             if (options.delay > 0) {
689                 return this.delayEvent(e, options, target);
690             }
691             targetid = target.get('id');
692             if (targetid.match(regex)) {
693                 item = this.dockeditems[targetid.replace(regex, '$1')];
694                 if (item.active) {
695                     item.hide();
696                 } else {
697                     item.show();
698                 }
699             } else if (item) {
700                 item.hide();
701             }
702         }
703         return true;
704     },
705     /**
706      * Delays an event.
707      *
708      * @method delayEvent
709      * @param {EventFacade} event
710      * @param {Object} options
711      * @param {Node} target
712      * @return {Boolean}
713      */
714     delayEvent: function(event, options, target) {
715         var self = this;
716         self.delayedevent = (function() {
717             return {
718                 target: target,
719                 event: BODY.on('mousemove', function(e) {
720                     self.delayedevent.target = e.target;
721                 }),
722                 timeout: null
723             };
724         })(self);
725         self.delayedevent.timeout = setTimeout(function() {
726             self.delayedevent.timeout = null;
727             self.delayedevent.event.detach();
728             if (options.iscontained === self.get('dockNode').contains(self.delayedevent.target)) {
729                 self.handleEvent(event, {cssselector: options.cssselector, delay: 0, iscontained: options.iscontained});
730             }
731         }, options.delay * 1000);
732         return true;
733     },
734     /**
735      * Resizes block spaces.
736      * @method resizeBlockSpace
737      */
738     resizeBlockSpace: function() {
739         if (Y.all(SELECTOR.dockonload).size() > 0) {
740             // Do not resize during initial load
741             return;
742         }
744         var populatedRegionCount = 0,
745             populatedBlockRegions = [],
746             unpopulatedBlockRegions = [],
747             isMoving = false,
748             populatedLegacyRegions = [],
749             containsLegacyRegions = false,
750             classesToAdd = [],
751             classesToRemove = [];
753         // First look for understood regions.
754         Y.all(SELECTOR.blockregion).each(function(region) {
755             var regionname = region.getData('blockregion');
756             if (region.all('.block').size() > 0) {
757                 populatedBlockRegions.push(regionname);
758                 populatedRegionCount++;
759             } else if (region.all('.block_dock_placeholder').size() > 0) {
760                 unpopulatedBlockRegions.push(regionname);
761             }
762         });
764         // Next check for legacy regions.
765         Y.all('.block-region').each(function(region) {
766             if (region.test(SELECTOR.blockregion)) {
767                 // This is a new region, we've already processed it.
768                 return;
769             }
771             // Sigh - there are legacy regions.
772             containsLegacyRegions = true;
774             var regionname = region.get('id').replace(/^region\-/, 'side-'),
775                 hasblocks = (region.all('.block').size() > 0);
777             if (hasblocks) {
778                 populatedLegacyRegions.push(regionname);
779                 populatedRegionCount++;
780             } else {
781                 // This legacy region has no blocks so cannot have the -only body tag.
782                 classesToRemove.push(
783                         regionname + '-only'
784                     );
785             }
786         });
788         if (BODY.hasClass('blocks-moving')) {
789             // When we're moving blocks, we do not want to collapse.
790             isMoving = true;
791         }
793         Y.each(unpopulatedBlockRegions, function(regionname) {
794             classesToAdd.push(
795                     // This block region is empty.
796                     'empty-region-' + regionname,
798                     // Which has the same effect as being docked.
799                     'docked-region-' + regionname
800                 );
801             classesToRemove.push(
802                     // It is no-longer used.
803                     'used-region-' + regionname,
805                     // It cannot be the only region on screen if it is empty.
806                     regionname + '-only'
807                 );
808         }, this);
810         Y.each(populatedBlockRegions, function(regionname) {
811             classesToAdd.push(
812                     // This block region is in use.
813                     'used-region-' + regionname
814                 );
815             classesToRemove.push(
816                     // It is not empty.
817                     'empty-region-' + regionname,
819                     // Is it not docked.
820                     'docked-region-' + regionname
821                 );
823             if (populatedRegionCount === 1 && isMoving === false) {
824                 // There was only one populated region, and we are not moving blocks.
825                 classesToAdd.push(regionname + '-only');
826             } else {
827                 // There were multiple block regions visible - remove any 'only' classes.
828                 classesToRemove.push(regionname + '-only');
829             }
830         }, this);
832         if (containsLegacyRegions) {
833             // Handle the classing for legacy blocks. These have slightly different class names for the body.
834             if (isMoving || populatedRegionCount !== 1) {
835                 Y.each(populatedLegacyRegions, function(regionname) {
836                     classesToRemove.push(regionname + '-only');
837                 });
838             } else {
839                 Y.each(populatedLegacyRegions, function(regionname) {
840                     classesToAdd.push(regionname + '-only');
841                 });
842             }
843         }
845         if (!BODY.hasClass('has-region-content')) {
846             // This page does not have a content region, therefore content-only is implied when all block regions are docked.
847             if (populatedRegionCount === 0 && isMoving === false) {
848                 // If all blocks are docked, ensure that the content-only class is added anyway.
849                 classesToAdd.push('content-only');
850             } else {
851                 // Otherwise remove it.
852                 classesToRemove.push('content-only');
853             }
854         }
856         // Modify the body clases.
857         Y.each(classesToRemove, function(className) {
858             BODY.removeClass(className);
859         });
860         Y.each(classesToAdd, function(className) {
861             BODY.addClass(className);
862         });
863     },
864     /**
865      * Adds an item to the dock.
866      * @method add
867      * @param {DOCKEDITEM} item
868      */
869     add: function(item) {
870         // Set the dockitem id to the total count and then increment it.
871         item.set('id', this.totalcount);
872         Y.log('Adding block ' + item._getLogDescription() + ' to the dock.', 'debug', LOGNS);
873         this.count++;
874         this.totalcount++;
875         this.dockeditems[item.get('id')] = item;
876         this.dockeditems[item.get('id')].draw();
877         this.fire('dock:itemadded', item);
878         this.fire('dock:itemschanged', item);
879     },
880     /**
881      * Appends an item to the dock (putting it in the item container.
882      * @method append
883      * @param {Node} docknode
884      */
885     append: function(docknode) {
886         this.get('itemContainerNode').append(docknode);
887     },
888     /**
889      * Handles events that require a docked block to be returned to the page./
890      * @method handleReturnToBlock
891      * @param {EventFacade} e
892      */
893     handleReturnToBlock: function(e) {
894         e.halt();
895         this.remove(this.getActiveItem().get('id'));
896     },
897     /**
898      * Removes a docked item from the dock.
899      * @method remove
900      * @param {Number} id The docked item id.
901      * @return {Boolean}
902      */
903     remove: function(id) {
904         if (!this.dockeditems[id]) {
905             return false;
906         }
907         Y.log('Removing block ' + this.dockeditems[id]._getLogDescription() + ' from the dock.', 'debug', LOGNS);
908         this.dockeditems[id].remove();
909         delete this.dockeditems[id];
910         this.count--;
911         this.fire('dock:itemremoved', id);
912         this.fire('dock:itemschanged', id);
913         return true;
914     },
915     /**
916      * Ensures the the first item in the dock has the correct class.
917      * @method resetFirstItem
918      */
919     resetFirstItem: function() {
920         this.get('dockNode').all('.' + CSS.dockeditem + '.firstdockitem').removeClass('firstdockitem');
921         if (this.get('dockNode').one('.' + CSS.dockeditem)) {
922             this.get('dockNode').one('.' + CSS.dockeditem).addClass('firstdockitem');
923         }
924     },
925     /**
926      * Removes all docked blocks returning them to the page.
927      * @method removeAll
928      * @return {Boolean}
929      */
930     removeAll: function() {
931         Y.log('Undocking all ' + this.dockeditems.length + ' blocks', 'debug', LOGNS);
932         var i;
933         for (i in this.dockeditems) {
934             if (Y.Lang.isNumber(i) || Y.Lang.isString(i)) {
935                 this.remove(i);
936             }
937         }
938         return true;
939     },
940     /**
941      * Hides the active item.
942      * @method hideActive
943      */
944     hideActive: function() {
945         var item = this.getActiveItem();
946         if (item) {
947             item.hide();
948         }
949     },
950     /**
951      * Checks wether the dock should be shown or hidden
952      * @method checkDockVisibility
953      */
954     checkDockVisibility: function() {
955         var bodyclass = CSS.body + '_' + this.get('position') + '_' + this.get('orientation');
956         if (!this.count) {
957             this.get('dockNode').addClass('nothingdocked');
958             BODY.removeClass(CSS.body).removeClass();
959             this.fire('dock:hidden');
960         } else {
961             this.fire('dock:beforeshow');
962             this.get('dockNode').removeClass('nothingdocked');
963             BODY.addClass(CSS.body).addClass(bodyclass);
964             this.fire('dock:shown');
965         }
966     },
967     /**
968      * This function checks the size and position of the panel and moves/resizes if
969      * required to keep it within the bounds of the window.
970      * @method resize
971      * @return {Boolean}
972      */
973     resize: function() {
974         var panel = this.getPanel(),
975             item = this.getActiveItem(),
976             buffer,
977             screenh,
978             docky,
979             titletop,
980             containery,
981             containerheight,
982             scrolltop,
983             panelheight,
984             dockx,
985             titleleft;
986         if (!panel.get('visible') || !item) {
987             return true;
988         }
990         this.fire('dock:panelresizestart');
991         if (this.get('orientation') === 'vertical') {
992             buffer = this.get('bufferPanel');
993             screenh = parseInt(BODY.get('winHeight'), 10) - (buffer * 2);
994             docky = this.get('dockNode').getY();
995             titletop = item.get('dockTitleNode').getY() - docky - buffer;
996             containery = this.get('itemContainerNode').getY();
997             containerheight = containery - docky + this.get('buttonsNode').get('offsetHeight');
998             scrolltop = panel.get('bodyNode').get('scrollTop');
999             panel.get('bodyNode').setStyle('height', 'auto');
1000             panel.get('node').removeClass('oversized_content');
1001             panelheight = panel.get('node').get('offsetHeight');
1003             if (Y.UA.ie > 0 && Y.UA.ie < 7) {
1004                 panel.setTop(item.get('dockTitleNode').getY());
1005             } else if (panelheight > screenh) {
1006                 panel.setTop(buffer - containerheight);
1007                 panel.get('bodyNode').setStyle('height', (screenh - panel.get('headerNode').get('offsetHeight')) + 'px');
1008                 panel.get('node').addClass('oversized_content');
1009             } else if (panelheight > (screenh - (titletop - buffer))) {
1010                 panel.setTop(titletop - containerheight - (panelheight - (screenh - titletop)) + buffer);
1011             } else {
1012                 panel.setTop(titletop - containerheight + buffer);
1013             }
1015             if (scrolltop) {
1016                 panel.get('bodyNode').set('scrollTop', scrolltop);
1017             }
1018         }
1020         if (this.get('position') === 'right') {
1021             panel.get('node').setStyle('left', '-' + panel.get('node').get('offsetWidth') + 'px');
1023         } else if (this.get('position') === 'top') {
1024             dockx = this.get('dockNode').getX();
1025             titleleft = item.get('dockTitleNode').getX() - dockx;
1026             panel.get('node').setStyle('left', titleleft + 'px');
1027         }
1029         this.fire('dock:resizepanelcomplete');
1030         return true;
1031     },
1032     /**
1033      * Returns the currently active dock item or false
1034      * @method getActiveItem
1035      * @return {DOCKEDITEM}
1036      */
1037     getActiveItem: function() {
1038         var i;
1039         for (i in this.dockeditems) {
1040             if (this.dockeditems[i].active) {
1041                 return this.dockeditems[i];
1042             }
1043         }
1044         return false;
1045     },
1046     /**
1047      * Adds an item to the holding area.
1048      * @method addToHoldingArea
1049      * @param {Node} node
1050      */
1051     addToHoldingArea: function(node) {
1052         this.holdingareanode.append(node);
1053     }
1054 };
1056 Y.extend(DOCK, Y.Base, DOCK.prototype, {
1057     NAME: 'moodle-core-dock',
1058     ATTRS: {
1059         /**
1060          * The dock itself. #dock.
1061          * @attribute dockNode
1062          * @type Node
1063          * @writeOnce
1064          */
1065         dockNode: {
1066             writeOnce: true
1067         },
1068         /**
1069          * The docks panel.
1070          * @attribute panel
1071          * @type DOCKPANEL
1072          * @writeOnce
1073          */
1074         panel: {
1075             writeOnce: true
1076         },
1077         /**
1078          * A container within the dock used for buttons.
1079          * @attribute buttonsNode
1080          * @type Node
1081          * @writeOnce
1082          */
1083         buttonsNode: {
1084             writeOnce: true
1085         },
1086         /**
1087          * A container within the dock used for docked blocks.
1088          * @attribute itemContainerNode
1089          * @type Node
1090          * @writeOnce
1091          */
1092         itemContainerNode: {
1093             writeOnce: true
1094         },
1096         /**
1097          * Buffer used when containing a panel.
1098          * @attribute bufferPanel
1099          * @type Number
1100          * @default 10
1101          */
1102         bufferPanel: {
1103             value: 10,
1104             validator: Y.Lang.isNumber
1105         },
1107         /**
1108          * Position of the dock.
1109          * @attribute position
1110          * @type String
1111          * @default left
1112          */
1113         position: {
1114             value: 'left',
1115             validator: Y.Lang.isString
1116         },
1118         /**
1119          * vertical || horizontal determines if we change the title
1120          * @attribute orientation
1121          * @type String
1122          * @default vertical
1123          */
1124         orientation: {
1125             value: 'vertical',
1126             validator: Y.Lang.isString,
1127             setter: function(value) {
1128                 if (value.match(/^vertical$/i)) {
1129                     return 'vertical';
1130                 }
1131                 return 'horizontal';
1132             }
1133         },
1135         /**
1136          * Space between the top of the dock and the first item.
1137          * @attribute bufferBeforeFirstItem
1138          * @type Number
1139          * @default 10
1140          */
1141         bufferBeforeFirstItem: {
1142             value: 10,
1143             validator: Y.Lang.isNumber
1144         },
1146         /**
1147          * Icon URL for the icon to undock all blocks
1148          * @attribute undockAllIconUrl
1149          * @type String
1150          * @default t/dock_to_block
1151          */
1152         undockAllIconUrl: {
1153             value: M.util.image_url((window.right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block', 'moodle'),
1154             validator: Y.Lang.isString
1155         }
1156     }
1157 });
1158 Y.augment(DOCK, Y.EventTarget);