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