blocks-dock MDL-23891 Avoided use of broken YUI ancestor function in IE
[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
48 dockeditem:'dockeditem', // CSS class added to each item in the dock
49 dockeditemcontainer:'dockeditem_container',
50 dockedtitle:'dockedtitle', // CSS class added to the item's title in each dock
51 activeitem:'activeitem' // CSS class added to the active item
edc858dc 52};
7e4617f7
SH
53/**
54 * Augments the classes as required and processes early bindings
55 */
56M.core_dock.init = function(Y) {
57 if (this.initialised) {
1ce15fda 58 return true;
7e4617f7
SH
59 }
60 var css = this.css;
61 this.initialised = true;
62 this.Y = Y;
63 this.nodes.body = Y.one(document.body);
64
65 // Give the dock item class the event properties/methods
66 Y.augment(this.item, Y.EventTarget);
67 Y.augment(this, Y.EventTarget, true);
68
69 // Publish the events the dock has
70 this.publish('dock:beforedraw', {prefix:'dock'});
71 this.publish('dock:beforeshow', {prefix:'dock'});
72 this.publish('dock:shown', {prefix:'dock'});
73 this.publish('dock:hidden', {prefix:'dock'});
74 this.publish('dock:initialised', {prefix:'dock'});
75 this.publish('dock:itemadded', {prefix:'dock'});
76 this.publish('dock:itemremoved', {prefix:'dock'});
77 this.publish('dock:itemschanged', {prefix:'dock'});
78 this.publish('dock:panelgenerated', {prefix:'dock'});
79 this.publish('dock:panelresizestart', {prefix:'dock'});
80 this.publish('dock:resizepanelcomplete', {prefix:'dock'});
81 this.publish('dock:starting', {prefix: 'dock',broadcast: 2,emitFacade: true});
82 this.fire('dock:starting');
83 // Re-apply early bindings properly now that we can
84 this.applyBinds();
85 // Check if there is a customisation function
86 if (typeof(customise_dock_for_theme) === 'function') {
87 try {
88 // Run the customisation function
89 customise_dock_for_theme();
90 } catch (exception) {
91 // Do nothing at the moment
1ce15fda 92 }
7e4617f7
SH
93 }
94
a6338a13
SH
95 var dock = Y.one('#dock');
96 if (!dock) {
97 // Start the construction of the dock
98 dock = Y.Node.create('<div id="dock" class="'+css.dock+' '+css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation+'"></div>')
99 .append(Y.Node.create('<div class="'+css.dockeditemcontainer+'"></div>'));
100 this.nodes.body.append(dock);
101 } else {
102 dock.addClass(css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation);
103 }
ea9ad1f3 104 this.holdingarea = Y.Node.create('<div></div>').setStyles({display:'none'});
673835d4 105 this.nodes.body.append(this.holdingarea);
a6338a13
SH
106 if (Y.UA.ie > 0 && Y.UA.ie < 7) {
107 // Adjust for IE 6 (can't handle fixed pos)
108 dock.setStyle('height', dock.get('winHeight')+'px');
109 }
110 // Store the dock
111 this.nodes.dock = dock;
112 this.nodes.container = dock.one('.'+css.dockeditemcontainer);
113
7e4617f7
SH
114 if (Y.all('.block.dock_on_load').size() == 0) {
115 // Nothing on the dock... hide it using CSS
116 dock.addClass('nothingdocked');
117 } else {
118 this.nodes.body.addClass(this.css.body);
119 }
a6338a13 120
7e4617f7 121 this.fire('dock:beforedraw');
a6338a13 122
7e4617f7 123 // Add a removeall button
8c29a17d
SH
124 // Must set the image src seperatly of we get an error with XML strict headers
125 var removeall = Y.Node.create('<img alt="'+M.str.block.undockall+'" title="'+M.str.block.undockall+'" />');
126 removeall.setAttribute('src',this.cfg.removeallicon);
7e4617f7
SH
127 removeall.on('removeall|click', this.remove_all, this);
128 dock.appendChild(Y.Node.create('<div class="'+css.controls+'"></div>').append(removeall));
129
130 // Create a manager for the height of the tabs. Once set this can be forgotten about
131 new (function(Y){
132 return {
133 enabled : false, // True if the item_sizer is being used, false otherwise
134 /**
135 * Initialises the dock sizer which then attaches itself to the required
136 * events in order to monitor the dock
137 * @param {YUI} Y
138 */
139 init : function() {
140 M.core_dock.on('dock:itemschanged', this.checkSizing, this);
141 Y.on('windowresize', this.checkSizing, this);
142 },
143 /**
144 * Check if the size dock items needs to be adjusted
145 */
146 checkSizing : function() {
147 var dock = M.core_dock;
148 var possibleheight = dock.nodes.dock.get('offsetHeight') - dock.nodes.dock.one('.controls').get('offsetHeight') - (dock.cfg.buffer*3) - (dock.items.length*2);
149 var totalheight = 0;
150 for (var id in dock.items) {
151 var dockedtitle = Y.one(dock.items[id].title).ancestor('.'+dock.css.dockedtitle);
152 if (dockedtitle) {
153 if (this.enabled) {
154 dockedtitle.setStyle('height', 'auto');
155 }
156 totalheight += dockedtitle.get('offsetHeight') || 0;
157 }
158 }
159 if (totalheight > possibleheight) {
160 this.enable(possibleheight);
161 }
162 },
163 /**
164 * Enables the dock sizer and resizes where required.
165 */
166 enable : function(possibleheight) {
167 var dock = M.core_dock;
168 var runningcount = 0;
169 var usedheight = 0;
170 this.enabled = true;
171 for (var id in dock.items) {
172 var itemtitle = Y.one(dock.items[id].title).ancestor('.'+dock.css.dockedtitle);
173 if (!itemtitle) {
174 continue;
175 }
176 var itemheight = Math.floor((possibleheight-usedheight) / (dock.count - runningcount));
177 var offsetheight = itemtitle.get('offsetHeight');
178 itemtitle.setStyle('overflow', 'hidden');
179 if (offsetheight > itemheight) {
180 itemtitle.setStyle('height', itemheight+'px');
181 usedheight += itemheight;
182 } else {
183 usedheight += offsetheight;
184 }
185 runningcount++;
186 }
1ce15fda 187 }
edc858dc 188 };
7e4617f7
SH
189 })(Y).init();
190
191 // Attach the required event listeners
192 // We use delegate here as that way a handful of events are created for the dock
193 // and all items rather than the same number for the dock AND every item individually
194 Y.delegate('click', this.handleEvent, this.nodes.dock, '.'+this.css.dockedtitle, this, {cssselector:'.'+this.css.dockedtitle, delay:0});
195 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
196 //Y.delegate('mouseleave', this.handleEvent, this.nodes.body, '#dock', this, {cssselector:'#dock', delay:0.5, iscontained:false});
197 this.nodes.dock.on('mouseleave', this.handleEvent, this, {cssselector:'#dock', delay:0.5, iscontained:false});
198
7e4617f7
SH
199 this.nodes.body.on('click', this.handleEvent, this, {cssselector:'body', delay:0});
200 this.on('dock:itemschanged', this.resizeBlockSpace, this);
201 this.on('dock:itemschanged', this.checkDockVisibility, this);
4e2e816a 202 this.on('dock:itemschanged', this.resetFirstItem, this);
7e4617f7
SH
203 // Inform everyone the dock has been initialised
204 this.fire('dock:initialised');
205 return true;
edc858dc 206};
7e4617f7
SH
207/**
208 * Get the panel docked blocks will be shown in and initialise it if we havn't already.
209 */
210M.core_dock.getPanel = function() {
211 if (this.nodes.panel === null) {
212 // Initialise the dockpanel .. should only happen once
213 this.nodes.panel = (function(Y, parent){
214 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>');
215 // Give the dockpanel event target properties and methods
216 Y.augment(dockpanel, Y.EventTarget);
217 // Publish events for the dock panel
218 dockpanel.publish('dockpanel:beforeshow', {prefix:'dockpanel'});
219 dockpanel.publish('dockpanel:shown', {prefix:'dockpanel'});
220 dockpanel.publish('dockpanel:beforehide', {prefix:'dockpanel'});
221 dockpanel.publish('dockpanel:hidden', {prefix:'dockpanel'});
222 dockpanel.publish('dockpanel:visiblechange', {prefix:'dockpanel'});
223 // Cache the content nodes
224 dockpanel.contentNode = dockpanel.one('.dockeditempanel_content');
225 dockpanel.contentHeader = dockpanel.contentNode.one('.dockeditempanel_hd');
226 dockpanel.contentBody = dockpanel.contentNode.one('.dockeditempanel_bd');
227 // Set the x position of the panel
228 //dockpanel.setX(parent.get('offsetWidth'));
229 dockpanel.visible = false;
230 // Add a show event
231 dockpanel.show = function() {
232 this.fire('dockpanel:beforeshow');
233 this.visible = true;
234 this.removeClass('dockitempanel_hidden');
235 this.fire('dockpanel:shown');
236 this.fire('dockpanel:visiblechange');
edc858dc 237 };
7e4617f7
SH
238 // Add a hide event
239 dockpanel.hide = function() {
240 this.fire('dockpanel:beforehide');
241 this.visible = false;
242 this.addClass('dockitempanel_hidden');
243 this.fire('dockpanel:hidden');
244 this.fire('dockpanel:visiblechange');
edc858dc 245 };
7e4617f7
SH
246 // Add a method to set the header content
247 dockpanel.setHeader = function(content) {
248 this.contentHeader.setContent(content);
249 if (arguments.length > 1) {
250 for (var i=1;i < arguments.length;i++) {
251 this.contentHeader.append(arguments[i]);
1ce15fda 252 }
1ce15fda 253 }
edc858dc 254 };
7e4617f7
SH
255 // Add a method to set the body content
256 dockpanel.setBody = function(content) {
257 this.contentBody.setContent(content);
edc858dc 258 };
7e4617f7
SH
259 // Add a method to set the top of the panel position
260 dockpanel.setTop = function(newtop) {
261 this.setY(newtop);
262 return;
263 if (Y.UA.ie > 0) {
264 this.setY(newtop);
265 return true;
1ce15fda 266 }
7e4617f7 267 this.setStyle('top', newtop+'px');
edc858dc 268 };
7e4617f7
SH
269 // Put the dockpanel in the body
270 parent.append(dockpanel);
271 // Return it
272 return dockpanel;
273 })(this.Y, this.nodes.dock);
274 this.nodes.panel.on('panel:visiblechange', this.resize, this);
275 this.Y.on('windowresize', this.resize, this);
276 this.fire('dock:panelgenerated');
277 }
278 return this.nodes.panel;
edc858dc 279};
7e4617f7
SH
280/**
281 * Handles a generic event within the dock
282 * @param {Y.Event} e
283 * @param {object} options Event configuration object
284 */
285M.core_dock.handleEvent = function(e, options) {
286 var item = this.getActiveItem();
7e4617f7
SH
287 if (options.cssselector == 'body') {
288 if (!this.nodes.dock.contains(e.target)) {
289 if (item) {
290 item.hide();
291 }
292 }
b110d3c6
SH
293 } else {
294 var target;
295 if (e.target.test(options.cssselector)) {
296 target = e.target;
297 } else {
298 target = e.target.ancestor(options.cssselector);
299 }
300 if (!target) {
301 return true;
302 }
7e4617f7
SH
303 if (this.preventevent !== null && e.type === this.preventevent) {
304 return true;
305 }
306 if (options.preventevent) {
307 this.preventevent = options.preventevent;
308 if (options.preventdelay) {
309 setTimeout(function(){M.core_dock.preventevent = null;}, options.preventdelay*1000);
310 }
311 }
312 if (this.delayedevent && this.delayedevent.timeout) {
313 clearTimeout(this.delayedevent.timeout);
314 this.delayedevent.event.detach();
315 this.delayedevent = null;
316 }
317 if (options.delay > 0) {
318 return this.delayEvent(e, options, target);
319 }
320 var targetid = target.get('id');
321 if (targetid.match(/^dock_item_(\d+)_title$/)) {
322 item = this.items[targetid.replace(/^dock_item_(\d+)_title$/, '$1')];
323 if (item.active) {
324 item.hide();
325 } else {
326 item.show();
1ce15fda 327 }
7e4617f7
SH
328 } else if (item) {
329 item.hide();
1ce15fda 330 }
6d5f8015 331 }
7e4617f7 332 return true;
edc858dc 333};
7e4617f7
SH
334/**
335 * This function delays an event and then fires it providing the cursor if either
336 * within or outside of the original target (options.iscontained=true|false)
337 * @param {Y.Event} event
338 * @param {object} options
339 * @param {Y.Node} target
340 * @return bool
341 */
342M.core_dock.delayEvent = function(event, options, target) {
343 var self = this;
344 self.delayedevent = (function(){
345 return {
346 target : target,
347 event : self.nodes.body.on('mousemove', function(e){
348 self.delayedevent.target = e.target;
349 }),
350 timeout : null
edc858dc 351 };
7e4617f7
SH
352 })(self);
353 self.delayedevent.timeout = setTimeout(function(){
354 self.delayedevent.timeout = null;
355 self.delayedevent.event.detach();
356 if (options.iscontained == self.nodes.dock.contains(self.delayedevent.target)) {
357 self.handleEvent(event, {cssselector:options.cssselector, delay:0, iscontained:options.iscontained});
358 }
359 }, options.delay*1000);
360 return true;
edc858dc 361};
7e4617f7
SH
362/**
363 * Corrects the orientation of the title, which for the default
364 * dock just means making it vertical
365 * The orientation is determined by M.str.langconfig.thisdirectionvertical:
366 * ver : Letters are stacked rather than rotated
367 * ttb : Title is rotated clockwise so the first letter is at the top
368 * btt : Title is rotated counterclockwise so the first letter is at the bottom.
369 * @param {string} title
370 */
10a995c1 371M.core_dock.fixTitleOrientation = function(item, title, text) {
7e4617f7
SH
372 var Y = this.Y;
373
374 var title = Y.one(title);
6d5f8015 375
7e4617f7
SH
376 if (Y.UA.ie > 0 && Y.UA.ie < 8) {
377 // IE 6/7 can't rotate text so force ver
378 M.str.langconfig.thisdirectionvertical = 'ver';
379 }
380
381 var clockwise = false;
382 switch (M.str.langconfig.thisdirectionvertical) {
383 case 'ver':
384 // Stacked is easy
385 return title.setContent(text.split('').join('<br />'));
386 case 'ttb':
387 clockwise = true;
388 break;
389 case 'btt':
390 clockwise = false;
391 break;
392 }
393
394 if (Y.UA.ie > 7) {
395 // IE8 can flip the text via CSS but not handle SVG
edc858dc 396 title.setContent(text);
7e4617f7
SH
397 title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;');
398 title.addClass('filterrotate');
399 return title;
400 }
401
402 // Cool, we can use SVG!
0999f864 403 var test = Y.Node.create('<h2><span style="font-size:10px;">'+text+'</span></h2>');
7e4617f7
SH
404 this.nodes.body.append(test);
405 var height = test.one('span').get('offsetWidth')+4;
406 var width = test.one('span').get('offsetHeight')*2;
407 var qwidth = width/4;
408 test.remove();
409
410 // Create the text for the SVG
411 var txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
412 txt.setAttribute('font-size','10px');
413 if (clockwise) {
414 txt.setAttribute('transform','rotate(90 '+(qwidth/2)+' '+qwidth+')');
415 } else {
416 txt.setAttribute('y', height);
417 txt.setAttribute('transform','rotate(270 '+qwidth+' '+(height-qwidth)+')');
418 }
419 txt.appendChild(document.createTextNode(text));
420
421 var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
422 svg.setAttribute('version', '1.1');
423 svg.setAttribute('height', height);
424 svg.setAttribute('width', width);
425 svg.appendChild(txt);
426
edc858dc 427 title.append(svg);
10a995c1
SH
428
429 item.on('dockeditem:drawcomplete', function(txt, title){
430 txt.setAttribute('fill', Y.one(title).getStyle('color'));
431 }, item, txt, title);
432
7e4617f7 433 return title;
edc858dc 434};
7e4617f7
SH
435/**
436 * Resizes the space that contained blocks if there were no blocks left in
437 * it. e.g. if all blocks have been moved to the dock
438 * @param {Y.Node} node
439 */
440M.core_dock.resizeBlockSpace = function(node) {
441
442 if (this.Y.all('.block.dock_on_load').size()>0) {
443 // Do not resize during initial load
444 return;
445 }
446 var blockregions = [];
447 var populatedblockregions = 0;
448 this.Y.all('.block-region').each(function(region){
449 var hasblocks = (region.all('.block').size() > 0);
450 if (hasblocks) {
451 populatedblockregions++;
452 }
453 blockregions[region.get('id')] = {hasblocks: hasblocks, bodyclass: region.get('id').replace(/^region\-/, 'side-')+'-only'};
454 });
455 var bodynode = M.core_dock.nodes.body;
456 var noblocksbodyclass = 'content-only';
457 var i = null;
458 if (populatedblockregions==0) {
459 bodynode.addClass(noblocksbodyclass);
460 for (i in blockregions) {
461 bodynode.removeClass(blockregions[i].bodyclass);
462 }
463 } else if (populatedblockregions==1) {
464 bodynode.removeClass(noblocksbodyclass);
465 for (i in blockregions) {
466 if (!blockregions[i].hasblocks) {
467 bodynode.removeClass(blockregions[i].bodyclass);
468 } else {
469 bodynode.addClass(blockregions[i].bodyclass);
470 }
471 }
472 } else {
473 bodynode.removeClass(noblocksbodyclass);
474 for (i in blockregions) {
475 bodynode.removeClass(blockregions[i].bodyclass);
476 }
477 }
edc858dc 478};
7e4617f7
SH
479/**
480 * Adds a dock item into the dock
481 * @function
482 * @param {M.core_dock.item} item
483 */
484M.core_dock.add = function(item) {
485 item.id = this.totalcount;
486 this.count++;
487 this.totalcount++;
488 this.items[item.id] = item;
489 this.items[item.id].draw();
490 this.fire('dock:itemadded', item);
491 this.fire('dock:itemschanged', item);
edc858dc 492};
7e4617f7
SH
493/**
494 * Appends a dock item to the dock
495 * @param {YUI.Node} docknode
496 */
497M.core_dock.append = function(docknode) {
498 this.nodes.container.append(docknode);
edc858dc 499};
7e4617f7
SH
500/**
501 * Initialises a generic block object
502 * @param {YUI} Y
503 * @param {int} id
504 */
505M.core_dock.init_genericblock = function(Y, id) {
506 if (!this.initialised) {
507 this.init(Y);
508 }
509 new this.genericblock(id).init(Y, Y.one('#inst'+id));
edc858dc 510};
7e4617f7
SH
511/**
512 * Removes the node at the given index and puts it back into conventional page sturcture
513 * @function
514 * @param {int} uid Unique identifier for the block
515 * @return {boolean}
516 */
517M.core_dock.remove = function(uid) {
518 if (!this.items[uid]) {
519 return false;
520 }
521 this.items[uid].remove();
522 delete this.items[uid];
523 this.count--;
524 this.fire('dock:itemremoved', uid);
525 this.fire('dock:itemschanged', uid);
526 return true;
edc858dc 527};
4e2e816a
SH
528/**
529 * Ensures the the first item in the dock has the correct class
530 */
531M.core_dock.resetFirstItem = function() {
532 this.nodes.dock.all('.'+this.css.dockeditem+'.firstdockitem').removeClass('firstdockeditem');
533 if (this.nodes.dock.one('.'+this.css.dockeditem)) {
534 this.nodes.dock.one('.'+this.css.dockeditem).addClass('firstdockitem');
535 }
edc858dc 536};
7e4617f7
SH
537/**
538 * Removes all nodes and puts them back into conventional page sturcture
539 * @function
540 * @return {boolean}
541 */
542M.core_dock.remove_all = function() {
543 for (var i in this.items) {
544 this.remove(i);
545 }
546 return true;
edc858dc 547};
7e4617f7
SH
548/**
549 * Hides the active item
550 */
551M.core_dock.hideActive = function() {
552 var item = this.getActiveItem();
553 if (item) {
554 item.hide();
555 }
edc858dc 556};
7e4617f7
SH
557/**
558 * Checks wether the dock should be shown or hidden
559 */
560M.core_dock.checkDockVisibility = function() {
561 if (!this.count) {
562 this.nodes.dock.addClass('nothingdocked');
563 this.nodes.body.removeClass(this.css.body);
564 this.fire('dock:hidden');
565 } else {
566 this.fire('dock:beforeshow');
567 this.nodes.dock.removeClass('nothingdocked');
568 this.nodes.body.addClass(this.css.body);
569 this.fire('dock:shown');
570 }
edc858dc 571};
7e4617f7
SH
572/**
573 * This smart little function allows developers to attach event listeners before
574 * the dock has been augmented to allows event listeners.
575 * Once the augmentation is complete this function will be replaced with the proper
576 * on method for handling event listeners.
577 * Finally applyBinds needs to be called in order to properly bind events.
578 * @param {string} event
579 * @param {function} callback
580 */
581M.core_dock.on = function(event, callback) {
582 this.earlybinds.push({event:event,callback:callback});
edc858dc 583};
7e4617f7
SH
584/**
585 * This function takes all early binds and attaches them as listeners properly
586 * This should only be called once augmentation is complete.
587 */
588M.core_dock.applyBinds = function() {
589 for (var i in this.earlybinds) {
590 var bind = this.earlybinds[i];
591 this.on(bind.event, bind.callback);
592 }
593 this.earlybinds = [];
edc858dc 594};
7e4617f7
SH
595/**
596 * This function checks the size and position of the panel and moves/resizes if
597 * required to keep it within the bounds of the window.
598 */
599M.core_dock.resize = function() {
600 this.fire('dock:panelresizestart');
601 var panel = this.getPanel();
602 var item = this.getActiveItem();
603 if (!panel.visible || !item) {
604 return;
605 }
606 var buffer = this.cfg.buffer;
607 var screenheight = parseInt(this.nodes.body.get('winHeight'))-(buffer*2);
b1c8c40e
SH
608 var docky = this.nodes.dock.getY();
609 var titletop = item.nodes.docktitle.getY()-docky-buffer;
610 var containery = this.nodes.container.getY();
611 var containerheight = containery-docky+this.nodes.container.get('offsetHeight');
0999f864
SH
612 panel.contentBody.setStyle('height', 'auto');
613 panel.removeClass('oversized_content');
7e4617f7
SH
614 var panelheight = panel.get('offsetHeight');
615
616 if (panelheight > screenheight) {
617 panel.setStyle('top', (buffer-containerheight)+'px');
0999f864
SH
618 panel.contentBody.setStyle('height', (screenheight-panel.contentHeader.get('offsetHeight'))+'px');
619 panel.addClass('oversized_content');
7e4617f7
SH
620 } else if (panelheight > (screenheight-(titletop-buffer))) {
621 var difference = panelheight - (screenheight-titletop);
622 panel.setStyle('top', (titletop-containerheight-difference+buffer)+'px');
623 } else {
624 panel.setStyle('top', (titletop-containerheight+buffer)+'px');
625 }
626 this.fire('dock:resizepanelcomplete');
627 return;
edc858dc 628};
7e4617f7
SH
629/**
630 * Returns the currently active dock item or false
631 */
632M.core_dock.getActiveItem = function() {
633 for (var i in this.items) {
634 if (this.items[i].active) {
635 return this.items[i];
636 }
637 }
638 return false;
edc858dc 639};
6d5f8015
SH
640/**
641 * This class represents a generic block
7e4617f7 642 * @class M.core_dock.genericblock
6d5f8015
SH
643 * @constructor
644 */
7e4617f7 645M.core_dock.genericblock = function(id) {
6d5f8015 646 // Nothing to actually do here but it needs a constructor!
7e4617f7
SH
647 if (id) {
648 this.id = id;
649 }
6d5f8015
SH
650};
651M.core_dock.genericblock.prototype = {
652 Y : null, // A YUI instance to use with the block
653 id : null, // The block instance id
654 cachedcontentnode : null, // The cached content node for the actual block
655 blockspacewidth : null, // The width of the block's original container
656 skipsetposition : false, // If true the user preference isn't updated
7e4617f7 657 isdocked : false, // True if it is docked
1ce15fda 658 /**
6d5f8015
SH
659 * This function should be called within the block's constructor and is used to
660 * set up the initial controls for swtiching block position as well as an initial
661 * moves that may be required.
662 *
663 * @param {YUI} Y
664 * @param {YUI.Node} node The node that contains all of the block's content
7e4617f7 665 * @return {M.core_dock.genericblock}
1ce15fda 666 */
6d5f8015 667 init : function(Y, node) {
7e4617f7
SH
668 M.core_dock.init(Y);
669
6d5f8015
SH
670 this.Y = Y;
671 if (!node) {
7e4617f7 672 return false;
6d5f8015 673 }
53fc3e70 674
6d5f8015
SH
675 var commands = node.one('.header .title .commands');
676 if (!commands) {
677 commands = this.Y.Node.create('<div class="commands"></div>');
678 if (node.one('.header .title')) {
679 node.one('.header .title').append(commands);
1ce15fda 680 }
6d5f8015 681 }
1ce15fda 682
8c29a17d
SH
683 // Must set the image src seperatly of we get an error with XML strict headers
684 var moveto = Y.Node.create('<input type="image" class="moveto customcommand requiresjs" alt="'+M.str.block.addtodock+'" title="'+M.str.block.addtodock+'" />');
685 moveto.setAttribute('src', M.util.image_url('t/block_to_dock', 'moodle'));
6d5f8015 686 moveto.on('movetodock|click', this.move_to_dock, this, commands);
1ce15fda 687
6d5f8015
SH
688 var blockaction = node.one('.block_action');
689 if (blockaction) {
690 blockaction.prepend(moveto);
691 } else {
692 commands.append(moveto);
693 }
46de77b6 694
6d5f8015
SH
695 // Move the block straight to the dock if required
696 if (node.hasClass('dock_on_load')) {
edc858dc 697 node.removeClass('dock_on_load');
6d5f8015
SH
698 this.skipsetposition = true;
699 this.move_to_dock(null, commands);
700 }
7e4617f7 701 return this;
6d5f8015 702 },
1ce15fda 703
6d5f8015
SH
704 /**
705 * This function is reponsible for moving a block from the page structure onto the
706 * dock
707 * @param {event}
708 */
709 move_to_dock : function(e, commands) {
710 if (e) {
711 e.halt(true);
712 }
1ce15fda 713
7e4617f7 714 var Y = this.Y;
edc858dc 715 var dock = M.core_dock;
7e4617f7
SH
716
717 var node = Y.one('#inst'+this.id);
6d5f8015
SH
718 var blockcontent = node.one('.content');
719 if (!blockcontent) {
720 return;
721 }
1ce15fda 722
6d5f8015
SH
723 var blockclass = (function(classes){
724 var r = /(^|\s)(block_[a-zA-Z0-9_]+)(\s|$)/;
725 var m = r.exec(classes);
726 return (m)?m[2]:m;
727 })(node.getAttribute('className').toString());
1ce15fda 728
6d5f8015 729 this.cachedcontentnode = node;
90723839 730
7e4617f7 731 node.replace(Y.Node.getDOMNode(Y.Node.create('<div id="content_placeholder_'+this.id+'" class="block_dock_placeholder"></div>')));
673835d4 732 M.core_dock.holdingarea.append(node);
6d5f8015 733 node = null;
1ce15fda 734
7e4617f7 735 var blocktitle = Y.Node.getDOMNode(this.cachedcontentnode.one('.title h2')).cloneNode(true);
1ce15fda 736
6d5f8015
SH
737 var blockcommands = this.cachedcontentnode.one('.title .commands');
738 if (!blockcommands) {
7e4617f7 739 blockcommands = Y.Node.create('<div class="commands"></div>');
6d5f8015
SH
740 this.cachedcontentnode.one('.title').append(blockcommands);
741 }
8c29a17d
SH
742
743 // Must set the image src seperatly of we get an error with XML strict headers
744 var movetoimg = Y.Node.create('<img alt="'+M.str.block.undockitem+'" title="'+M.str.block.undockitem+'" />');
745 movetoimg.setAttribute('src', M.util.image_url('t/dock_to_block', 'moodle'));
746 var moveto = Y.Node.create('<a class="moveto customcommand requiresjs"></a>').append(movetoimg);
6d5f8015
SH
747 if (location.href.match(/\?/)) {
748 moveto.set('href', location.href+'&dock='+this.id);
749 } else {
750 moveto.set('href', location.href+'?dock='+this.id);
751 }
752 blockcommands.append(moveto);
1ce15fda 753
6d5f8015 754 // Create a new dock item for the block
7e4617f7 755 var dockitem = new dock.item(Y, this.id, blocktitle, blockcontent, blockcommands, blockclass);
6d5f8015
SH
756 // Wire the draw events to register remove events
757 dockitem.on('dockeditem:drawcomplete', function(e){
758 // check the contents block [editing=off]
759 this.contents.all('.moveto').on('returntoblock|click', function(e){
760 e.halt();
edc858dc 761 dock.remove(this.id);
6d5f8015
SH
762 }, this);
763 // check the commands block [editing=on]
764 this.commands.all('.moveto').on('returntoblock|click', function(e){
765 e.halt();
edc858dc 766 dock.remove(this.id);
6d5f8015
SH
767 }, this);
768 // Add a close icon
8c29a17d
SH
769 // Must set the image src seperatly of we get an error with XML strict headers
770 var closeicon = Y.Node.create('<span class="hidepanelicon"><img alt="" style="width:11px;height:11px;cursor:pointer;" /></span>');
771 closeicon.one('img').setAttribute('src', M.util.image_url('t/dockclose', 'moodle'));
7e4617f7 772 closeicon.on('forceclose|click', this.hide, this);
6d5f8015
SH
773 this.commands.append(closeicon);
774 }, dockitem);
6d5f8015 775 // Register an event so that when it is removed we can put it back as a block
7e4617f7
SH
776 dockitem.on('dockeditem:itemremoved', this.return_to_block, this, dockitem);
777 dock.add(dockitem);
778
6d5f8015
SH
779 if (!this.skipsetposition) {
780 // save the users preference
781 M.util.set_user_preference('docked_block_instance_'+this.id, 1);
782 } else {
783 this.skipsetposition = false;
784 }
1ce15fda 785
7e4617f7 786 this.isdocked = true;
1ce15fda
SH
787 },
788 /**
6d5f8015
SH
789 * This function removes a block from the dock and puts it back into the page
790 * structure.
791 * @param {M.core_dock.class.item}
1ce15fda 792 */
6d5f8015
SH
793 return_to_block : function(dockitem) {
794 var placeholder = this.Y.one('#content_placeholder_'+this.id);
1ce15fda 795
6d5f8015
SH
796 if (this.cachedcontentnode.one('.header')) {
797 this.cachedcontentnode.one('.header').insert(dockitem.contents, 'after');
798 } else {
799 this.cachedcontentnode.insert(dockitem.contents);
800 }
1ce15fda 801
6d5f8015
SH
802 placeholder.replace(this.Y.Node.getDOMNode(this.cachedcontentnode));
803 this.cachedcontentnode = this.Y.one('#'+this.cachedcontentnode.get('id'));
d2e68385 804
7e4617f7 805 var commands = this.cachedcontentnode.one('.title .commands');
6d5f8015
SH
806 if (commands) {
807 commands.all('.hidepanelicon').remove();
808 commands.all('.moveto').remove();
809 commands.remove();
1ce15fda 810 }
6d5f8015
SH
811 this.cachedcontentnode.one('.title').append(commands);
812 this.cachedcontentnode = null;
813 M.util.set_user_preference('docked_block_instance_'+this.id, 0);
7e4617f7 814 this.isdocked = false;
6d5f8015 815 return true;
1ce15fda 816 }
edc858dc 817};
1ce15fda
SH
818
819/**
820 * This class represents an item in the dock
7e4617f7 821 * @class M.core_dock.item
1ce15fda 822 * @constructor
d2e68385 823 * @param {YUI} Y The YUI instance to use for this item
1ce15fda 824 * @param {int} uid The unique ID for the item
53fc3e70
SH
825 * @param {this.Y.Node} title
826 * @param {this.Y.Node} contents
827 * @param {this.Y.Node} commands
3406acde 828 * @param {string} blockclass
1ce15fda 829 */
90723839 830M.core_dock.item = function(Y, uid, title, contents, commands, blockclass){
53fc3e70 831 this.Y = Y;
10a995c1
SH
832 this.publish('dockeditem:drawstart', {prefix:'dockeditem'});
833 this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'});
834 this.publish('dockeditem:showstart', {prefix:'dockeditem'});
835 this.publish('dockeditem:showcomplete', {prefix:'dockeditem'});
836 this.publish('dockeditem:hidestart', {prefix:'dockeditem'});
837 this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'});
838 this.publish('dockeditem:itemremoved', {prefix:'dockeditem'});
53fc3e70
SH
839 if (uid && this.id==null) {
840 this.id = uid;
841 }
842 if (title && this.title==null) {
7e4617f7
SH
843 this.titlestring = title.cloneNode(true);
844 this.title = document.createElement(title.nodeName);
10a995c1 845 M.core_dock.fixTitleOrientation(this, this.title, this.titlestring.firstChild.nodeValue);
53fc3e70
SH
846 }
847 if (contents && this.contents==null) {
848 this.contents = contents;
849 }
850 if (commands && this.commands==null) {
851 this.commands = commands;
852 }
90723839 853 if (blockclass && this.blockclass==null) {
edc858dc 854 this.blockclass = blockclass;
90723839 855 }
7e4617f7 856 this.nodes = (function(){
edc858dc 857 return {docktitle : null, dockitem : null, container: null};
7e4617f7 858 })();
edc858dc 859};
6d5f8015
SH
860/**
861 *
862 */
863M.core_dock.item.prototype = {
864 Y : null, // The YUI instance to use with this dock item
865 id : null, // The unique id for the item
866 name : null, // The name of the item
867 title : null, // The title of the item
7e4617f7 868 titlestring : null, // The title as a plain string
6d5f8015
SH
869 contents : null, // The content of the item
870 commands : null, // The commands for the item
871 active : false, // True if the item is being shown
7e4617f7
SH
872 blockclass : null, // The class of the block this item relates to
873 nodes : null,
6d5f8015
SH
874 /**
875 * This function draws the item on the dock
876 */
877 draw : function() {
878 this.fire('dockeditem:drawstart');
6d5f8015 879
7e4617f7
SH
880 var Y = this.Y;
881 var css = M.core_dock.css;
882
883 this.nodes.docktitle = Y.Node.create('<div id="dock_item_'+this.id+'_title" class="'+css.dockedtitle+'"></div>');
884 this.nodes.docktitle.append(this.title);
885 this.nodes.dockitem = Y.Node.create('<div id="dock_item_'+this.id+'" class="'+css.dockeditem+'"></div>');
886 if (M.core_dock.count === 1) {
887 this.nodes.dockitem.addClass('firstdockitem');
6d5f8015 888 }
7e4617f7
SH
889 this.nodes.dockitem.append(this.nodes.docktitle);
890 M.core_dock.append(this.nodes.dockitem);
6d5f8015 891 this.fire('dockeditem:drawcomplete');
7e4617f7 892 return true;
6d5f8015
SH
893 },
894 /**
895 * This function toggles makes the item active and shows it
6d5f8015 896 */
7e4617f7
SH
897 show : function() {
898 M.core_dock.hideActive();
899 var Y = this.Y;
900 var css = M.core_dock.css;
901 var panel = M.core_dock.getPanel();
6d5f8015 902 this.fire('dockeditem:showstart');
7e4617f7
SH
903 panel.setHeader(this.titlestring, this.commands);
904 panel.setBody(Y.Node.create('<div class="'+this.blockclass+' block_docked"></div>').append(this.contents));
905 panel.show();
906
6d5f8015
SH
907 this.active = true;
908 // Add active item class first up
7e4617f7 909 this.nodes.docktitle.addClass(css.activeitem);
6d5f8015 910 this.fire('dockeditem:showcomplete');
7e4617f7 911 M.core_dock.resize();
6d5f8015
SH
912 return true;
913 },
914 /**
915 * This function hides the item and makes it inactive
6d5f8015 916 */
7e4617f7
SH
917 hide : function() {
918 var css = M.core_dock.css;
919 this.fire('dockeditem:hidestart');
920 // No longer active
921 this.active = false;
922 // Remove the active class
923 this.nodes.docktitle.removeClass(css.activeitem);
924 // Hide the panel
925 M.core_dock.getPanel().hide();
926 this.fire('dockeditem:hidecomplete');
6d5f8015
SH
927 },
928 /**
7e4617f7 929 * This function removes the node and destroys it's bits
6d5f8015
SH
930 * @param {Event} e
931 */
7e4617f7
SH
932 remove : function () {
933 this.hide();
934 this.nodes.dockitem.remove();
935 this.fire('dockeditem:itemremoved');
6d5f8015 936 }
edc858dc 937};