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