MDL-38788 behat: Generic form elements interaction compatibile with JS disabled
[moodle.git] / blocks / dock.js
CommitLineData
1ce15fda
SH
1/**
2 * The dock namespace: Contains all things dock related
3 * @namespace
4 */
53fc3e70 5M.core_dock = {
7e4617f7
SH
6 count : 0, // The number of dock items currently
7 totalcount : 0, // The number of dock items through the page life
8 items : [], // An array of dock items
9 earlybinds : [], // Events added before the dock was augmented to support events
10 Y : null, // The YUI instance to use with dock related code
11 initialised : false, // True once thedock has been initialised
12 delayedevent : null, // Will be an object if there is a delayed event in effect
673835d4
SH
13 preventevent : null, // Will be an eventtype if there is an eventyoe to prevent
14 holdingarea : null
edc858dc 15};
7e4617f7
SH
16/**
17 * Namespace containing the nodes that relate to the dock
18 * @namespace
19 */
20M.core_dock.nodes = {
21 dock : null, // The dock itself
22 body : null, // The body of the page
23 panel : null // The docks panel
edc858dc 24};
7e4617f7
SH
25/**
26 * Configuration parameters used during the initialisation and setup
27 * of dock and dock items.
28 * This is here specifically so that themers can override core parameters and
29 * design aspects without having to re-write navigation
30 * @namespace
31 */
32M.core_dock.cfg = {
33 buffer:10, // Buffer used when containing a panel
34 position:'left', // position of the dock
35 orientation:'vertical', // vertical || horizontal determines if we change the title
36 spacebeforefirstitem: 10, // Space between the top of the dock and the first item
37 removeallicon: M.util.image_url('t/dock_to_block', 'moodle')
edc858dc 38};
7e4617f7
SH
39/**
40 * CSS classes to use with the dock
41 * @namespace
42 */
43M.core_dock.css = {
44 dock:'dock', // CSS Class applied to the dock box
45 dockspacer:'dockspacer', // CSS class applied to the dockspacer
46 controls:'controls', // CSS class applied to the controls box
47 body:'has_dock', // CSS class added to the body when there is a dock
827c319a 48 buttonscontainer: 'buttons_container',
7e4617f7
SH
49 dockeditem:'dockeditem', // CSS class added to each item in the dock
50 dockeditemcontainer:'dockeditem_container',
51 dockedtitle:'dockedtitle', // CSS class added to the item's title in each dock
52 activeitem:'activeitem' // CSS class added to the active item
edc858dc 53};
7e4617f7
SH
54/**
55 * Augments the classes as required and processes early bindings
56 */
57M.core_dock.init = function(Y) {
58 if (this.initialised) {
1ce15fda 59 return true;
7e4617f7
SH
60 }
61 var css = this.css;
62 this.initialised = true;
63 this.Y = Y;
64 this.nodes.body = Y.one(document.body);
65
66 // Give the dock item class the event properties/methods
67 Y.augment(this.item, Y.EventTarget);
68 Y.augment(this, Y.EventTarget, true);
eb25449a 69 /**
0193d9df 70 * A 'dock:actionkey' Event.
eb25449a 71 * The event consists of the left arrow, right arrow, enter and space keys.
0193d9df
AB
72 * More keys can be mapped to action meanings.
73 * actions: collapse , expand, toggle, enter.
74 *
75 * This event is subscribed to by dockitems.
76 * The on() method to subscribe allows specifying the desired trigger actions as JSON.
77 *
78 * This event can also be delegated if needed.
eb25449a
AB
79 * Todo: This could be centralised, a similar Event is defined in blocks/navigation/yui/navigation/navigation.js
80 */
81 Y.Event.define("dock:actionkey", {
82 // Webkit and IE repeat keydown when you hold down arrow keys.
83 // Opera links keypress to page scroll; others keydown.
84 // Firefox prevents page scroll via preventDefault() on either
85 // keydown or keypress.
86 _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
87
88 _keys: {
89 //arrows
90 '37': 'collapse',
91 '39': 'expand',
92 //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings)
93 '32': 'toggle',
94 '13': 'enter'
95 },
96
0193d9df
AB
97 _keyHandler: function (e, notifier, args) {
98 if (!args.actions) {
99 var actObj = {collapse:true, expand:true, toggle:true, enter:true};
100 } else {
101 var actObj = args.actions;
102 }
103 if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) {
eb25449a
AB
104 e.action = this._keys[e.keyCode];
105 notifier.fire(e);
106 }
107 },
108
109 on: function (node, sub, notifier) {
0193d9df
AB
110 // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
111 if (sub.args == null) {
112 //no actions given
113 sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false});
114 } else {
115 sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]);
116 }
eb25449a 117 },
7e4617f7 118
eb25449a 119 detach: function (node, sub, notifier) {
0193d9df 120 //detach our _detacher handle of the subscription made in on()
eb25449a
AB
121 sub._detacher.detach();
122 },
123
124 delegate: function (node, sub, notifier, filter) {
0193d9df
AB
125 // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
126 if (sub.args == null) {
127 //no actions given
128 sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false});
129 } else {
130 sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]);
131 }
eb25449a
AB
132 },
133
134 detachDelegate: function (node, sub, notifier) {
135 sub._delegateDetacher.detach();
136 }
137 });
7e4617f7
SH
138 // Publish the events the dock has
139 this.publish('dock:beforedraw', {prefix:'dock'});
140 this.publish('dock:beforeshow', {prefix:'dock'});
141 this.publish('dock:shown', {prefix:'dock'});
142 this.publish('dock:hidden', {prefix:'dock'});
143 this.publish('dock:initialised', {prefix:'dock'});
144 this.publish('dock:itemadded', {prefix:'dock'});
145 this.publish('dock:itemremoved', {prefix:'dock'});
146 this.publish('dock:itemschanged', {prefix:'dock'});
147 this.publish('dock:panelgenerated', {prefix:'dock'});
148 this.publish('dock:panelresizestart', {prefix:'dock'});
149 this.publish('dock:resizepanelcomplete', {prefix:'dock'});
150 this.publish('dock:starting', {prefix: 'dock',broadcast: 2,emitFacade: true});
151 this.fire('dock:starting');
152 // Re-apply early bindings properly now that we can
153 this.applyBinds();
154 // Check if there is a customisation function
155 if (typeof(customise_dock_for_theme) === 'function') {
156 try {
157 // Run the customisation function
158 customise_dock_for_theme();
159 } catch (exception) {
160 // Do nothing at the moment
1ce15fda 161 }
7e4617f7
SH
162 }
163
a6338a13
SH
164 var dock = Y.one('#dock');
165 if (!dock) {
166 // Start the construction of the dock
e9921f6a 167 dock = Y.Node.create('<div id="dock" role="menubar" class="'+css.dock+' '+css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation+'"></div>')
827c319a
SH
168 .append(Y.Node.create('<div class="'+css.buttonscontainer+'"></div>')
169 .append(Y.Node.create('<div class="'+css.dockeditemcontainer+'"></div>')));
a6338a13
SH
170 this.nodes.body.append(dock);
171 } else {
172 dock.addClass(css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation);
173 }
ea9ad1f3 174 this.holdingarea = Y.Node.create('<div></div>').setStyles({display:'none'});
673835d4 175 this.nodes.body.append(this.holdingarea);
a6338a13
SH
176 if (Y.UA.ie > 0 && Y.UA.ie < 7) {
177 // Adjust for IE 6 (can't handle fixed pos)
178 dock.setStyle('height', dock.get('winHeight')+'px');
179 }
180 // Store the dock
181 this.nodes.dock = dock;
827c319a
SH
182 this.nodes.buttons = dock.one('.'+css.buttonscontainer);
183 this.nodes.container = this.nodes.buttons.one('.'+css.dockeditemcontainer);
a6338a13 184
7e4617f7
SH
185 if (Y.all('.block.dock_on_load').size() == 0) {
186 // Nothing on the dock... hide it using CSS
187 dock.addClass('nothingdocked');
188 } else {
827c319a 189 this.nodes.body.addClass(this.css.body).addClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation);
7e4617f7 190 }
a6338a13 191
7e4617f7 192 this.fire('dock:beforedraw');
a6338a13 193
7e4617f7 194 // Add a removeall button
8c29a17d 195 // Must set the image src seperatly of we get an error with XML strict headers
eb25449a 196 var removeall = Y.Node.create('<img alt="'+M.str.block.undockall+'" title="'+M.str.block.undockall+'" tabindex="0"/>');
8c29a17d 197 removeall.setAttribute('src',this.cfg.removeallicon);
7e4617f7 198 removeall.on('removeall|click', this.remove_all, this);
0193d9df 199 removeall.on('dock:actionkey', this.remove_all, this, {actions:{enter:true}});
827c319a 200 this.nodes.buttons.appendChild(Y.Node.create('<div class="'+css.controls+'"></div>').append(removeall));
7e4617f7
SH
201
202 // Create a manager for the height of the tabs. Once set this can be forgotten about
203 new (function(Y){
204 return {
205 enabled : false, // True if the item_sizer is being used, false otherwise
206 /**
207 * Initialises the dock sizer which then attaches itself to the required
208 * events in order to monitor the dock
209 * @param {YUI} Y
210 */
211 init : function() {
212 M.core_dock.on('dock:itemschanged', this.checkSizing, this);
213 Y.on('windowresize', this.checkSizing, this);
214 },
215 /**
216 * Check if the size dock items needs to be adjusted
217 */
218 checkSizing : function() {
219 var dock = M.core_dock;
220 var possibleheight = dock.nodes.dock.get('offsetHeight') - dock.nodes.dock.one('.controls').get('offsetHeight') - (dock.cfg.buffer*3) - (dock.items.length*2);
221 var totalheight = 0;
222 for (var id in dock.items) {
223 var dockedtitle = Y.one(dock.items[id].title).ancestor('.'+dock.css.dockedtitle);
224 if (dockedtitle) {
225 if (this.enabled) {
226 dockedtitle.setStyle('height', 'auto');
227 }
228 totalheight += dockedtitle.get('offsetHeight') || 0;
229 }
230 }
231 if (totalheight > possibleheight) {
232 this.enable(possibleheight);
233 }
234 },
235 /**
236 * Enables the dock sizer and resizes where required.
237 */
238 enable : function(possibleheight) {
239 var dock = M.core_dock;
240 var runningcount = 0;
241 var usedheight = 0;
242 this.enabled = true;
243 for (var id in dock.items) {
244 var itemtitle = Y.one(dock.items[id].title).ancestor('.'+dock.css.dockedtitle);
245 if (!itemtitle) {
246 continue;
247 }
248 var itemheight = Math.floor((possibleheight-usedheight) / (dock.count - runningcount));
249 var offsetheight = itemtitle.get('offsetHeight');
250 itemtitle.setStyle('overflow', 'hidden');
251 if (offsetheight > itemheight) {
252 itemtitle.setStyle('height', itemheight+'px');
253 usedheight += itemheight;
254 } else {
255 usedheight += offsetheight;
256 }
257 runningcount++;
258 }
1ce15fda 259 }
edc858dc 260 };
7e4617f7
SH
261 })(Y).init();
262
263 // Attach the required event listeners
264 // We use delegate here as that way a handful of events are created for the dock
265 // and all items rather than the same number for the dock AND every item individually
266 Y.delegate('click', this.handleEvent, this.nodes.dock, '.'+this.css.dockedtitle, this, {cssselector:'.'+this.css.dockedtitle, delay:0});
267 Y.delegate('mouseenter', this.handleEvent, this.nodes.dock, '.'+this.css.dockedtitle, this, {cssselector:'.'+this.css.dockedtitle, delay:0.5, iscontained:true, preventevent:'click', preventdelay:3});
8c29a17d
SH
268 //Y.delegate('mouseleave', this.handleEvent, this.nodes.body, '#dock', this, {cssselector:'#dock', delay:0.5, iscontained:false});
269 this.nodes.dock.on('mouseleave', this.handleEvent, this, {cssselector:'#dock', delay:0.5, iscontained:false});
270
7e4617f7
SH
271 this.nodes.body.on('click', this.handleEvent, this, {cssselector:'body', delay:0});
272 this.on('dock:itemschanged', this.resizeBlockSpace, this);
273 this.on('dock:itemschanged', this.checkDockVisibility, this);
4e2e816a 274 this.on('dock:itemschanged', this.resetFirstItem, this);
7e4617f7
SH
275 // Inform everyone the dock has been initialised
276 this.fire('dock:initialised');
277 return true;
edc858dc 278};
7e4617f7
SH
279/**
280 * Get the panel docked blocks will be shown in and initialise it if we havn't already.
281 */
282M.core_dock.getPanel = function() {
283 if (this.nodes.panel === null) {
284 // Initialise the dockpanel .. should only happen once
285 this.nodes.panel = (function(Y, parent){
286 var dockpanel = Y.Node.create('<div id="dockeditempanel" class="dockitempanel_hidden"><div class="dockeditempanel_content"><div class="dockeditempanel_hd"></div><div class="dockeditempanel_bd"></div></div></div>');
287 // Give the dockpanel event target properties and methods
288 Y.augment(dockpanel, Y.EventTarget);
289 // Publish events for the dock panel
290 dockpanel.publish('dockpanel:beforeshow', {prefix:'dockpanel'});
291 dockpanel.publish('dockpanel:shown', {prefix:'dockpanel'});
292 dockpanel.publish('dockpanel:beforehide', {prefix:'dockpanel'});
293 dockpanel.publish('dockpanel:hidden', {prefix:'dockpanel'});
294 dockpanel.publish('dockpanel:visiblechange', {prefix:'dockpanel'});
295 // Cache the content nodes
296 dockpanel.contentNode = dockpanel.one('.dockeditempanel_content');
297 dockpanel.contentHeader = dockpanel.contentNode.one('.dockeditempanel_hd');
298 dockpanel.contentBody = dockpanel.contentNode.one('.dockeditempanel_bd');
299 // Set the x position of the panel
300 //dockpanel.setX(parent.get('offsetWidth'));
301 dockpanel.visible = false;
302 // Add a show event
303 dockpanel.show = function() {
304 this.fire('dockpanel:beforeshow');
305 this.visible = true;
306 this.removeClass('dockitempanel_hidden');
307 this.fire('dockpanel:shown');
308 this.fire('dockpanel:visiblechange');
edc858dc 309 };
7e4617f7
SH
310 // Add a hide event
311 dockpanel.hide = function() {
312 this.fire('dockpanel:beforehide');
313 this.visible = false;
314 this.addClass('dockitempanel_hidden');
315 this.fire('dockpanel:hidden');
316 this.fire('dockpanel:visiblechange');
edc858dc 317 };
7e4617f7
SH
318 // Add a method to set the header content
319 dockpanel.setHeader = function(content) {
320 this.contentHeader.setContent(content);
321 if (arguments.length > 1) {
322 for (var i=1;i < arguments.length;i++) {
323 this.contentHeader.append(arguments[i]);
1ce15fda 324 }
1ce15fda 325 }
edc858dc 326 };
7e4617f7
SH
327 // Add a method to set the body content
328 dockpanel.setBody = function(content) {
329 this.contentBody.setContent(content);
edc858dc 330 };
7e4617f7
SH
331 // Add a method to set the top of the panel position
332 dockpanel.setTop = function(newtop) {
006051fd 333 if (Y.UA.ie > 0 && Y.UA.ie < 7) {
7e4617f7 334 this.setY(newtop);
006051fd
SH
335 } else {
336 this.setStyle('top', newtop.toString()+'px');
1ce15fda 337 }
006051fd 338 return;
edc858dc 339 };
48d8d090
SH
340 /**
341 * Increases the width of the panel to avoid horizontal scrolling
342 * if possible.
343 */
344 dockpanel.correctWidth = function() {
345 var bd = this.one('.dockeditempanel_bd');
346
347 // Width of content
348 var w = bd.get('clientWidth');
349 // Scrollable width of content
350 var s = bd.get('scrollWidth');
351 // Width of content container with overflow
352 var ow = this.get('offsetWidth');
353 // The new width
354 var nw = w;
355 // The max width (80% of screen)
356 var mw = Math.round(this.get('winWidth') * 0.8);
357
358 // If the scrollable width is more than the visible width
359 if (s > w) {
360 // Content width
361 // + the difference
362 // + any rendering difference (borders, padding)
363 // + 10px to make it look nice.
364 nw = w + (s-w) + ((ow-w)*2) + 10;
365 }
366
367 // Make sure its not more then the maxwidth
368 if (nw > mw) {
369 nw = mw;
370 }
371
372 // Set the new width if its more than the old width.
373 if (nw > ow) {
374 this.setStyle('width', nw+'px');
375 }
376 }
7e4617f7
SH
377 // Put the dockpanel in the body
378 parent.append(dockpanel);
379 // Return it
380 return dockpanel;
381 })(this.Y, this.nodes.dock);
382 this.nodes.panel.on('panel:visiblechange', this.resize, this);
383 this.Y.on('windowresize', this.resize, this);
384 this.fire('dock:panelgenerated');
385 }
386 return this.nodes.panel;
edc858dc 387};
7e4617f7
SH
388/**
389 * Handles a generic event within the dock
390 * @param {Y.Event} e
391 * @param {object} options Event configuration object
392 */
393M.core_dock.handleEvent = function(e, options) {
394 var item = this.getActiveItem();
7e4617f7
SH
395 if (options.cssselector == 'body') {
396 if (!this.nodes.dock.contains(e.target)) {
397 if (item) {
398 item.hide();
399 }
400 }
b110d3c6
SH
401 } else {
402 var target;
403 if (e.target.test(options.cssselector)) {
404 target = e.target;
405 } else {
406 target = e.target.ancestor(options.cssselector);
407 }
408 if (!target) {
409 return true;
410 }
7e4617f7
SH
411 if (this.preventevent !== null && e.type === this.preventevent) {
412 return true;
413 }
414 if (options.preventevent) {
415 this.preventevent = options.preventevent;
416 if (options.preventdelay) {
417 setTimeout(function(){M.core_dock.preventevent = null;}, options.preventdelay*1000);
418 }
419 }
420 if (this.delayedevent && this.delayedevent.timeout) {
421 clearTimeout(this.delayedevent.timeout);
422 this.delayedevent.event.detach();
423 this.delayedevent = null;
424 }
425 if (options.delay > 0) {
426 return this.delayEvent(e, options, target);
427 }
428 var targetid = target.get('id');
429 if (targetid.match(/^dock_item_(\d+)_title$/)) {
430 item = this.items[targetid.replace(/^dock_item_(\d+)_title$/, '$1')];
431 if (item.active) {
432 item.hide();
433 } else {
434 item.show();
1ce15fda 435 }
7e4617f7
SH
436 } else if (item) {
437 item.hide();
1ce15fda 438 }
6d5f8015 439 }
7e4617f7 440 return true;
edc858dc 441};
7e4617f7
SH
442/**
443 * This function delays an event and then fires it providing the cursor if either
444 * within or outside of the original target (options.iscontained=true|false)
445 * @param {Y.Event} event
446 * @param {object} options
447 * @param {Y.Node} target
448 * @return bool
449 */
450M.core_dock.delayEvent = function(event, options, target) {
451 var self = this;
452 self.delayedevent = (function(){
453 return {
454 target : target,
455 event : self.nodes.body.on('mousemove', function(e){
456 self.delayedevent.target = e.target;
457 }),
458 timeout : null
edc858dc 459 };
7e4617f7
SH
460 })(self);
461 self.delayedevent.timeout = setTimeout(function(){
462 self.delayedevent.timeout = null;
463 self.delayedevent.event.detach();
464 if (options.iscontained == self.nodes.dock.contains(self.delayedevent.target)) {
465 self.handleEvent(event, {cssselector:options.cssselector, delay:0, iscontained:options.iscontained});
466 }
467 }, options.delay*1000);
468 return true;
edc858dc 469};
7e4617f7
SH
470/**
471 * Corrects the orientation of the title, which for the default
472 * dock just means making it vertical
473 * The orientation is determined by M.str.langconfig.thisdirectionvertical:
474 * ver : Letters are stacked rather than rotated
475 * ttb : Title is rotated clockwise so the first letter is at the top
476 * btt : Title is rotated counterclockwise so the first letter is at the bottom.
477 * @param {string} title
478 */
10a995c1 479M.core_dock.fixTitleOrientation = function(item, title, text) {
7e4617f7 480 var Y = this.Y;
5dbfbacc 481
7e4617f7 482 var title = Y.one(title);
6d5f8015 483
827c319a
SH
484 if(M.core_dock.cfg.orientation != 'vertical') {
485 // If the dock isn't vertical don't adjust it!
486 title.setContent(text);
487 return title
488 }
489
7e4617f7
SH
490 if (Y.UA.ie > 0 && Y.UA.ie < 8) {
491 // IE 6/7 can't rotate text so force ver
492 M.str.langconfig.thisdirectionvertical = 'ver';
493 }
494
495 var clockwise = false;
496 switch (M.str.langconfig.thisdirectionvertical) {
497 case 'ver':
498 // Stacked is easy
499 return title.setContent(text.split('').join('<br />'));
500 case 'ttb':
501 clockwise = true;
502 break;
503 case 'btt':
504 clockwise = false;
505 break;
506 }
507
32db37bb
SH
508 if (Y.UA.ie == 8) {
509 // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute.
edc858dc 510 title.setContent(text);
7e4617f7
SH
511 title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;');
512 title.addClass('filterrotate');
513 return title;
514 }
515
32db37bb
SH
516 // We need to fix a font-size - sorry theme designers.
517 var fontsize = '11px';
518 var transform = (clockwise) ? 'rotate(90deg)' : 'rotate(270deg)';
62434c8a 519 var test = Y.Node.create('<h2><span class="transform-test-node" style="font-size:'+fontsize+';">'+text+'</span></h2>');
32db37bb
SH
520 this.nodes.body.insert(test, 0);
521 var width = test.one('span').get('offsetWidth') * 1.2;
522 var height = test.one('span').get('offsetHeight');
7e4617f7
SH
523 test.remove();
524
32db37bb
SH
525 title.setContent(text);
526 title.addClass('css3transform');
527
528 // Move the title into position
529 title.setStyles({
530 'margin' : '0',
531 'padding' : '0',
532 'position' : 'relative',
533 'fontSize' : fontsize,
534 'width' : width,
11f87187 535 'top' : width/2
32db37bb 536 });
7e4617f7 537
11f87187 538 // Positioning is different when in RTL mode.
59fa7fd0 539 if (right_to_left()) {
11f87187
FM
540 title.setStyle('left', width/2 - height);
541 } else {
542 title.setStyle('right', width/2 - height);
543 }
544
32db37bb
SH
545 // Rotate the text
546 title.setStyles({
547 'transform' : transform,
548 '-ms-transform' : transform,
549 '-moz-transform' : transform,
550 '-webkit-transform' : transform,
551 '-o-transform' : transform
552 });
10a995c1 553
32db37bb
SH
554 var container = Y.Node.create('<div></div>');
555 container.append(title);
556 container.setStyle('height', width + (width / 4));
557 container.setStyle('position', 'relative');
558 return container;
10a995c1 559
7e4617f7 560 return title;
edc858dc 561};
7e4617f7
SH
562/**
563 * Resizes the space that contained blocks if there were no blocks left in
564 * it. e.g. if all blocks have been moved to the dock
565 * @param {Y.Node} node
566 */
567M.core_dock.resizeBlockSpace = function(node) {
568
569 if (this.Y.all('.block.dock_on_load').size()>0) {
570 // Do not resize during initial load
571 return;
572 }
573 var blockregions = [];
574 var populatedblockregions = 0;
575 this.Y.all('.block-region').each(function(region){
576 var hasblocks = (region.all('.block').size() > 0);
577 if (hasblocks) {
578 populatedblockregions++;
579 }
580 blockregions[region.get('id')] = {hasblocks: hasblocks, bodyclass: region.get('id').replace(/^region\-/, 'side-')+'-only'};
581 });
582 var bodynode = M.core_dock.nodes.body;
39726f35
AB
583 var showregions = false;
584 if (bodynode.hasClass('blocks-moving')) {
585 // open up blocks during blocks positioning
586 showregions = true;
587 }
588
7e4617f7
SH
589 var noblocksbodyclass = 'content-only';
590 var i = null;
39726f35 591 if (populatedblockregions==0 && showregions==false) {
7e4617f7
SH
592 bodynode.addClass(noblocksbodyclass);
593 for (i in blockregions) {
594 bodynode.removeClass(blockregions[i].bodyclass);
595 }
39726f35 596 } else if (populatedblockregions==1 && showregions==false) {
7e4617f7
SH
597 bodynode.removeClass(noblocksbodyclass);
598 for (i in blockregions) {
599 if (!blockregions[i].hasblocks) {
600 bodynode.removeClass(blockregions[i].bodyclass);
601 } else {
602 bodynode.addClass(blockregions[i].bodyclass);
603 }
604 }
605 } else {
606 bodynode.removeClass(noblocksbodyclass);
607 for (i in blockregions) {
608 bodynode.removeClass(blockregions[i].bodyclass);
609 }
610 }
edc858dc 611};
7e4617f7
SH
612/**
613 * Adds a dock item into the dock
614 * @function
615 * @param {M.core_dock.item} item
616 */
617M.core_dock.add = function(item) {
618 item.id = this.totalcount;
619 this.count++;
620 this.totalcount++;
621 this.items[item.id] = item;
622 this.items[item.id].draw();
623 this.fire('dock:itemadded', item);
624 this.fire('dock:itemschanged', item);
edc858dc 625};
7e4617f7
SH
626/**
627 * Appends a dock item to the dock
628 * @param {YUI.Node} docknode
629 */
630M.core_dock.append = function(docknode) {
631 this.nodes.container.append(docknode);
edc858dc 632};
7e4617f7
SH
633/**
634 * Initialises a generic block object
635 * @param {YUI} Y
636 * @param {int} id
637 */
638M.core_dock.init_genericblock = function(Y, id) {
639 if (!this.initialised) {
640 this.init(Y);
641 }
48d8d090 642 new this.genericblock(id).initialise_block(Y, Y.one('#inst'+id));
edc858dc 643};
7e4617f7
SH
644/**
645 * Removes the node at the given index and puts it back into conventional page sturcture
646 * @function
647 * @param {int} uid Unique identifier for the block
648 * @return {boolean}
649 */
650M.core_dock.remove = function(uid) {
651 if (!this.items[uid]) {
652 return false;
653 }
654 this.items[uid].remove();
655 delete this.items[uid];
656 this.count--;
657 this.fire('dock:itemremoved', uid);
658 this.fire('dock:itemschanged', uid);
659 return true;
edc858dc 660};
4e2e816a
SH
661/**
662 * Ensures the the first item in the dock has the correct class
663 */
664M.core_dock.resetFirstItem = function() {
665 this.nodes.dock.all('.'+this.css.dockeditem+'.firstdockitem').removeClass('firstdockeditem');
666 if (this.nodes.dock.one('.'+this.css.dockeditem)) {
667 this.nodes.dock.one('.'+this.css.dockeditem).addClass('firstdockitem');
668 }
edc858dc 669};
7e4617f7
SH
670/**
671 * Removes all nodes and puts them back into conventional page sturcture
672 * @function
673 * @return {boolean}
674 */
eb25449a 675M.core_dock.remove_all = function(e) {
0193d9df
AB
676 for (var i in this.items) {
677 this.remove(i);
7e4617f7
SH
678 }
679 return true;
edc858dc 680};
7e4617f7
SH
681/**
682 * Hides the active item
683 */
684M.core_dock.hideActive = function() {
685 var item = this.getActiveItem();
686 if (item) {
687 item.hide();
688 }
edc858dc 689};
7e4617f7
SH
690/**
691 * Checks wether the dock should be shown or hidden
692 */
693M.core_dock.checkDockVisibility = function() {
694 if (!this.count) {
695 this.nodes.dock.addClass('nothingdocked');
827c319a
SH
696 this.nodes.body.removeClass(this.css.body)
697 .removeClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation);
7e4617f7
SH
698 this.fire('dock:hidden');
699 } else {
700 this.fire('dock:beforeshow');
701 this.nodes.dock.removeClass('nothingdocked');
827c319a
SH
702 this.nodes.body.addClass(this.css.body)
703 .addClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation);
7e4617f7
SH
704 this.fire('dock:shown');
705 }
edc858dc 706};
7e4617f7
SH
707/**
708 * This smart little function allows developers to attach event listeners before
709 * the dock has been augmented to allows event listeners.
710 * Once the augmentation is complete this function will be replaced with the proper
711 * on method for handling event listeners.
712 * Finally applyBinds needs to be called in order to properly bind events.
713 * @param {string} event
714 * @param {function} callback
715 */
716M.core_dock.on = function(event, callback) {
717 this.earlybinds.push({event:event,callback:callback});
edc858dc 718};
7e4617f7
SH
719/**
720 * This function takes all early binds and attaches them as listeners properly
721 * This should only be called once augmentation is complete.
722 */
723M.core_dock.applyBinds = function() {
724 for (var i in this.earlybinds) {
725 var bind = this.earlybinds[i];
726 this.on(bind.event, bind.callback);
727 }
728 this.earlybinds = [];
edc858dc 729};
7e4617f7
SH
730/**
731 * This function checks the size and position of the panel and moves/resizes if
732 * required to keep it within the bounds of the window.
733 */
734M.core_dock.resize = function() {
735 this.fire('dock:panelresizestart');
736 var panel = this.getPanel();
737 var item = this.getActiveItem();
738 if (!panel.visible || !item) {
739 return;
740 }
827c319a
SH
741
742 if (this.cfg.orientation=='vertical') {
743 var buffer = this.cfg.buffer;
744 var screenheight = parseInt(this.nodes.body.get('winHeight'))-(buffer*2);
745 var docky = this.nodes.dock.getY();
746 var titletop = item.nodes.docktitle.getY()-docky-buffer;
747 var containery = this.nodes.container.getY();
748 var containerheight = containery-docky+this.nodes.buttons.get('offsetHeight');
a467d243 749 var scrolltop = panel.contentBody.get('scrollTop');
827c319a
SH
750 panel.contentBody.setStyle('height', 'auto');
751 panel.removeClass('oversized_content');
752 var panelheight = panel.get('offsetHeight');
753
754 if (this.Y.UA.ie > 0 && this.Y.UA.ie < 7) {
755 panel.setTop(item.nodes.docktitle.getY());
756 } else if (panelheight > screenheight) {
757 panel.setTop(buffer-containerheight);
758 panel.contentBody.setStyle('height', (screenheight-panel.contentHeader.get('offsetHeight'))+'px');
759 panel.addClass('oversized_content');
760 } else if (panelheight > (screenheight-(titletop-buffer))) {
761 var difference = panelheight - (screenheight-titletop);
762 panel.setTop(titletop-containerheight-difference+buffer);
763 } else {
764 panel.setTop(titletop-containerheight+buffer);
765 }
a467d243
SH
766
767 if (scrolltop) {
768 panel.contentBody.set('scrollTop', scrolltop);
769 }
7e4617f7 770 }
827c319a
SH
771
772 if (this.cfg.position=='right') {
773 panel.setStyle('left', -panel.get('offsetWidth')+'px');
774
775 } else if (this.cfg.position=='top') {
776 var dockx = this.nodes.dock.getX();
777 var titleleft = item.nodes.docktitle.getX()-dockx;
778 panel.setStyle('left', titleleft+'px');
779 }
780
7e4617f7
SH
781 this.fire('dock:resizepanelcomplete');
782 return;
edc858dc 783};
7e4617f7
SH
784/**
785 * Returns the currently active dock item or false
786 */
787M.core_dock.getActiveItem = function() {
788 for (var i in this.items) {
789 if (this.items[i].active) {
790 return this.items[i];
791 }
792 }
793 return false;
edc858dc 794};
6d5f8015
SH
795/**
796 * This class represents a generic block
7e4617f7 797 * @class M.core_dock.genericblock
6d5f8015
SH
798 * @constructor
799 */
7e4617f7 800M.core_dock.genericblock = function(id) {
6d5f8015 801 // Nothing to actually do here but it needs a constructor!
7e4617f7
SH
802 if (id) {
803 this.id = id;
804 }
6d5f8015
SH
805};
806M.core_dock.genericblock.prototype = {
807 Y : null, // A YUI instance to use with the block
808 id : null, // The block instance id
809 cachedcontentnode : null, // The cached content node for the actual block
810 blockspacewidth : null, // The width of the block's original container
811 skipsetposition : false, // If true the user preference isn't updated
7e4617f7 812 isdocked : false, // True if it is docked
1ce15fda 813 /**
6d5f8015
SH
814 * This function should be called within the block's constructor and is used to
815 * set up the initial controls for swtiching block position as well as an initial
816 * moves that may be required.
817 *
818 * @param {YUI} Y
819 * @param {YUI.Node} node The node that contains all of the block's content
7e4617f7 820 * @return {M.core_dock.genericblock}
1ce15fda 821 */
48d8d090 822 initialise_block : function(Y, node) {
7e4617f7 823 M.core_dock.init(Y);
5dbfbacc 824
6d5f8015
SH
825 this.Y = Y;
826 if (!node) {
7e4617f7 827 return false;
6d5f8015 828 }
53fc3e70 829
6d5f8015
SH
830 var commands = node.one('.header .title .commands');
831 if (!commands) {
832 commands = this.Y.Node.create('<div class="commands"></div>');
833 if (node.one('.header .title')) {
834 node.one('.header .title').append(commands);
1ce15fda 835 }
6d5f8015 836 }
1ce15fda 837
8c29a17d
SH
838 // Must set the image src seperatly of we get an error with XML strict headers
839 var moveto = Y.Node.create('<input type="image" class="moveto customcommand requiresjs" alt="'+M.str.block.addtodock+'" title="'+M.str.block.addtodock+'" />');
59fa7fd0
FM
840 var icon = 't/block_to_dock';
841 if (right_to_left()) {
842 icon = 't/block_to_dock_rtl';
843 }
844 moveto.setAttribute('src', M.util.image_url(icon, 'moodle'));
6d5f8015 845 moveto.on('movetodock|click', this.move_to_dock, this, commands);
1ce15fda 846
6d5f8015
SH
847 var blockaction = node.one('.block_action');
848 if (blockaction) {
849 blockaction.prepend(moveto);
850 } else {
851 commands.append(moveto);
852 }
46de77b6 853
6d5f8015
SH
854 // Move the block straight to the dock if required
855 if (node.hasClass('dock_on_load')) {
edc858dc 856 node.removeClass('dock_on_load');
6d5f8015
SH
857 this.skipsetposition = true;
858 this.move_to_dock(null, commands);
859 }
7e4617f7 860 return this;
6d5f8015 861 },
1ce15fda 862
6d5f8015
SH
863 /**
864 * This function is reponsible for moving a block from the page structure onto the
865 * dock
866 * @param {event}
867 */
868 move_to_dock : function(e, commands) {
869 if (e) {
870 e.halt(true);
871 }
1ce15fda 872
7e4617f7 873 var Y = this.Y;
edc858dc 874 var dock = M.core_dock;
7e4617f7
SH
875
876 var node = Y.one('#inst'+this.id);
6d5f8015
SH
877 var blockcontent = node.one('.content');
878 if (!blockcontent) {
879 return;
880 }
1ce15fda 881
20b12d54
FM
882 // Disable the skip anchor when docking
883 var skipanchor = node.previous();
884 if (skipanchor.hasClass('skip-block')) {
885 skipanchor.hide();
886 }
887
6d5f8015
SH
888 var blockclass = (function(classes){
889 var r = /(^|\s)(block_[a-zA-Z0-9_]+)(\s|$)/;
890 var m = r.exec(classes);
891 return (m)?m[2]:m;
892 })(node.getAttribute('className').toString());
1ce15fda 893
6d5f8015 894 this.cachedcontentnode = node;
90723839 895
7e4617f7 896 node.replace(Y.Node.getDOMNode(Y.Node.create('<div id="content_placeholder_'+this.id+'" class="block_dock_placeholder"></div>')));
673835d4 897 M.core_dock.holdingarea.append(node);
6d5f8015 898 node = null;
1ce15fda 899
7e4617f7 900 var blocktitle = Y.Node.getDOMNode(this.cachedcontentnode.one('.title h2')).cloneNode(true);
1ce15fda 901
6d5f8015
SH
902 var blockcommands = this.cachedcontentnode.one('.title .commands');
903 if (!blockcommands) {
7e4617f7 904 blockcommands = Y.Node.create('<div class="commands"></div>');
6d5f8015
SH
905 this.cachedcontentnode.one('.title').append(blockcommands);
906 }
8c29a17d
SH
907
908 // Must set the image src seperatly of we get an error with XML strict headers
b9271ffd 909 var movetoimg = Y.Node.create('<img alt="'+M.str.block.undockitem+'" title="'+M.util.get_string('undockblock', 'block', blocktitle.innerHTML)+'" />');
59fa7fd0
FM
910 var icon = 't/dock_to_block';
911 if (right_to_left()) {
912 icon = 't/dock_to_block_rtl';
913 }
914 movetoimg.setAttribute('src', M.util.image_url(icon, 'moodle'));
8c29a17d 915 var moveto = Y.Node.create('<a class="moveto customcommand requiresjs"></a>').append(movetoimg);
6d5f8015
SH
916 if (location.href.match(/\?/)) {
917 moveto.set('href', location.href+'&dock='+this.id);
918 } else {
919 moveto.set('href', location.href+'?dock='+this.id);
920 }
921 blockcommands.append(moveto);
1ce15fda 922
6d5f8015 923 // Create a new dock item for the block
7e4617f7 924 var dockitem = new dock.item(Y, this.id, blocktitle, blockcontent, blockcommands, blockclass);
6d5f8015
SH
925 // Wire the draw events to register remove events
926 dockitem.on('dockeditem:drawcomplete', function(e){
927 // check the contents block [editing=off]
928 this.contents.all('.moveto').on('returntoblock|click', function(e){
929 e.halt();
edc858dc 930 dock.remove(this.id);
6d5f8015
SH
931 }, this);
932 // check the commands block [editing=on]
933 this.commands.all('.moveto').on('returntoblock|click', function(e){
934 e.halt();
edc858dc 935 dock.remove(this.id);
6d5f8015
SH
936 }, this);
937 // Add a close icon
8c29a17d 938 // Must set the image src seperatly of we get an error with XML strict headers
d01176bf 939 var closeicon = Y.Node.create('<span class="hidepanelicon" tabindex="0"><img alt="'+M.str.block.hidepanel+'" title="'+M.str.block.hidedockpanel+'" /></span>');
8c29a17d 940 closeicon.one('img').setAttribute('src', M.util.image_url('t/dockclose', 'moodle'));
7e4617f7 941 closeicon.on('forceclose|click', this.hide, this);
0193d9df 942 closeicon.on('dock:actionkey',this.hide, this, {actions:{enter:true,toggle:true}});
6d5f8015
SH
943 this.commands.append(closeicon);
944 }, dockitem);
6d5f8015 945 // Register an event so that when it is removed we can put it back as a block
7e4617f7
SH
946 dockitem.on('dockeditem:itemremoved', this.return_to_block, this, dockitem);
947 dock.add(dockitem);
5dbfbacc 948
6d5f8015
SH
949 if (!this.skipsetposition) {
950 // save the users preference
951 M.util.set_user_preference('docked_block_instance_'+this.id, 1);
952 } else {
953 this.skipsetposition = false;
954 }
1ce15fda 955
7e4617f7 956 this.isdocked = true;
1ce15fda
SH
957 },
958 /**
6d5f8015
SH
959 * This function removes a block from the dock and puts it back into the page
960 * structure.
961 * @param {M.core_dock.class.item}
1ce15fda 962 */
6d5f8015
SH
963 return_to_block : function(dockitem) {
964 var placeholder = this.Y.one('#content_placeholder_'+this.id);
1ce15fda 965
20b12d54
FM
966 // Enable the skip anchor when going back to block mode
967 var skipanchor = placeholder.previous();
968 if (skipanchor.hasClass('skip-block')) {
969 skipanchor.show();
970 }
971
6d5f8015
SH
972 if (this.cachedcontentnode.one('.header')) {
973 this.cachedcontentnode.one('.header').insert(dockitem.contents, 'after');
974 } else {
975 this.cachedcontentnode.insert(dockitem.contents);
976 }
1ce15fda 977
6d5f8015
SH
978 placeholder.replace(this.Y.Node.getDOMNode(this.cachedcontentnode));
979 this.cachedcontentnode = this.Y.one('#'+this.cachedcontentnode.get('id'));
d2e68385 980
ecc8ce88 981 var commands = dockitem.commands;
6d5f8015
SH
982 if (commands) {
983 commands.all('.hidepanelicon').remove();
984 commands.all('.moveto').remove();
985 commands.remove();
1ce15fda 986 }
6d5f8015
SH
987 this.cachedcontentnode.one('.title').append(commands);
988 this.cachedcontentnode = null;
989 M.util.set_user_preference('docked_block_instance_'+this.id, 0);
7e4617f7 990 this.isdocked = false;
6d5f8015 991 return true;
1ce15fda 992 }
edc858dc 993};
1ce15fda
SH
994
995/**
996 * This class represents an item in the dock
7e4617f7 997 * @class M.core_dock.item
1ce15fda 998 * @constructor
d2e68385 999 * @param {YUI} Y The YUI instance to use for this item
1ce15fda 1000 * @param {int} uid The unique ID for the item
53fc3e70
SH
1001 * @param {this.Y.Node} title
1002 * @param {this.Y.Node} contents
1003 * @param {this.Y.Node} commands
3406acde 1004 * @param {string} blockclass
1ce15fda 1005 */
90723839 1006M.core_dock.item = function(Y, uid, title, contents, commands, blockclass){
53fc3e70 1007 this.Y = Y;
10a995c1
SH
1008 this.publish('dockeditem:drawstart', {prefix:'dockeditem'});
1009 this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'});
1010 this.publish('dockeditem:showstart', {prefix:'dockeditem'});
1011 this.publish('dockeditem:showcomplete', {prefix:'dockeditem'});
1012 this.publish('dockeditem:hidestart', {prefix:'dockeditem'});
1013 this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'});
1014 this.publish('dockeditem:itemremoved', {prefix:'dockeditem'});
53fc3e70
SH
1015 if (uid && this.id==null) {
1016 this.id = uid;
1017 }
1018 if (title && this.title==null) {
7e4617f7
SH
1019 this.titlestring = title.cloneNode(true);
1020 this.title = document.createElement(title.nodeName);
32db37bb 1021 this.title = M.core_dock.fixTitleOrientation(this, this.title, this.titlestring.firstChild.nodeValue);
53fc3e70
SH
1022 }
1023 if (contents && this.contents==null) {
1024 this.contents = contents;
1025 }
1026 if (commands && this.commands==null) {
1027 this.commands = commands;
1028 }
90723839 1029 if (blockclass && this.blockclass==null) {
edc858dc 1030 this.blockclass = blockclass;
90723839 1031 }
7e4617f7 1032 this.nodes = (function(){
edc858dc 1033 return {docktitle : null, dockitem : null, container: null};
7e4617f7 1034 })();
edc858dc 1035};
6d5f8015
SH
1036/**
1037 *
1038 */
1039M.core_dock.item.prototype = {
1040 Y : null, // The YUI instance to use with this dock item
1041 id : null, // The unique id for the item
1042 name : null, // The name of the item
1043 title : null, // The title of the item
7e4617f7 1044 titlestring : null, // The title as a plain string
6d5f8015
SH
1045 contents : null, // The content of the item
1046 commands : null, // The commands for the item
1047 active : false, // True if the item is being shown
7e4617f7
SH
1048 blockclass : null, // The class of the block this item relates to
1049 nodes : null,
6d5f8015
SH
1050 /**
1051 * This function draws the item on the dock
1052 */
1053 draw : function() {
1054 this.fire('dockeditem:drawstart');
6d5f8015 1055
7e4617f7
SH
1056 var Y = this.Y;
1057 var css = M.core_dock.css;
1058
e9921f6a 1059 this.nodes.docktitle = Y.Node.create('<div id="dock_item_'+this.id+'_title" role="menu" aria-haspopup="true" class="'+css.dockedtitle+'"></div>');
7e4617f7 1060 this.nodes.docktitle.append(this.title);
eb25449a
AB
1061 this.nodes.dockitem = Y.Node.create('<div id="dock_item_'+this.id+'" class="'+css.dockeditem+'" tabindex="0"></div>');
1062 this.nodes.dockitem.on('dock:actionkey', this.toggle, this);
7e4617f7
SH
1063 if (M.core_dock.count === 1) {
1064 this.nodes.dockitem.addClass('firstdockitem');
6d5f8015 1065 }
7e4617f7
SH
1066 this.nodes.dockitem.append(this.nodes.docktitle);
1067 M.core_dock.append(this.nodes.dockitem);
6d5f8015 1068 this.fire('dockeditem:drawcomplete');
7e4617f7 1069 return true;
6d5f8015
SH
1070 },
1071 /**
1072 * This function toggles makes the item active and shows it
6d5f8015 1073 */
7e4617f7
SH
1074 show : function() {
1075 M.core_dock.hideActive();
1076 var Y = this.Y;
1077 var css = M.core_dock.css;
1078 var panel = M.core_dock.getPanel();
6d5f8015 1079 this.fire('dockeditem:showstart');
7e4617f7
SH
1080 panel.setHeader(this.titlestring, this.commands);
1081 panel.setBody(Y.Node.create('<div class="'+this.blockclass+' block_docked"></div>').append(this.contents));
1082 panel.show();
48d8d090 1083 panel.correctWidth();
827c319a 1084
6d5f8015
SH
1085 this.active = true;
1086 // Add active item class first up
7e4617f7 1087 this.nodes.docktitle.addClass(css.activeitem);
e9921f6a
RT
1088 // Set aria-exapanded property to true.
1089 this.nodes.docktitle.set('aria-expanded', "true");
6d5f8015 1090 this.fire('dockeditem:showcomplete');
7e4617f7 1091 M.core_dock.resize();
6d5f8015
SH
1092 return true;
1093 },
1094 /**
1095 * This function hides the item and makes it inactive
6d5f8015 1096 */
7e4617f7
SH
1097 hide : function() {
1098 var css = M.core_dock.css;
1099 this.fire('dockeditem:hidestart');
1100 // No longer active
1101 this.active = false;
1102 // Remove the active class
1103 this.nodes.docktitle.removeClass(css.activeitem);
1104 // Hide the panel
1105 M.core_dock.getPanel().hide();
e9921f6a
RT
1106 // Set aria-exapanded property to false
1107 this.nodes.docktitle.set('aria-expanded', "false");
7e4617f7 1108 this.fire('dockeditem:hidecomplete');
6d5f8015 1109 },
eb25449a
AB
1110 /**
1111 * A toggle between calling show and hide functions based on css.activeitem
1112 * Applies rules to key press events (dock:actionkey)
1113 * @param {Event} e
1114 */
1115 toggle : function(e) {
1116 var css = M.core_dock.css;
1117 if (this.nodes.docktitle.hasClass(css.activeitem) && !(e.type == 'dock:actionkey' && e.action=='expand')) {
1118 this.hide();
1119 } else if (!this.nodes.docktitle.hasClass(css.activeitem) && !(e.type == 'dock:actionkey' && e.action=='collapse')) {
1120 this.show();
1121 }
1122 },
6d5f8015 1123 /**
7e4617f7 1124 * This function removes the node and destroys it's bits
6d5f8015
SH
1125 * @param {Event} e
1126 */
7e4617f7
SH
1127 remove : function () {
1128 this.hide();
1129 this.nodes.dockitem.remove();
1130 this.fire('dockeditem:itemremoved');
6d5f8015 1131 }
827c319a 1132};