143593e48ad88b5fa076fc9fff39e4808852f416
[moodle.git] / lib / yui / build / moodle-core-dock / moodle-core-dock.js
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  */
11 var LOGNS = 'moodle-core-dock',
12     BODY = Y.one(Y.config.doc.body),
13     CSS = {
14         dock: 'dock',                    // CSS Class applied to the dock box
15         dockspacer: 'dockspacer',        // CSS class applied to the dockspacer
16         controls: 'controls',            // CSS class applied to the controls box
17         body: 'has_dock',                // CSS class added to the body when there is a dock
18         buttonscontainer: 'buttons_container',
19         dockeditem: 'dockeditem',        // CSS class added to each item in the dock
20         dockeditemcontainer: 'dockeditem_container',
21         dockedtitle: 'dockedtitle',      // CSS class added to the item's title in each dock
22         activeitem: 'activeitem',        // CSS class added to the active item
23         dockonload: 'dock_on_load'
24     },
25     SELECTOR = {
26         dockableblock: '.block[data-instanceid][data-dockable]',
27         blockmoveto: '.block[data-instanceid][data-dockable] .moveto',
28         panelmoveto: '#dockeditempanel .commands a.moveto',
29         dockonload: '.block.' + CSS.dockonload,
30         blockregion: '[data-blockregion]'
31     },
32     DOCK,
33     DOCKPANEL,
34     TABHEIGHTMANAGER,
35     BLOCK,
36     DOCKEDITEM; // eslint-disable-line no-unused-vars
38 M.core = M.core || {};
39 M.core.dock = M.core.dock || {};
41 /**
42  * The dock - once initialised.
43  *
44  * @private
45  * @property _dock
46  * @type DOCK
47  */
48 M.core.dock._dock = null;
50 /**
51  * An associative array of dockable blocks.
52  * @property _dockableblocks
53  * @type {Array} An array of BLOCK objects organised by instanceid.
54  * @private
55  */
56 M.core.dock._dockableblocks = {};
58 /**
59  * Initialises the dock.
60  * This method registers dockable blocks, and creates delegations to dock them.
61  * @static
62  * @method init
63  */
64 M.core.dock.init = function() {
65     Y.all(SELECTOR.dockableblock).each(M.core.dock.registerDockableBlock);
66     Y.Global.on(M.core.globalEvents.BLOCK_CONTENT_UPDATED, function(e) {
67         M.core.dock.notifyBlockChange(e.instanceid);
68     }, this);
69     BODY.delegate('click', M.core.dock.dockBlock, SELECTOR.blockmoveto);
70     BODY.delegate('key', M.core.dock.dockBlock, SELECTOR.blockmoveto, 'enter');
71 };
73 /**
74  * Returns an instance of the dock.
75  * Initialises one if one hasn't already being initialised.
76  *
77  * @static
78  * @method get
79  * @return DOCK
80  */
81 M.core.dock.get = function() {
82     if (this._dock === null) {
83         this._dock = new DOCK();
84     }
85     return this._dock;
86 };
88 /**
89  * Registers a dockable block with the dock.
90  *
91  * @static
92  * @method registerDockableBlock
93  * @param {int} id The block instance ID.
94  * @return void
95  */
96 M.core.dock.registerDockableBlock = function(id) {
97     if (typeof id === 'object' && typeof id.getData === 'function') {
98         id = id.getData('instanceid');
99     }
100     M.core.dock._dockableblocks[id] = new BLOCK({id: id});
101 };
103 /**
104  * Docks a block given either its instanceid, its node, or an event fired from within the block.
105  * @static
106  * @method dockBlockByInstanceID
107  * @param id
108  * @return void
109  */
110 M.core.dock.dockBlock = function(id) {
111     if (typeof id === 'object' && id.target !== 'undefined') {
112         id = id.target;
113     }
114     if (typeof id === "object") {
115         if (!id.test(SELECTOR.dockableblock)) {
116             id = id.ancestor(SELECTOR.dockableblock);
117         }
118         if (typeof id === 'object' && typeof id.getData === 'function' && !id.ancestor('.' + CSS.dock)) {
119             id = id.getData('instanceid');
120         } else {
121             return;
122         }
123     }
124     var block = M.core.dock._dockableblocks[id];
125     if (block) {
126         block.moveToDock();
127     }
128 };
130 /**
131  * Fixes the title orientation. Rotating it if required.
132  *
133  * @static
134  * @method fixTitleOrientation
135  * @param {Node} title The title node we are looking at.
136  * @param {String} text The string to use as the title.
137  * @return {Node} The title node to use.
138  */
139 M.core.dock.fixTitleOrientation = function(title, text) {
140     var dock = M.core.dock.get(),
141         fontsize = '11px',
142         transform = 'rotate(270deg)',
143         test,
144         width,
145         height,
146         container,
147         verticaldirection = M.util.get_string('thisdirectionvertical', 'langconfig');
148     title = Y.one(title);
150     if (dock.get('orientation') !== 'vertical') {
151         // If the dock isn't vertical don't adjust it!
152         title.set('innerHTML', text);
153         return title;
154     }
156     if (Y.UA.ie > 0 && Y.UA.ie < 8) {
157         // IE 6/7 can't rotate text so force ver
158         verticaldirection = 'ver';
159     }
161     switch (verticaldirection) {
162         case 'ver':
163             // Stacked is easy
164             return title.set('innerHTML', text.split('').join('<br />'));
165         case 'ttb':
166             transform = 'rotate(90deg)';
167             break;
168         case 'btt':
169             // Nothing to do here. transform default is good.
170             break;
171     }
173     if (Y.UA.ie === 8) {
174         // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute.
175         title.set('innerHTML', text);
176         title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;');
177         title.addClass('filterrotate');
178         return title;
179     }
181     // We need to fix a font-size - sorry theme designers.
182     test = Y.Node.create('<h2 class="transform-test-heading"><span class="transform-test-node" style="font-size:' +
183             fontsize + ';">' + text + '</span></h2>');
184     BODY.insert(test, 0);
185     width = test.one('span').get('offsetWidth') * 1.2;
186     height = test.one('span').get('offsetHeight');
187     test.remove();
189     title.set('innerHTML', text);
190     title.addClass('css3transform');
192     // Move the title into position
193     title.setStyles({
194         'position': 'relative',
195         'fontSize': fontsize,
196         'width': width,
197         'top': (width - height) / 2
198     });
200     // Positioning is different when in RTL mode.
201     if (window.right_to_left()) {
202         title.setStyle('left', width / 2 - height);
203     } else {
204         title.setStyle('right', width / 2 - height);
205     }
207     // Rotate the text
208     title.setStyles({
209         'transform': transform,
210         '-ms-transform': transform,
211         '-moz-transform': transform,
212         '-webkit-transform': transform,
213         '-o-transform': transform
214     });
216     container = Y.Node.create('<div></div>');
217     container.append(title);
218     container.setStyles({
219         height: width + (width / 4),
220         position: 'relative'
221     });
222     return container;
223 };
225 /**
226  * Informs the dock that the content of the block has changed.
227  * This should be called by the blocks JS code if its content has been updated dynamically.
228  * This method ensure the dock resizes if need be.
229  *
230  * @static
231  * @method notifyBlockChange
232  * @param {Number} instanceid
233  * @return void
234  */
235 M.core.dock.notifyBlockChange = function(instanceid) {
236     if (this._dock !== null) {
237         var dock = M.core.dock.get(),
238             activeitem = dock.getActiveItem();
239         if (activeitem && activeitem.get('blockinstanceid') === parseInt(instanceid, 10)) {
240             dock.resizePanelIfRequired();
241         }
242     }
243 };
245 /**
246  * The Dock.
247  *
248  * @namespace M.core.dock
249  * @class Dock
250  * @constructor
251  * @extends Base
252  * @uses EventTarget
253  */
254 DOCK = function() {
255     DOCK.superclass.constructor.apply(this, arguments);
256 };
257 DOCK.prototype = {
258     /**
259      * Tab height manager used to ensure tabs are always visible.
260      * @protected
261      * @property tabheightmanager
262      * @type TABHEIGHTMANAGER
263      */
264     tabheightmanager: null,
265     /**
266      * Will be an eventtype if there is an eventype to prevent.
267      * @protected
268      * @property preventevent
269      * @type String
270      */
271     preventevent: null,
272     /**
273      * Will be an object if there is a delayed event in effect.
274      * @protected
275      * @property delayedevent
276      * @type {Object}
277      */
278     delayedevent: null,
279     /**
280      * An array of currently docked items.
281      * @protected
282      * @property dockeditems
283      * @type Array
284      */
285     dockeditems: [],
286     /**
287      * Set to true once the dock has been drawn.
288      * @protected
289      * @property dockdrawn
290      * @type Boolean
291      */
292     dockdrawn: false,
293     /**
294      * The number of blocks that are currently docked.
295      * @protected
296      * @property count
297      * @type Number
298      */
299     count: 0,
300     /**
301      * The total number of blocks that have been docked.
302      * @protected
303      * @property totalcount
304      * @type Number
305      */
306     totalcount: 0,
307     /**
308      * A hidden node used as a holding area for DOM objects used by blocks that have been docked.
309      * @protected
310      * @property holdingareanode
311      * @type Node
312      */
313     holdingareanode: null,
314     /**
315      * Called during the initialisation process of the object.
316      * @method initializer
317      */
318     initializer: function() {
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             }
496             // Now to work out what they did.
497             var key, value,
498                 warned = false,
499                 cfgmap = {
500                     buffer: 'bufferPanel',
501                     orientation: 'orientation',
502                     position: 'position',
503                     spacebeforefirstitem: 'bufferBeforeFirstItem',
504                     removeallicon: 'undockAllIconUrl'
505                 };
506             // Check for and apply any legacy configuration.
507             for (key in M.core_dock.cfg) {
508                 if (Y.Lang.isString(key) && cfgmap[key]) {
509                     value = M.core_dock.cfg[key];
510                     if (value === null) {
511                         continue;
512                     }
513                     if (!warned) {
514                         warned = true;
515                     }
516                     // Damn, the've set something.
517                     this.set(cfgmap[key], value);
518                 }
519             }
520             // Check for and apply any legacy CSS changes..
521             for (key in M.core_dock.css) {
522                 if (Y.Lang.isString(key)) {
523                     value = M.core_dock.css[key];
524                     if (value === null) {
525                         continue;
526                     }
527                     if (!warned) {
528                         warned = true;
529                     }
530                     // Damn, they've set something.
531                     CSS[key] = value;
532                 }
533             }
534         }
535     },
536     /**
537      * Initialises the dock node, creating it and its content if required.
538      *
539      * @private
540      * @method _initialiseDockNode
541      * @return {Node} The dockNode
542      */
543     _initialiseDockNode: function() {
544         var dock = this.get('dockNode'),
545             positionorientationclass = CSS.dock + '_' + this.get('position') + '_' + this.get('orientation'),
546             holdingarea = Y.Node.create('<div></div>').setStyles({display: 'none'}),
547             buttons = this.get('buttonsNode'),
548             container = this.get('itemContainerNode');
550         if (!dock) {
551             dock = Y.one('#' + CSS.dock);
552         }
553         if (!dock) {
554             dock = Y.Node.create('<div id="' + CSS.dock + '"></div>');
555             BODY.append(dock);
556         }
557         dock.setAttribute('role', 'menubar').addClass(positionorientationclass);
558         if (Y.all(SELECTOR.dockonload).size() === 0) {
559             // Nothing on the dock... hide it using CSS
560             dock.addClass('nothingdocked');
561         } else {
562             positionorientationclass = CSS.body + '_' + this.get('position') + '_' + this.get('orientation');
563             BODY.addClass(CSS.body).addClass();
564         }
566         if (!buttons) {
567             buttons = dock.one('.' + CSS.buttonscontainer);
568         }
569         if (!buttons) {
570             buttons = Y.Node.create('<div class="' + CSS.buttonscontainer + '"></div>');
571             dock.append(buttons);
572         }
574         if (!container) {
575             container = dock.one('.' + CSS.dockeditemcontainer);
576         }
577         if (!container) {
578             container = Y.Node.create('<div class="' + CSS.dockeditemcontainer + '"></div>');
579             buttons.append(container);
580         }
582         BODY.append(holdingarea);
583         this.holdingareanode = holdingarea;
585         this.set('dockNode', dock);
586         this.set('buttonsNode', buttons);
587         this.set('itemContainerNode', container);
589         return dock;
590     },
591     /**
592      * Initialises the dock controls.
593      *
594      * @private
595      * @method _initialiseDockControls
596      */
597     _initialiseDockControls: function() {
598         // Add a removeall button
599         // Must set the image src seperatly of we get an error with XML strict headers
601         var removeall = Y.Node.create('<img alt="' + M.util.get_string('undockall', 'block') + '" tabindex="0" />');
602         removeall.setAttribute('src', this.get('undockAllIconUrl'));
603         removeall.on('removeall|click', this.removeAll, this);
604         removeall.on('dock:actionkey', this.removeAll, this, {actions: {enter: true}});
605         this.get('buttonsNode').append(Y.Node.create('<div class="' + CSS.controls + '"></div>').append(removeall));
606     },
607     /**
608      * Returns the dock panel. Initialising it if it hasn't already been initialised.
609      * @method getPanel
610      * @return {DOCKPANEL}
611      */
612     getPanel: function() {
613         var panel = this.get('panel');
614         if (!panel) {
615             panel = new DOCKPANEL({dock: this});
616             panel.on('panel:visiblechange', this.resize, this);
617             Y.on('windowresize', this.resize, this);
618             // Initialise the dockpanel .. should only happen once
619             this.set('panel', panel);
620             this.fire('dock:panelgenerated');
621         }
622         return panel;
623     },
624     /**
625      * Resizes the dock panel if required.
626      * @method resizePanelIfRequired
627      */
628     resizePanelIfRequired: function() {
629         this.resize();
630         var panel = this.get('panel');
631         if (panel) {
632             panel.correctWidth();
633         }
634     },
635     /**
636      * Handles a dock event sending it to the right place.
637      *
638      * @method handleEvent
639      * @param {EventFacade} e
640      * @param {Object} options
641      * @return {Boolean}
642      */
643     handleEvent: function(e, options) {
644         var item = this.getActiveItem(),
645             target,
646             targetid,
647             regex = /^dock_item_(\d+)_title$/,
648             self = this;
649         if (options.cssselector === 'body') {
650             if (!this.get('dockNode').contains(e.target)) {
651                 if (item) {
652                     item.hide();
653                 }
654             }
655         } else {
656             if (e.target.test(options.cssselector)) {
657                 target = e.target;
658             } else {
659                 target = e.target.ancestor(options.cssselector);
660             }
661             if (!target) {
662                 return true;
663             }
664             if (this.preventevent !== null && e.type === this.preventevent) {
665                 return true;
666             }
667             if (options.preventevent) {
668                 this.preventevent = options.preventevent;
669                 if (options.preventdelay) {
670                     setTimeout(function() {
671                         self.preventevent = null;
672                     }, options.preventdelay * 1000);
673                 }
674             }
675             if (this.delayedevent && this.delayedevent.timeout) {
676                 clearTimeout(this.delayedevent.timeout);
677                 this.delayedevent.event.detach();
678                 this.delayedevent = null;
679             }
680             if (options.delay > 0) {
681                 return this.delayEvent(e, options, target);
682             }
683             targetid = target.get('id');
684             if (targetid.match(regex)) {
685                 item = this.dockeditems[targetid.replace(regex, '$1')];
686                 if (item.active) {
687                     item.hide();
688                 } else {
689                     item.show();
690                 }
691             } else if (item) {
692                 item.hide();
693             }
694         }
695         return true;
696     },
697     /**
698      * Delays an event.
699      *
700      * @method delayEvent
701      * @param {EventFacade} event
702      * @param {Object} options
703      * @param {Node} target
704      * @return {Boolean}
705      */
706     delayEvent: function(event, options, target) {
707         var self = this;
708         self.delayedevent = (function() {
709             return {
710                 target: target,
711                 event: BODY.on('mousemove', function(e) {
712                     self.delayedevent.target = e.target;
713                 }),
714                 timeout: null
715             };
716         })(self);
717         self.delayedevent.timeout = setTimeout(function() {
718             self.delayedevent.timeout = null;
719             self.delayedevent.event.detach();
720             if (options.iscontained === self.get('dockNode').contains(self.delayedevent.target)) {
721                 self.handleEvent(event, {cssselector: options.cssselector, delay: 0, iscontained: options.iscontained});
722             }
723         }, options.delay * 1000);
724         return true;
725     },
726     /**
727      * Resizes block spaces.
728      * @method resizeBlockSpace
729      */
730     resizeBlockSpace: function() {
731         if (Y.all(SELECTOR.dockonload).size() > 0) {
732             // Do not resize during initial load
733             return;
734         }
736         var populatedRegionCount = 0,
737             populatedBlockRegions = [],
738             unpopulatedBlockRegions = [],
739             isMoving = false,
740             populatedLegacyRegions = [],
741             containsLegacyRegions = false,
742             classesToAdd = [],
743             classesToRemove = [];
745         // First look for understood regions.
746         Y.all(SELECTOR.blockregion).each(function(region) {
747             var regionname = region.getData('blockregion');
748             if (region.all('.block').size() > 0) {
749                 populatedBlockRegions.push(regionname);
750                 populatedRegionCount++;
751             } else if (region.all('.block_dock_placeholder').size() > 0) {
752                 unpopulatedBlockRegions.push(regionname);
753             }
754         });
756         // Next check for legacy regions.
757         Y.all('.block-region').each(function(region) {
758             if (region.test(SELECTOR.blockregion)) {
759                 // This is a new region, we've already processed it.
760                 return;
761             }
763             // Sigh - there are legacy regions.
764             containsLegacyRegions = true;
766             var regionname = region.get('id').replace(/^region\-/, 'side-'),
767                 hasblocks = (region.all('.block').size() > 0);
769             if (hasblocks) {
770                 populatedLegacyRegions.push(regionname);
771                 populatedRegionCount++;
772             } else {
773                 // This legacy region has no blocks so cannot have the -only body tag.
774                 classesToRemove.push(
775                         regionname + '-only'
776                     );
777             }
778         });
780         if (BODY.hasClass('blocks-moving')) {
781             // When we're moving blocks, we do not want to collapse.
782             isMoving = true;
783         }
785         Y.each(unpopulatedBlockRegions, function(regionname) {
786             classesToAdd.push(
787                     // This block region is empty.
788                     'empty-region-' + regionname,
790                     // Which has the same effect as being docked.
791                     'docked-region-' + regionname
792                 );
793             classesToRemove.push(
794                     // It is no-longer used.
795                     'used-region-' + regionname,
797                     // It cannot be the only region on screen if it is empty.
798                     regionname + '-only'
799                 );
800         }, this);
802         Y.each(populatedBlockRegions, function(regionname) {
803             classesToAdd.push(
804                     // This block region is in use.
805                     'used-region-' + regionname
806                 );
807             classesToRemove.push(
808                     // It is not empty.
809                     'empty-region-' + regionname,
811                     // Is it not docked.
812                     'docked-region-' + regionname
813                 );
815             if (populatedRegionCount === 1 && isMoving === false) {
816                 // There was only one populated region, and we are not moving blocks.
817                 classesToAdd.push(regionname + '-only');
818             } else {
819                 // There were multiple block regions visible - remove any 'only' classes.
820                 classesToRemove.push(regionname + '-only');
821             }
822         }, this);
824         if (containsLegacyRegions) {
825             // Handle the classing for legacy blocks. These have slightly different class names for the body.
826             if (isMoving || populatedRegionCount !== 1) {
827                 Y.each(populatedLegacyRegions, function(regionname) {
828                     classesToRemove.push(regionname + '-only');
829                 });
830             } else {
831                 Y.each(populatedLegacyRegions, function(regionname) {
832                     classesToAdd.push(regionname + '-only');
833                 });
834             }
835         }
837         if (!BODY.hasClass('has-region-content')) {
838             // This page does not have a content region, therefore content-only is implied when all block regions are docked.
839             if (populatedRegionCount === 0 && isMoving === false) {
840                 // If all blocks are docked, ensure that the content-only class is added anyway.
841                 classesToAdd.push('content-only');
842             } else {
843                 // Otherwise remove it.
844                 classesToRemove.push('content-only');
845             }
846         }
848         // Modify the body clases.
849         Y.each(classesToRemove, function(className) {
850             BODY.removeClass(className);
851         });
852         Y.each(classesToAdd, function(className) {
853             BODY.addClass(className);
854         });
855     },
856     /**
857      * Adds an item to the dock.
858      * @method add
859      * @param {DOCKEDITEM} item
860      */
861     add: function(item) {
862         // Set the dockitem id to the total count and then increment it.
863         item.set('id', this.totalcount);
864         this.count++;
865         this.totalcount++;
866         this.dockeditems[item.get('id')] = item;
867         this.dockeditems[item.get('id')].draw();
868         this.fire('dock:itemadded', item);
869         this.fire('dock:itemschanged', item);
870     },
871     /**
872      * Appends an item to the dock (putting it in the item container.
873      * @method append
874      * @param {Node} docknode
875      */
876     append: function(docknode) {
877         this.get('itemContainerNode').append(docknode);
878     },
879     /**
880      * Handles events that require a docked block to be returned to the page./
881      * @method handleReturnToBlock
882      * @param {EventFacade} e
883      */
884     handleReturnToBlock: function(e) {
885         e.halt();
886         this.remove(this.getActiveItem().get('id'));
887     },
888     /**
889      * Removes a docked item from the dock.
890      * @method remove
891      * @param {Number} id The docked item id.
892      * @return {Boolean}
893      */
894     remove: function(id) {
895         if (!this.dockeditems[id]) {
896             return false;
897         }
898         this.dockeditems[id].remove();
899         delete this.dockeditems[id];
900         this.count--;
901         this.fire('dock:itemremoved', id);
902         this.fire('dock:itemschanged', id);
903         return true;
904     },
905     /**
906      * Ensures the the first item in the dock has the correct class.
907      * @method resetFirstItem
908      */
909     resetFirstItem: function() {
910         this.get('dockNode').all('.' + CSS.dockeditem + '.firstdockitem').removeClass('firstdockitem');
911         if (this.get('dockNode').one('.' + CSS.dockeditem)) {
912             this.get('dockNode').one('.' + CSS.dockeditem).addClass('firstdockitem');
913         }
914     },
915     /**
916      * Removes all docked blocks returning them to the page.
917      * @method removeAll
918      * @return {Boolean}
919      */
920     removeAll: function() {
921         var i;
922         for (i in this.dockeditems) {
923             if (Y.Lang.isNumber(i) || Y.Lang.isString(i)) {
924                 this.remove(i);
925             }
926         }
927         return true;
928     },
929     /**
930      * Hides the active item.
931      * @method hideActive
932      */
933     hideActive: function() {
934         var item = this.getActiveItem();
935         if (item) {
936             item.hide();
937         }
938     },
939     /**
940      * Checks wether the dock should be shown or hidden
941      * @method checkDockVisibility
942      */
943     checkDockVisibility: function() {
944         var bodyclass = CSS.body + '_' + this.get('position') + '_' + this.get('orientation');
945         if (!this.count) {
946             this.get('dockNode').addClass('nothingdocked');
947             BODY.removeClass(CSS.body).removeClass();
948             this.fire('dock:hidden');
949         } else {
950             this.fire('dock:beforeshow');
951             this.get('dockNode').removeClass('nothingdocked');
952             BODY.addClass(CSS.body).addClass(bodyclass);
953             this.fire('dock:shown');
954         }
955     },
956     /**
957      * This function checks the size and position of the panel and moves/resizes if
958      * required to keep it within the bounds of the window.
959      * @method resize
960      * @return {Boolean}
961      */
962     resize: function() {
963         var panel = this.getPanel(),
964             item = this.getActiveItem(),
965             buffer,
966             screenh,
967             docky,
968             titletop,
969             containery,
970             containerheight,
971             scrolltop,
972             panelheight,
973             dockx,
974             titleleft;
975         if (!panel.get('visible') || !item) {
976             return true;
977         }
979         this.fire('dock:panelresizestart');
980         if (this.get('orientation') === 'vertical') {
981             buffer = this.get('bufferPanel');
982             screenh = parseInt(BODY.get('winHeight'), 10) - (buffer * 2);
983             docky = this.get('dockNode').getY();
984             titletop = item.get('dockTitleNode').getY() - docky - buffer;
985             containery = this.get('itemContainerNode').getY();
986             containerheight = containery - docky + this.get('buttonsNode').get('offsetHeight');
987             scrolltop = panel.get('bodyNode').get('scrollTop');
988             panel.get('bodyNode').setStyle('height', 'auto');
989             panel.get('node').removeClass('oversized_content');
990             panelheight = panel.get('node').get('offsetHeight');
992             if (Y.UA.ie > 0 && Y.UA.ie < 7) {
993                 panel.setTop(item.get('dockTitleNode').getY());
994             } else if (panelheight > screenh) {
995                 panel.setTop(buffer - containerheight);
996                 panel.get('bodyNode').setStyle('height', (screenh - panel.get('headerNode').get('offsetHeight')) + 'px');
997                 panel.get('node').addClass('oversized_content');
998             } else if (panelheight > (screenh - (titletop - buffer))) {
999                 panel.setTop(titletop - containerheight - (panelheight - (screenh - titletop)) + buffer);
1000             } else {
1001                 panel.setTop(titletop - containerheight + buffer);
1002             }
1004             if (scrolltop) {
1005                 panel.get('bodyNode').set('scrollTop', scrolltop);
1006             }
1007         }
1009         if (this.get('position') === 'right') {
1010             panel.get('node').setStyle('left', '-' + panel.get('node').get('offsetWidth') + 'px');
1012         } else if (this.get('position') === 'top') {
1013             dockx = this.get('dockNode').getX();
1014             titleleft = item.get('dockTitleNode').getX() - dockx;
1015             panel.get('node').setStyle('left', titleleft + 'px');
1016         }
1018         this.fire('dock:resizepanelcomplete');
1019         return true;
1020     },
1021     /**
1022      * Returns the currently active dock item or false
1023      * @method getActiveItem
1024      * @return {DOCKEDITEM}
1025      */
1026     getActiveItem: function() {
1027         var i;
1028         for (i in this.dockeditems) {
1029             if (this.dockeditems[i].active) {
1030                 return this.dockeditems[i];
1031             }
1032         }
1033         return false;
1034     },
1035     /**
1036      * Adds an item to the holding area.
1037      * @method addToHoldingArea
1038      * @param {Node} node
1039      */
1040     addToHoldingArea: function(node) {
1041         this.holdingareanode.append(node);
1042     }
1043 };
1045 Y.extend(DOCK, Y.Base, DOCK.prototype, {
1046     NAME: 'moodle-core-dock',
1047     ATTRS: {
1048         /**
1049          * The dock itself. #dock.
1050          * @attribute dockNode
1051          * @type Node
1052          * @writeOnce
1053          */
1054         dockNode: {
1055             writeOnce: true
1056         },
1057         /**
1058          * The docks panel.
1059          * @attribute panel
1060          * @type DOCKPANEL
1061          * @writeOnce
1062          */
1063         panel: {
1064             writeOnce: true
1065         },
1066         /**
1067          * A container within the dock used for buttons.
1068          * @attribute buttonsNode
1069          * @type Node
1070          * @writeOnce
1071          */
1072         buttonsNode: {
1073             writeOnce: true
1074         },
1075         /**
1076          * A container within the dock used for docked blocks.
1077          * @attribute itemContainerNode
1078          * @type Node
1079          * @writeOnce
1080          */
1081         itemContainerNode: {
1082             writeOnce: true
1083         },
1085         /**
1086          * Buffer used when containing a panel.
1087          * @attribute bufferPanel
1088          * @type Number
1089          * @default 10
1090          */
1091         bufferPanel: {
1092             value: 10,
1093             validator: Y.Lang.isNumber
1094         },
1096         /**
1097          * Position of the dock.
1098          * @attribute position
1099          * @type String
1100          * @default left
1101          */
1102         position: {
1103             value: 'left',
1104             validator: Y.Lang.isString
1105         },
1107         /**
1108          * vertical || horizontal determines if we change the title
1109          * @attribute orientation
1110          * @type String
1111          * @default vertical
1112          */
1113         orientation: {
1114             value: 'vertical',
1115             validator: Y.Lang.isString,
1116             setter: function(value) {
1117                 if (value.match(/^vertical$/i)) {
1118                     return 'vertical';
1119                 }
1120                 return 'horizontal';
1121             }
1122         },
1124         /**
1125          * Space between the top of the dock and the first item.
1126          * @attribute bufferBeforeFirstItem
1127          * @type Number
1128          * @default 10
1129          */
1130         bufferBeforeFirstItem: {
1131             value: 10,
1132             validator: Y.Lang.isNumber
1133         },
1135         /**
1136          * Icon URL for the icon to undock all blocks
1137          * @attribute undockAllIconUrl
1138          * @type String
1139          * @default t/dock_to_block
1140          */
1141         undockAllIconUrl: {
1142             value: M.util.image_url((window.right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block', 'moodle'),
1143             validator: Y.Lang.isString
1144         }
1145     }
1146 });
1147 Y.augment(DOCK, Y.EventTarget);
1148 /* global DOCKPANEL, LOGNS */
1150 /**
1151  * Dock JS.
1152  *
1153  * This file contains the panel class used by the dock to display the content of docked blocks.
1154  *
1155  * @module moodle-core-dock
1156  */
1158 /**
1159  * Panel.
1160  *
1161  * @namespace M.core.dock
1162  * @class Panel
1163  * @constructor
1164  * @extends Base
1165  * @uses EventTarget
1166  */
1167 DOCKPANEL = function() {
1168     DOCKPANEL.superclass.constructor.apply(this, arguments);
1169 };
1170 DOCKPANEL.prototype = {
1171     /**
1172      * True once the panel has been created.
1173      * @property created
1174      * @protected
1175      * @type {Boolean}
1176      */
1177     created: false,
1178     /**
1179      * Called during the initialisation process of the object.
1180      * @method initializer
1181      */
1182     initializer: function() {
1183         /**
1184          * Fired before the panel is shown.
1185          * @event dockpane::beforeshow
1186          */
1187         this.publish('dockpanel:beforeshow', {prefix: 'dockpanel'});
1188         /**
1189          * Fired after the panel is shown.
1190          * @event dockpanel:shown
1191          */
1192         this.publish('dockpanel:shown', {prefix: 'dockpanel'});
1193         /**
1194          * Fired before the panel is hidden.
1195          * @event dockpane::beforehide
1196          */
1197         this.publish('dockpanel:beforehide', {prefix: 'dockpanel'});
1198         /**
1199          * Fired after the panel is hidden.
1200          * @event dockpanel:hidden
1201          */
1202         this.publish('dockpanel:hidden', {prefix: 'dockpanel'});
1203         /**
1204          * Fired when ever the dock panel is either hidden or shown.
1205          * Always fired after the shown or hidden events.
1206          * @event dockpanel:visiblechange
1207          */
1208         this.publish('dockpanel:visiblechange', {prefix: 'dockpanel'});
1209     },
1210     /**
1211      * Creates the Panel if it has not already been created.
1212      * @method create
1213      * @return {Boolean}
1214      */
1215     create: function() {
1216         if (this.created) {
1217             return true;
1218         }
1219         this.created = true;
1220         var dock = this.get('dock'),
1221             node = dock.get('dockNode');
1222         this.set('node', Y.Node.create('<div id="dockeditempanel" class="dockitempanel_hidden"></div>'));
1223         this.set('contentNode', Y.Node.create('<div class="dockeditempanel_content"></div>'));
1224         this.set('headerNode', Y.Node.create('<div class="dockeditempanel_hd"></div>'));
1225         this.set('bodyNode', Y.Node.create('<div class="dockeditempanel_bd"></div>'));
1226         node.append(
1227             this.get('node').append(this.get('contentNode').append(this.get('headerNode')).append(this.get('bodyNode')))
1228         );
1229     },
1230     /**
1231      * Displays the panel.
1232      * @method show
1233      */
1234     show: function() {
1235         this.create();
1236         this.fire('dockpanel:beforeshow');
1237         this.set('visible', true);
1238         this.get('node').removeClass('dockitempanel_hidden');
1239         this.fire('dockpanel:shown');
1240         this.fire('dockpanel:visiblechange');
1241     },
1242     /**
1243      * Hides the panel
1244      * @method hide
1245      */
1246     hide: function() {
1247         this.fire('dockpanel:beforehide');
1248         this.set('visible', false);
1249         this.get('node').addClass('dockitempanel_hidden');
1250         this.fire('dockpanel:hidden');
1251         this.fire('dockpanel:visiblechange');
1252     },
1253     /**
1254      * Sets the panel header.
1255      * @method setHeader
1256      * @param {Node|String} content
1257      */
1258     setHeader: function(content) {
1259         this.create();
1260         var header = this.get('headerNode'),
1261             i;
1262         header.setContent(content);
1263         if (arguments.length > 1) {
1264             for (i = 1; i < arguments.length; i++) {
1265                 if (Y.Lang.isNumber(i) || Y.Lang.isString(i)) {
1266                     header.append(arguments[i]);
1267                 }
1268             }
1269         }
1270     },
1271     /**
1272      * Sets the panel body.
1273      * @method setBody
1274      * @param {Node|String} content
1275      */
1276     setBody: function(content) {
1277         this.create();
1278         this.get('bodyNode').setContent(content);
1279     },
1280     /**
1281      * Sets the new top mark of the panel.
1282      *
1283      * @method setTop
1284      * @param {Number} newtop
1285      */
1286     setTop: function(newtop) {
1287         if (Y.UA.ie > 0 && Y.UA.ie < 7) {
1288             this.get('node').setY(newtop);
1289         } else {
1290             this.get('node').setStyle('top', newtop.toString() + 'px');
1291         }
1292     },
1293     /**
1294      * Corrects the width of the panel.
1295      * @method correctWidth
1296      */
1297     correctWidth: function() {
1298         var bodyNode = this.get('bodyNode'),
1299             // Width of content.
1300             width = bodyNode.get('clientWidth'),
1301             // Scrollable width of content.
1302             scroll = bodyNode.get('scrollWidth'),
1303             // Width of content container with overflow.
1304             offsetWidth = bodyNode.get('offsetWidth'),
1305             // The new width - defaults to the current width.
1306             newWidth = width,
1307             // The max width (80% of screen).
1308             maxWidth = Math.round(bodyNode.get('winWidth') * 0.8);
1310         // If the scrollable width is more than the visible width
1311         if (scroll > width) {
1312             //   Content width
1313             // + the difference
1314             // + any rendering difference (borders, padding)
1315             // + 10px to make it look nice.
1316             newWidth = width + (scroll - width) + ((offsetWidth - width) * 2) + 10;
1317         }
1319         // Make sure its not more then the maxwidth
1320         if (newWidth > maxWidth) {
1321             newWidth = maxWidth;
1322         }
1324         // Set the new width if its more than the old width.
1325         if (newWidth > offsetWidth) {
1326             this.get('node').setStyle('width', newWidth + 'px');
1327         }
1328     }
1329 };
1330 Y.extend(DOCKPANEL, Y.Base, DOCKPANEL.prototype, {
1331     NAME: 'moodle-core-dock-panel',
1332     ATTRS: {
1333         /**
1334          * The dock itself.
1335          * @attribute dock
1336          * @type DOCK
1337          * @writeonce
1338          */
1339         dock: {
1340             writeOnce: 'initOnly'
1341         },
1342         /**
1343          * The node that contains the whole panel.
1344          * @attribute node
1345          * @type Node
1346          */
1347         node: {
1348             value: null
1349         },
1350         /**
1351          * The node that contains the header, body and footer.
1352          * @attribute contentNode
1353          * @type Node
1354          */
1355         contentNode: {
1356             value: null
1357         },
1358         /**
1359          * The node that contains the header
1360          * @attribute headerNode
1361          * @type Node
1362          */
1363         headerNode: {
1364             value: null
1365         },
1366         /**
1367          * The node that contains the body
1368          * @attribute bodyNode
1369          * @type Node
1370          */
1371         bodyNode: {
1372             value: null
1373         },
1374         /**
1375          * True if the panel is currently visible.
1376          * @attribute visible
1377          * @type Boolean
1378          */
1379         visible: {
1380             value: false
1381         }
1382     }
1383 });
1384 Y.augment(DOCKPANEL, Y.EventTarget);
1385 /* global TABHEIGHTMANAGER, LOGNS */
1387 /**
1388  * Dock JS.
1389  *
1390  * This file contains the tab height manager.
1391  * The tab height manager is responsible for ensure all tabs are visible all the time.
1392  *
1393  * @module moodle-core-dock
1394  */
1396 /**
1397  * Tab height manager.
1398  *
1399  * @namespace M.core.dock
1400  * @class TabHeightManager
1401  * @constructor
1402  * @extends Base
1403  */
1404 TABHEIGHTMANAGER = function() {
1405     TABHEIGHTMANAGER.superclass.constructor.apply(this, arguments);
1406 };
1407 TABHEIGHTMANAGER.prototype = {
1408     /**
1409      * Initialises the dock sizer which then attaches itself to the required
1410      * events in order to monitor the dock
1411      * @method initializer
1412      */
1413     initializer: function() {
1414         var dock = this.get('dock');
1415         dock.on('dock:itemschanged', this.checkSizing, this);
1416         Y.on('windowresize', this.checkSizing, this);
1417     },
1418     /**
1419      * Check if the size dock items needs to be adjusted
1420      * @method checkSizing
1421      */
1422     checkSizing: function() {
1423         var dock = this.get('dock'),
1424             node = dock.get('dockNode'),
1425             items = dock.dockeditems,
1426             containermargin = parseInt(node.one('.dockeditem_container').getStyle('marginTop').replace('/[^0-9]+$/', ''), 10),
1427             dockheight = node.get('offsetHeight') - containermargin,
1428             controlheight = node.one('.controls').get('offsetHeight'),
1429             buffer = (dock.get('bufferPanel') * 3),
1430             possibleheight = dockheight - controlheight - buffer - (items.length * 2),
1431             totalheight = 0,
1432             id, dockedtitle;
1433         if (items.length > 0) {
1434             for (id in items) {
1435                 if (Y.Lang.isNumber(id) || Y.Lang.isString(id)) {
1436                     dockedtitle = Y.one(items[id].get('title')).ancestor('.' + CSS.dockedtitle);
1437                     if (dockedtitle) {
1438                         if (this.get('enabled')) {
1439                             dockedtitle.setStyle('height', 'auto');
1440                         }
1441                         totalheight += dockedtitle.get('offsetHeight') || 0;
1442                     }
1443                 }
1444             }
1445             if (totalheight > possibleheight) {
1446                 this.enable(possibleheight);
1447             }
1448         }
1449     },
1450     /**
1451      * Enables the dock sizer and resizes where required.
1452      * @method enable
1453      * @param {Number} possibleheight
1454      */
1455     enable: function(possibleheight) {
1456         var dock = this.get('dock'),
1457             items = dock.dockeditems,
1458             count = dock.count,
1459             runningcount = 0,
1460             usedheight = 0,
1461             id, itemtitle, itemheight, offsetheight;
1462         this.set('enabled', true);
1463         for (id in items) {
1464             if (Y.Lang.isNumber(id) || Y.Lang.isString(id)) {
1465                 itemtitle = Y.one(items[id].get('title')).ancestor('.' + CSS.dockedtitle);
1466                 if (!itemtitle) {
1467                     continue;
1468                 }
1469                 itemheight = Math.floor((possibleheight - usedheight) / (count - runningcount));
1470                 offsetheight = itemtitle.get('offsetHeight');
1471                 itemtitle.setStyle('overflow', 'hidden');
1472                 if (offsetheight > itemheight) {
1473                     itemtitle.setStyle('height', itemheight + 'px');
1474                     usedheight += itemheight;
1475                 } else {
1476                     usedheight += offsetheight;
1477                 }
1478                 runningcount++;
1479             }
1480         }
1481     }
1482 };
1483 Y.extend(TABHEIGHTMANAGER, Y.Base, TABHEIGHTMANAGER.prototype, {
1484     NAME: 'moodle-core-tabheightmanager',
1485     ATTRS: {
1486         /**
1487          * The dock.
1488          * @attribute dock
1489          * @type DOCK
1490          * @writeOnce
1491          */
1492         dock: {
1493             writeOnce: 'initOnly'
1494         },
1495         /**
1496          * True if the item_sizer is being used, false otherwise.
1497          * @attribute enabled
1498          * @type Bool
1499          */
1500         enabled: {
1501             value: false
1502         }
1503     }
1504 });
1505 /**
1506  * Dock JS.
1507  *
1508  * This file contains the action key event definition that is used for accessibility handling within the Dock.
1509  *
1510  * @module moodle-core-dock
1511  */
1513 /**
1514  * A 'dock:actionkey' Event.
1515  * The event consists of the left arrow, right arrow, enter and space keys.
1516  * More keys can be mapped to action meanings.
1517  * actions: collapse , expand, toggle, enter.
1518  *
1519  * This event is subscribed to by dockitems.
1520  * The on() method to subscribe allows specifying the desired trigger actions as JSON.
1521  *
1522  * This event can also be delegated if needed.
1523  *
1524  * @namespace M.core.dock
1525  * @class ActionKey
1526  */
1527 Y.Event.define("dock:actionkey", {
1528     // Webkit and IE repeat keydown when you hold down arrow keys.
1529     // Opera links keypress to page scroll; others keydown.
1530     // Firefox prevents page scroll via preventDefault() on either
1531     // keydown or keypress.
1532     _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
1534     /**
1535      * The keys to trigger on.
1536      * @property _keys
1537      */
1538     _keys: {
1539         // arrows
1540         '37': 'collapse',
1541         '39': 'expand',
1542         // (@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings)
1543         '32': 'toggle',
1544         '13': 'enter'
1545     },
1547     /**
1548      * Handles key events
1549      * @method _keyHandler
1550      * @param {EventFacade} e
1551      * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
1552      * @param {Object} args
1553      */
1554     _keyHandler: function(e, notifier, args) {
1555         var actObj;
1556         if (!args.actions) {
1557             actObj = {collapse: true, expand: true, toggle: true, enter: true};
1558         } else {
1559             actObj = args.actions;
1560         }
1561         if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) {
1562             e.action = this._keys[e.keyCode];
1563             notifier.fire(e);
1564         }
1565     },
1567     /**
1568      * Subscribes to events.
1569      * @method on
1570      * @param {Node} node The node this subscription was applied to.
1571      * @param {Subscription} sub The object tracking this subscription.
1572      * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
1573      */
1574     on: function(node, sub, notifier) {
1575         // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
1576         if (sub.args === null) {
1577             // no actions given
1578             sub._detacher = node.on(this._event, this._keyHandler, this, notifier, {actions: false});
1579         } else {
1580             sub._detacher = node.on(this._event, this._keyHandler, this, notifier, sub.args[0]);
1581         }
1582     },
1584     /**
1585      * Detaches an event listener
1586      * @method detach
1587      * @param {Node} node The node this subscription was applied to.
1588      * @param {Subscription} sub The object tracking this subscription.
1589      * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
1590      */
1591     detach: function(node, sub) {
1592         // detach our _detacher handle of the subscription made in on()
1593         sub._detacher.detach();
1594     },
1596     /**
1597      * Creates a delegated event listener.
1598      * @method delegate
1599      * @param {Node} node The node this subscription was applied to.
1600      * @param {Subscription} sub The object tracking this subscription.
1601      * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
1602      * @param {String|function} filter Selector string or function that accpets an event object and returns null.
1603      */
1604     delegate: function(node, sub, notifier, filter) {
1605         // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
1606         if (sub.args === null) {
1607             // no actions given
1608             sub._delegateDetacher = node.delegate(this._event, this._keyHandler, filter, this, notifier, {actions: false});
1609         } else {
1610             sub._delegateDetacher = node.delegate(this._event, this._keyHandler, filter, this, notifier, sub.args[0]);
1611         }
1612     },
1614     /**
1615      * Detaches a delegated event listener.
1616      * @method detachDelegate
1617      * @param {Node} node The node this subscription was applied to.
1618      * @param {Subscription} sub The object tracking this subscription.
1619      * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
1620      * @param {String|function} filter Selector string or function that accpets an event object and returns null.
1621      */
1622     detachDelegate: function(node, sub) {
1623         sub._delegateDetacher.detach();
1624     }
1625 });
1626 /* global BLOCK, LOGNS, DOCKEDITEM */
1628 /**
1629  * Dock JS.
1630  *
1631  * This file contains the block class used to manage blocks (both docked and not) for the dock.
1632  *
1633  * @module moodle-core-dock
1634  */
1636 /**
1637  * Block.
1638  *
1639  * @namespace M.core.dock
1640  * @class Block
1641  * @constructor
1642  * @extends Base
1643  */
1644 BLOCK = function() {
1645     BLOCK.superclass.constructor.apply(this, arguments);
1646 };
1647 BLOCK.prototype = {
1648     /**
1649      * A content place holder used when the block has been docked.
1650      * @property contentplaceholder
1651      * @protected
1652      * @type Node
1653      */
1654     contentplaceholder: null,
1655     /**
1656      * The skip link associated with this block.
1657      * @property contentskipanchor
1658      * @protected
1659      * @type Node
1660      */
1661     contentskipanchor: null,
1662     /**
1663      * The cached content node for the actual block
1664      * @property cachedcontentnode
1665      * @protected
1666      * @type Node
1667      */
1668     cachedcontentnode: null,
1669     /**
1670      * If true the user preference isn't updated
1671      * @property skipsetposition
1672      * @protected
1673      * @type Boolean
1674      */
1675     skipsetposition: true,
1676     /**
1677      * The dock item associated with this block
1678      * @property dockitem
1679      * @protected
1680      * @type DOCKEDITEM
1681      */
1682     dockitem: null,
1683     /**
1684      * Called during the initialisation process of the object.
1685      * @method initializer
1686      */
1687     initializer: function() {
1688         var node = Y.one('#inst' + this.get('id'));
1689         if (!node) {
1690             return false;
1691         }
1694         M.core.dock.ensureMoveToIconExists(node);
1696         // Move the block straight to the dock if required
1697         if (node.hasClass(CSS.dockonload)) {
1698             node.removeClass(CSS.dockonload);
1699             this.moveToDock();
1700         }
1701         this.skipsetposition = false;
1702         return true;
1703     },
1704     /**
1705      * Returns the class associated with this block.
1706      * @method _getBlockClass
1707      * @private
1708      * @param {Node} node
1709      * @return String
1710      */
1711     _getBlockClass: function(node) {
1712         var block = node.getData('block'),
1713             classes,
1714             matches;
1715         if (Y.Lang.isString(block) && block !== '') {
1716             return block;
1717         }
1718         classes = node.getAttribute('className').toString();
1719         matches = /(^| )block_([^ ]+)/.exec(classes);
1720         if (matches) {
1721             return matches[2];
1722         }
1723         return matches;
1724     },
1726     /**
1727      * This function is responsible for moving a block from the page structure onto the dock.
1728      * @method moveToDock
1729      * @param {EventFacade} e
1730      */
1731     moveToDock: function(e) {
1732         if (e) {
1733             e.halt(true);
1734         }
1736         var dock = M.core.dock.get(),
1737             id = this.get('id'),
1738             blockcontent = Y.one('#inst' + id).one('.content'),
1739             icon = (window.right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block',
1740             breakchar = (location.href.match(/\?/)) ? '&' : '?',
1741             blocktitle,
1742             blockcommands,
1743             movetoimg,
1744             moveto;
1746         if (!blockcontent) {
1747             return;
1748         }
1751         this.recordBlockState();
1753         blocktitle = this.cachedcontentnode.one('.title h2').cloneNode(true);
1755         // Build up the block commands.
1756         // These should not actually added to the DOM.
1757         blockcommands = this.cachedcontentnode.one('.title .commands');
1758         if (blockcommands) {
1759             blockcommands = blockcommands.cloneNode(true);
1760         } else {
1761             blockcommands = Y.Node.create('<div class="commands"></div>');
1762         }
1763         movetoimg = Y.Node.create('<img />').setAttrs({
1764             alt: Y.Escape.html(M.util.get_string('undockitem', 'block')),
1765             title: Y.Escape.html(M.util.get_string('undockblock', 'block', blocktitle.get('innerHTML'))),
1766             src: M.util.image_url(icon, 'moodle')
1767         });
1768         moveto = Y.Node.create('<a class="moveto customcommand requiresjs"></a>').setAttrs({
1769             href: Y.config.win.location.href + breakchar + 'dock=' + id
1770         });
1771         moveto.append(movetoimg);
1772         blockcommands.append(moveto.append(movetoimg));
1774         // Create a new dock item for the block
1775         this.dockitem = new DOCKEDITEM({
1776             block: this,
1777             dock: dock,
1778             blockinstanceid: id,
1779             title: blocktitle,
1780             contents: blockcontent,
1781             commands: blockcommands,
1782             blockclass: this._getBlockClass(Y.one('#inst' + id))
1783         });
1784         // Register an event so that when it is removed we can put it back as a block
1785         dock.add(this.dockitem);
1787         if (!this.skipsetposition) {
1788             // save the users preference
1789             M.util.set_user_preference('docked_block_instance_' + id, 1);
1790         }
1792         this.set('isDocked', true);
1793     },
1794     /**
1795      * Records the block state and adds it to the docks holding area.
1796      * @method recordBlockState
1797      */
1798     recordBlockState: function() {
1799         var id = this.get('id'),
1800             dock = M.core.dock.get(),
1801             node = Y.one('#inst' + id),
1802             skipanchor = node.previous();
1803         // Disable the skip anchor when docking
1804         if (skipanchor.hasClass('skip-block')) {
1805             this.contentskipanchor = skipanchor;
1806             this.contentskipanchor.hide();
1807         }
1808         this.cachedcontentnode = node;
1809         this.contentplaceholder = Y.Node.create('<div class="block_dock_placeholder"></div>');
1810         node.replace(this.contentplaceholder);
1811         dock.addToHoldingArea(node);
1812         node = null;
1813     },
1815     /**
1816      * This function removes a block from the dock and puts it back into the page structure.
1817      * @method returnToPage
1818      * @return {Boolean}
1819      */
1820     returnToPage: function() {
1821         var id = this.get('id');
1824         // Enable the skip anchor when going back to block mode
1825         if (this.contentskipanchor) {
1826             this.contentskipanchor.show();
1827         }
1829         if (this.cachedcontentnode.one('.header')) {
1830             this.cachedcontentnode.one('.header').insert(this.dockitem.get('contents'), 'after');
1831         } else {
1832             this.cachedcontentnode.insert(this.dockitem.get('contents'));
1833         }
1835         this.contentplaceholder.replace(this.cachedcontentnode);
1836         this.cachedcontentnode = null;
1838         M.util.set_user_preference('docked_block_instance_' + id, 0);
1839         this.set('isDocked', false);
1840         return true;
1841     }
1842 };
1843 Y.extend(BLOCK, Y.Base, BLOCK.prototype, {
1844     NAME: 'moodle-core-dock-block',
1845     ATTRS: {
1846         /**
1847          * The block instance ID
1848          * @attribute id
1849          * @writeOnce
1850          * @type Number
1851          */
1852         id: {
1853             writeOnce: 'initOnly',
1854             setter: function(value) {
1855                 return parseInt(value, 10);
1856             }
1857         },
1858         /**
1859          * True if the block has been docked.
1860          * @attribute isDocked
1861          * @default false
1862          * @type Boolean
1863          */
1864         isDocked: {
1865             value: false
1866         }
1867     }
1868 });
1869 /* global LOGNS, DOCKEDITEM */
1871 /**
1872  * Dock JS.
1873  *
1874  * This file contains the docked item class.
1875  *
1876  * @module moodle-core-dock
1877  */
1879 /**
1880  * Docked item.
1881  *
1882  * @namespace M.core.dock
1883  * @class DockedItem
1884  * @constructor
1885  * @extends Base
1886  * @uses EventTarget
1887  */
1888 DOCKEDITEM = function() {
1889     DOCKEDITEM.superclass.constructor.apply(this, arguments);
1890 };
1891 DOCKEDITEM.prototype = {
1892     /**
1893      * Set to true if this item is currently being displayed.
1894      * @property active
1895      * @protected
1896      * @type Boolean
1897      */
1898     active: false,
1899     /**
1900      * Called during the initialisation process of the object.
1901      * @method initializer
1902      */
1903     initializer: function() {
1904         var title = this.get('title'),
1905             titlestring,
1906             type;
1907         /**
1908          * Fired before the docked item has been drawn.
1909          * @event dockeditem:drawstart
1910          */
1911         this.publish('dockeditem:drawstart', {prefix: 'dockeditem'});
1912         /**
1913          * Fired after the docked item has been drawn.
1914          * @event dockeditem:drawcomplete
1915          */
1916         this.publish('dockeditem:drawcomplete', {prefix: 'dockeditem'});
1917         /**
1918          * Fired before the docked item is to be shown.
1919          * @event dockeditem:showstart
1920          */
1921         this.publish('dockeditem:showstart', {prefix: 'dockeditem'});
1922         /**
1923          * Fired after the docked item has been shown.
1924          * @event dockeditem:showcomplete
1925          */
1926         this.publish('dockeditem:showcomplete', {prefix: 'dockeditem'});
1927         /**
1928          * Fired before the docked item has been hidden.
1929          * @event dockeditem:hidestart
1930          */
1931         this.publish('dockeditem:hidestart', {prefix: 'dockeditem'});
1932         /**
1933          * Fired after the docked item has been hidden.
1934          * @event dockeditem:hidecomplete
1935          */
1936         this.publish('dockeditem:hidecomplete', {prefix: 'dockeditem'});
1937         /**
1938          * Fired when the docked item is removed from the dock.
1939          * @event dockeditem:itemremoved
1940          */
1941         this.publish('dockeditem:itemremoved', {prefix: 'dockeditem'});
1942         if (title) {
1943             type = title.get('nodeName');
1944             titlestring = title.cloneNode(true);
1945             title = Y.Node.create('<' + type + '></' + type + '>');
1946             title = M.core.dock.fixTitleOrientation(title, titlestring.get('text'));
1947             this.set('title', title);
1948             this.set('titlestring', titlestring);
1949         }
1950     },
1951     /**
1952      * This function draws the item on the dock.
1953      * @method draw
1954      * @return Boolean
1955      */
1956     draw: function() {
1957         var create = Y.Node.create,
1958             dock = this.get('dock'),
1959             count = dock.count,
1960             docktitle,
1961             dockitem,
1962             closeicon,
1963             closeiconimg,
1964             id = this.get('id');
1966         this.fire('dockeditem:drawstart');
1968         docktitle = create('<div id="dock_item_' + id + '_title" role="menu" aria-haspopup="true" class="'
1969             + CSS.dockedtitle + '"></div>');
1970         docktitle.append(this.get('title'));
1971         dockitem = create('<div id="dock_item_' + id + '" class="' + CSS.dockeditem + '" tabindex="0" rel="' + id + '"></div>');
1972         if (count === 1) {
1973             dockitem.addClass('firstdockitem');
1974         }
1975         dockitem.append(docktitle);
1976         dock.append(dockitem);
1978         closeiconimg = create('<img alt="' + M.util.get_string('hidepanel', 'block') +
1979                 '" title="' + M.util.get_string('hidedockpanel', 'block') + '" />');
1980         closeiconimg.setAttribute('src', M.util.image_url('t/dockclose', 'moodle'));
1981         closeicon = create('<span class="hidepanelicon" tabindex="0"></span>').append(closeiconimg);
1982         closeicon.on('forceclose|click', this.hide, this);
1983         closeicon.on('dock:actionkey', this.hide, this, {actions: {enter: true, toggle: true}});
1984         this.get('commands').append(closeicon);
1986         this.set('dockTitleNode', docktitle);
1987         this.set('dockItemNode', dockitem);
1989         this.fire('dockeditem:drawcomplete');
1990         return true;
1991     },
1992     /**
1993      * This function toggles makes the item active and shows it.
1994      * @method show
1995      * @return Boolean
1996      */
1997     show: function() {
1998         var dock = this.get('dock'),
1999             panel = dock.getPanel(),
2000             docktitle = this.get('dockTitleNode');
2002         dock.hideActive();
2003         this.fire('dockeditem:showstart');
2004         panel.setHeader(this.get('titlestring'), this.get('commands'));
2005         panel.setBody(Y.Node.create('<div class="block_' + this.get('blockclass') + ' block_docked"></div>')
2006              .append(this.get('contents')));
2007         if (M.core.actionmenu !== undefined) {
2008             M.core.actionmenu.newDOMNode(panel.get('node'));
2009         }
2010         panel.show();
2011         panel.correctWidth();
2013         this.active = true;
2014         // Add active item class first up
2015         docktitle.addClass(CSS.activeitem);
2016         // Set aria-exapanded property to true.
2017         docktitle.set('aria-expanded', "true");
2018         this.fire('dockeditem:showcomplete');
2019         dock.resize();
2020         return true;
2021     },
2022     /**
2023      * This function hides the item and makes it inactive.
2024      * @method hide
2025      */
2026     hide: function() {
2027         this.fire('dockeditem:hidestart');
2028         if (this.active) {
2029             // No longer active
2030             this.active = false;
2031             // Hide the panel
2032             this.get('dock').getPanel().hide();
2033         }
2034         // Remove the active class
2035         // Set aria-exapanded property to false
2036         this.get('dockTitleNode').removeClass(CSS.activeitem).set('aria-expanded', "false");
2037         this.fire('dockeditem:hidecomplete');
2038     },
2039     /**
2040      * A toggle between calling show and hide functions based on css.activeitem
2041      * Applies rules to key press events (dock:actionkey)
2042      * @method toggle
2043      * @param {String} action
2044      */
2045     toggle: function(action) {
2046         var docktitle = this.get('dockTitleNode');
2047         if (docktitle.hasClass(CSS.activeitem) && action !== 'expand') {
2048             this.hide();
2049         } else if (!docktitle.hasClass(CSS.activeitem) && action !== 'collapse') {
2050             this.show();
2051         }
2052     },
2053     /**
2054      * This function removes the node and destroys it's bits.
2055      * @method remove.
2056      */
2057     remove: function() {
2058         this.hide();
2059         // Return the block to its original position.
2060         this.get('block').returnToPage();
2061         // Remove the dock item node.
2062         this.get('dockItemNode').remove();
2063         this.fire('dockeditem:itemremoved');
2064     },
2065     /**
2066      * Returns the description of this item to use for log calls.
2067      * @method _getLogDescription
2068      * @private
2069      * @return {String}
2070      */
2071     _getLogDescription: function() {
2072         return this.get('titlestring').get('innerHTML') + ' (' + this.get('blockinstanceid') + ')';
2073     }
2074 };
2075 Y.extend(DOCKEDITEM, Y.Base, DOCKEDITEM.prototype, {
2076     NAME: 'moodle-core-dock-dockeditem',
2077     ATTRS: {
2078         /**
2079          * The block this docked item is associated with.
2080          * @attribute block
2081          * @type BLOCK
2082          * @writeOnce
2083          * @required
2084          */
2085         block: {
2086             writeOnce: 'initOnly'
2087         },
2088         /**
2089          * The dock itself.
2090          * @attribute dock
2091          * @type DOCK
2092          * @writeOnce
2093          * @required
2094          */
2095         dock: {
2096             writeOnce: 'initOnly'
2097         },
2098         /**
2099          * The docked item ID. This will be given by the dock.
2100          * @attribute id
2101          * @type Number
2102          */
2103         id: {},
2104         /**
2105          * Block instance id.Taken from the associated block.
2106          * @attribute blockinstanceid
2107          * @type Number
2108          * @writeOnce
2109          */
2110         blockinstanceid: {
2111             writeOnce: 'initOnly',
2112             setter: function(value) {
2113                 return parseInt(value, 10);
2114             }
2115         },
2116         /**
2117          * The title  nodeof the docked item.
2118          * @attribute title
2119          * @type Node
2120          * @default null
2121          */
2122         title: {
2123             value: null
2124         },
2125         /**
2126          * The title string.
2127          * @attribute titlestring
2128          * @type String
2129          */
2130         titlestring: {
2131             value: null
2132         },
2133         /**
2134          * The contents of the docked item
2135          * @attribute contents
2136          * @type Node
2137          * @writeOnce
2138          * @required
2139          */
2140         contents: {
2141             writeOnce: 'initOnly'
2142         },
2143         /**
2144          * Commands associated with the block.
2145          * @attribute commands
2146          * @type Node
2147          * @writeOnce
2148          * @required
2149          */
2150         commands: {
2151             writeOnce: 'initOnly'
2152         },
2153         /**
2154          * The block class.
2155          * @attribute blockclass
2156          * @type String
2157          * @writeOnce
2158          * @required
2159          */
2160         blockclass: {
2161             writeOnce: 'initOnly'
2162         },
2163         /**
2164          * The title node for the docked block.
2165          * @attribute dockTitleNode
2166          * @type Node
2167          */
2168         dockTitleNode: {
2169             value: null
2170         },
2171         /**
2172          * The item node for the docked block.
2173          * @attribute dockItemNode
2174          * @type Node
2175          */
2176         dockItemNode: {
2177             value: null
2178         },
2179         /**
2180          * The container for the docked item (will contain the block contents when visible)
2181          * @attribute dockcontainerNode
2182          * @type Node
2183          */
2184         dockcontainerNode: {
2185             value: null
2186         }
2187     }
2188 });
2189 Y.augment(DOCKEDITEM, Y.EventTarget);
2192 }, '@VERSION@', {
2193     "requires": [
2194         "base",
2195         "node",
2196         "event-custom",
2197         "event-mouseenter",
2198         "event-resize",
2199         "escape",
2200         "moodle-core-dock-loader",
2201         "moodle-core-event"
2202     ]
2203 });