641a1e19576e575704e6b656a0d0b039a8350225
[moodle.git] / blocks / dock.js
1 /**
2  * The dock namespace: Contains all things dock related
3  * @namespace
4  */
5 M.core_dock = {
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
13     preventevent : null,    // Will be an eventtype if there is an eventyoe to prevent
14     holdingarea : null
15 };
16 /**
17  * Namespace containing the nodes that relate to the dock
18  * @namespace
19  */
20 M.core_dock.nodes = {
21     dock : null, // The dock itself
22     body : null, // The body of the page
23     panel : null // The docks panel
24 };
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  */
32 M.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')
38 };
39 /**
40  * CSS classes to use with the dock
41  * @namespace
42  */
43 M.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
52 };
53 /**
54  * Augments the classes as required and processes early bindings
55  */
56 M.core_dock.init = function(Y) {
57     if (this.initialised) {
58         return true;
59     }
60     var css = this.css;
61     this.initialised = true;
62     this.Y = Y;
63     this.nodes.body = Y.one(document.body);
65     // Give the dock item class the event properties/methods
66     Y.augment(this.item, Y.EventTarget);
67     Y.augment(this, Y.EventTarget, true);
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
92         }
93     }
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     }
104     this.holdingarea = Y.Node.create('<div></div>').setStyles({display:'none'});
105     this.nodes.body.append(this.holdingarea);
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);
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     }
121     this.fire('dock:beforedraw');
123     // Add a removeall button
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);
127     removeall.on('removeall|click', this.remove_all, this);
128     dock.appendChild(Y.Node.create('<div class="'+css.controls+'"></div>').append(removeall));
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                 }
187             }
188         };
189     })(Y).init();
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});
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});
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);
202     this.on('dock:itemschanged', this.resetFirstItem, this);
203     // Inform everyone the dock has been initialised
204     this.fire('dock:initialised');
205     return true;
206 };
207 /**
208  * Get the panel docked blocks will be shown in and initialise it if we havn't already.
209  */
210 M.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');
237             };
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');
245             };
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]);
252                     }
253                 }
254             };
255             // Add a method to set the body content
256             dockpanel.setBody = function(content) {
257                 this.contentBody.setContent(content);
258             };
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;
266                 }
267                 this.setStyle('top', newtop+'px');
268             };
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;
279 };
280 /**
281  * Handles a generic event within the dock
282  * @param {Y.Event} e
283  * @param {object} options Event configuration object
284  */
285 M.core_dock.handleEvent = function(e, options) {
286     var item = this.getActiveItem();
287     var target = (e.target.test(options.cssselector))?e.target:e.target.ancestor(options.cssselector);
288     if (options.cssselector == 'body') {
289         if (!this.nodes.dock.contains(e.target)) {
290             if (item) {
291                 item.hide();
292             }
293         }
294     } else if (target) {
295         if (this.preventevent !== null && e.type === this.preventevent) {
296             return true;
297         }
298         if (options.preventevent) {
299             this.preventevent = options.preventevent;
300             if (options.preventdelay) {
301                 setTimeout(function(){M.core_dock.preventevent = null;}, options.preventdelay*1000);
302             }
303         }
304         if (this.delayedevent && this.delayedevent.timeout) {
305             clearTimeout(this.delayedevent.timeout);
306             this.delayedevent.event.detach();
307             this.delayedevent = null;
308         }
309         if (options.delay > 0) {
310             return this.delayEvent(e, options, target);
311         }
312         var targetid = target.get('id');
313         if (targetid.match(/^dock_item_(\d+)_title$/)) {
314             item = this.items[targetid.replace(/^dock_item_(\d+)_title$/, '$1')];
315             if (item.active) {
316                 item.hide();
317             } else {
318                 item.show();
319             }
320         } else if (item) {
321             item.hide();
322         }
323     }
324     return true;
325 };
326 /**
327  * This function delays an event and then fires it providing the cursor if either
328  * within or outside of the original target (options.iscontained=true|false)
329  * @param {Y.Event} event
330  * @param {object} options
331  * @param {Y.Node} target
332  * @return bool
333  */
334 M.core_dock.delayEvent = function(event, options, target) {
335     var self = this;
336     self.delayedevent = (function(){
337         return {
338             target : target,
339             event : self.nodes.body.on('mousemove', function(e){
340                 self.delayedevent.target = e.target;
341             }),
342             timeout : null
343         };
344     })(self);
345     self.delayedevent.timeout = setTimeout(function(){
346         self.delayedevent.timeout = null;
347         self.delayedevent.event.detach();
348         if (options.iscontained == self.nodes.dock.contains(self.delayedevent.target)) {
349             self.handleEvent(event, {cssselector:options.cssselector, delay:0, iscontained:options.iscontained});
350         }
351     }, options.delay*1000);
352     return true;
353 };
354 /**
355  * Corrects the orientation of the title, which for the default
356  * dock just means making it vertical
357  * The orientation is determined by M.str.langconfig.thisdirectionvertical:
358  *    ver : Letters are stacked rather than rotated
359  *    ttb : Title is rotated clockwise so the first letter is at the top
360  *    btt : Title is rotated counterclockwise so the first letter is at the bottom.
361  * @param {string} title
362  */
363 M.core_dock.fixTitleOrientation = function(item, title, text) {
364     var Y = this.Y;
366     var title = Y.one(title);
368     if (Y.UA.ie > 0 && Y.UA.ie < 8) {
369         // IE 6/7 can't rotate text so force ver
370         M.str.langconfig.thisdirectionvertical = 'ver';
371     }
373     var clockwise = false;
374     switch (M.str.langconfig.thisdirectionvertical) {
375         case 'ver':
376             // Stacked is easy
377             return title.setContent(text.split('').join('<br />'));
378         case 'ttb':
379             clockwise = true;
380             break;
381         case 'btt':
382             clockwise = false;
383             break;
384     }
386     if (Y.UA.ie > 7) {
387         // IE8 can flip the text via CSS but not handle SVG
388         title.setContent(text);
389         title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;');
390         title.addClass('filterrotate');
391         return title;
392     }
394     // Cool, we can use SVG!
395     var test = Y.Node.create('<h2><span style="font-size:10px;">'+text+'</span></h2>');
396     this.nodes.body.append(test);
397     var height = test.one('span').get('offsetWidth')+4;
398     var width = test.one('span').get('offsetHeight')*2;
399     var qwidth = width/4;
400     test.remove();
402     // Create the text for the SVG
403     var txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
404     txt.setAttribute('font-size','10px');
405     if (clockwise) {
406         txt.setAttribute('transform','rotate(90 '+(qwidth/2)+' '+qwidth+')');
407     } else {
408         txt.setAttribute('y', height);
409         txt.setAttribute('transform','rotate(270 '+qwidth+' '+(height-qwidth)+')');
410     }
411     txt.appendChild(document.createTextNode(text));
413     var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
414     svg.setAttribute('version', '1.1');
415     svg.setAttribute('height', height);
416     svg.setAttribute('width', width);
417     svg.appendChild(txt);
419     title.append(svg);
421     item.on('dockeditem:drawcomplete', function(txt, title){
422         txt.setAttribute('fill', Y.one(title).getStyle('color'));
423     }, item, txt, title);
425     return title;
426 };
427 /**
428  * Resizes the space that contained blocks if there were no blocks left in
429  * it. e.g. if all blocks have been moved to the dock
430  * @param {Y.Node} node
431  */
432 M.core_dock.resizeBlockSpace = function(node) {
434     if (this.Y.all('.block.dock_on_load').size()>0) {
435         // Do not resize during initial load
436         return;
437     }
438     var blockregions = [];
439     var populatedblockregions = 0;
440     this.Y.all('.block-region').each(function(region){
441         var hasblocks = (region.all('.block').size() > 0);
442         if (hasblocks) {
443             populatedblockregions++;
444         }
445         blockregions[region.get('id')] = {hasblocks: hasblocks, bodyclass: region.get('id').replace(/^region\-/, 'side-')+'-only'};
446     });
447     var bodynode = M.core_dock.nodes.body;
448     var noblocksbodyclass = 'content-only';
449     var i = null;
450     if (populatedblockregions==0) {
451         bodynode.addClass(noblocksbodyclass);
452         for (i in blockregions) {
453             bodynode.removeClass(blockregions[i].bodyclass);
454         }
455     } else if (populatedblockregions==1) {
456         bodynode.removeClass(noblocksbodyclass);
457         for (i in blockregions) {
458             if (!blockregions[i].hasblocks) {
459                 bodynode.removeClass(blockregions[i].bodyclass);
460             } else {
461                 bodynode.addClass(blockregions[i].bodyclass);
462             }
463         }
464     } else {
465         bodynode.removeClass(noblocksbodyclass);
466         for (i in blockregions) {
467             bodynode.removeClass(blockregions[i].bodyclass);
468         }
469     }
470 };
471 /**
472  * Adds a dock item into the dock
473  * @function
474  * @param {M.core_dock.item} item
475  */
476 M.core_dock.add = function(item) {
477     item.id = this.totalcount;
478     this.count++;
479     this.totalcount++;
480     this.items[item.id] = item;
481     this.items[item.id].draw();
482     this.fire('dock:itemadded', item);
483     this.fire('dock:itemschanged', item);
484 };
485 /**
486  * Appends a dock item to the dock
487  * @param {YUI.Node} docknode
488  */
489 M.core_dock.append = function(docknode) {
490     this.nodes.container.append(docknode);
491 };
492 /**
493  * Initialises a generic block object
494  * @param {YUI} Y
495  * @param {int} id
496  */
497 M.core_dock.init_genericblock = function(Y, id) {
498     if (!this.initialised) {
499         this.init(Y);
500     }
501     new this.genericblock(id).init(Y, Y.one('#inst'+id));
502 };
503 /**
504  * Removes the node at the given index and puts it back into conventional page sturcture
505  * @function
506  * @param {int} uid Unique identifier for the block
507  * @return {boolean}
508  */
509 M.core_dock.remove = function(uid) {
510     if (!this.items[uid]) {
511         return false;
512     }
513     this.items[uid].remove();
514     delete this.items[uid];
515     this.count--;
516     this.fire('dock:itemremoved', uid);
517     this.fire('dock:itemschanged', uid);
518     return true;
519 };
520 /**
521  * Ensures the the first item in the dock has the correct class
522  */
523 M.core_dock.resetFirstItem = function() {
524     this.nodes.dock.all('.'+this.css.dockeditem+'.firstdockitem').removeClass('firstdockeditem');
525     if (this.nodes.dock.one('.'+this.css.dockeditem)) {
526         this.nodes.dock.one('.'+this.css.dockeditem).addClass('firstdockitem');
527     }
528 };
529 /**
530  * Removes all nodes and puts them back into conventional page sturcture
531  * @function
532  * @return {boolean}
533  */
534 M.core_dock.remove_all = function() {
535     for (var i in this.items) {
536         this.remove(i);
537     }
538     return true;
539 };
540 /**
541  * Hides the active item
542  */
543 M.core_dock.hideActive = function() {
544     var item = this.getActiveItem();
545     if (item) {
546         item.hide();
547     }
548 };
549 /**
550  * Checks wether the dock should be shown or hidden
551  */
552 M.core_dock.checkDockVisibility = function() {
553     if (!this.count) {
554         this.nodes.dock.addClass('nothingdocked');
555         this.nodes.body.removeClass(this.css.body);
556         this.fire('dock:hidden');
557     } else {
558         this.fire('dock:beforeshow');
559         this.nodes.dock.removeClass('nothingdocked');
560         this.nodes.body.addClass(this.css.body);
561         this.fire('dock:shown');
562     }
563 };
564 /**
565  * This smart little function allows developers to attach event listeners before
566  * the dock has been augmented to allows event listeners.
567  * Once the augmentation is complete this function will be replaced with the proper
568  * on method for handling event listeners.
569  * Finally applyBinds needs to be called in order to properly bind events.
570  * @param {string} event
571  * @param {function} callback
572  */
573 M.core_dock.on = function(event, callback) {
574     this.earlybinds.push({event:event,callback:callback});
575 };
576 /**
577  * This function takes all early binds and attaches them as listeners properly
578  * This should only be called once augmentation is complete.
579  */
580 M.core_dock.applyBinds = function() {
581     for (var i in this.earlybinds) {
582         var bind = this.earlybinds[i];
583         this.on(bind.event, bind.callback);
584     }
585     this.earlybinds = [];
586 };
587 /**
588  * This function checks the size and position of the panel and moves/resizes if
589  * required to keep it within the bounds of the window.
590  */
591 M.core_dock.resize = function() {
592     this.fire('dock:panelresizestart');
593     var panel = this.getPanel();
594     var item = this.getActiveItem();
595     if (!panel.visible || !item) {
596         return;
597     }
598     var buffer = this.cfg.buffer;
599     var screenheight = parseInt(this.nodes.body.get('winHeight'))-(buffer*2);
600     var docky = this.nodes.dock.getY();
601     var titletop = item.nodes.docktitle.getY()-docky-buffer;
602     var containery = this.nodes.container.getY();
603     var containerheight = containery-docky+this.nodes.container.get('offsetHeight');
604     panel.contentBody.setStyle('height', 'auto');
605     panel.removeClass('oversized_content');
606     var panelheight = panel.get('offsetHeight');
608     if (panelheight > screenheight) {
609         panel.setStyle('top', (buffer-containerheight)+'px');
610         panel.contentBody.setStyle('height', (screenheight-panel.contentHeader.get('offsetHeight'))+'px');
611         panel.addClass('oversized_content');
612     } else if (panelheight > (screenheight-(titletop-buffer))) {
613         var difference = panelheight - (screenheight-titletop);
614         panel.setStyle('top', (titletop-containerheight-difference+buffer)+'px');
615     } else {
616         panel.setStyle('top', (titletop-containerheight+buffer)+'px');
617     }
618     this.fire('dock:resizepanelcomplete');
619     return;
620 };
621 /**
622  * Returns the currently active dock item or false
623  */
624 M.core_dock.getActiveItem = function() {
625     for (var i in this.items) {
626         if (this.items[i].active) {
627             return this.items[i];
628         }
629     }
630     return false;
631 };
632 /**
633  * This class represents a generic block
634  * @class M.core_dock.genericblock
635  * @constructor
636  */
637 M.core_dock.genericblock = function(id) {
638     // Nothing to actually do here but it needs a constructor!
639     if (id) {
640         this.id = id;
641     }
642 };
643 M.core_dock.genericblock.prototype = {
644     Y : null,                   // A YUI instance to use with the block
645     id : null,                  // The block instance id
646     cachedcontentnode : null,   // The cached content node for the actual block
647     blockspacewidth : null,     // The width of the block's original container
648     skipsetposition : false,    // If true the user preference isn't updated
649     isdocked : false,           // True if it is docked
650     /**
651      * This function should be called within the block's constructor and is used to
652      * set up the initial controls for swtiching block position as well as an initial
653      * moves that may be required.
654      *
655      * @param {YUI} Y
656      * @param {YUI.Node} node The node that contains all of the block's content
657      * @return {M.core_dock.genericblock}
658      */
659     init : function(Y, node) {
660         M.core_dock.init(Y);
661         
662         this.Y = Y;
663         if (!node) {
664             return false;
665         }
667         var commands = node.one('.header .title .commands');
668         if (!commands) {
669             commands = this.Y.Node.create('<div class="commands"></div>');
670             if (node.one('.header .title')) {
671                 node.one('.header .title').append(commands);
672             }
673         }
675         // Must set the image src seperatly of we get an error with XML strict headers
676         var moveto = Y.Node.create('<input type="image" class="moveto customcommand requiresjs" alt="'+M.str.block.addtodock+'" title="'+M.str.block.addtodock+'" />');
677         moveto.setAttribute('src', M.util.image_url('t/block_to_dock', 'moodle'));
678         moveto.on('movetodock|click', this.move_to_dock, this, commands);
680         var blockaction = node.one('.block_action');
681         if (blockaction) {
682             blockaction.prepend(moveto);
683         } else {
684             commands.append(moveto);
685         }
687         // Move the block straight to the dock if required
688         if (node.hasClass('dock_on_load')) {
689             node.removeClass('dock_on_load');
690             this.skipsetposition = true;
691             this.move_to_dock(null, commands);
692         }
693         return this;
694     },
696     /**
697      * This function is reponsible for moving a block from the page structure onto the
698      * dock
699      * @param {event}
700      */
701     move_to_dock : function(e, commands) {
702         if (e) {
703             e.halt(true);
704         }
706         var Y = this.Y;
707         var dock = M.core_dock;
709         var node = Y.one('#inst'+this.id);
710         var blockcontent = node.one('.content');
711         if (!blockcontent) {
712             return;
713         }
715         var blockclass = (function(classes){
716             var r = /(^|\s)(block_[a-zA-Z0-9_]+)(\s|$)/;
717             var m = r.exec(classes);
718             return (m)?m[2]:m;
719         })(node.getAttribute('className').toString());
721         this.cachedcontentnode = node;
723         node.replace(Y.Node.getDOMNode(Y.Node.create('<div id="content_placeholder_'+this.id+'" class="block_dock_placeholder"></div>')));
724         M.core_dock.holdingarea.append(node);
725         node = null;
727         var blocktitle = Y.Node.getDOMNode(this.cachedcontentnode.one('.title h2')).cloneNode(true);
729         var blockcommands = this.cachedcontentnode.one('.title .commands');
730         if (!blockcommands) {
731             blockcommands = Y.Node.create('<div class="commands"></div>');
732             this.cachedcontentnode.one('.title').append(blockcommands);
733         }
735         // Must set the image src seperatly of we get an error with XML strict headers
736         var movetoimg = Y.Node.create('<img alt="'+M.str.block.undockitem+'" title="'+M.str.block.undockitem+'" />');
737         movetoimg.setAttribute('src', M.util.image_url('t/dock_to_block', 'moodle'));
738         var moveto = Y.Node.create('<a class="moveto customcommand requiresjs"></a>').append(movetoimg);
739         if (location.href.match(/\?/)) {
740             moveto.set('href', location.href+'&dock='+this.id);
741         } else {
742             moveto.set('href', location.href+'?dock='+this.id);
743         }
744         blockcommands.append(moveto);
746         // Create a new dock item for the block
747         var dockitem = new dock.item(Y, this.id, blocktitle, blockcontent, blockcommands, blockclass);
748         // Wire the draw events to register remove events
749         dockitem.on('dockeditem:drawcomplete', function(e){
750             // check the contents block [editing=off]
751             this.contents.all('.moveto').on('returntoblock|click', function(e){
752                 e.halt();
753                 dock.remove(this.id);
754             }, this);
755             // check the commands block [editing=on]
756             this.commands.all('.moveto').on('returntoblock|click', function(e){
757                 e.halt();
758                 dock.remove(this.id);
759             }, this);
760             // Add a close icon
761             // Must set the image src seperatly of we get an error with XML strict headers
762             var closeicon = Y.Node.create('<span class="hidepanelicon"><img alt="" style="width:11px;height:11px;cursor:pointer;" /></span>');
763             closeicon.one('img').setAttribute('src', M.util.image_url('t/dockclose', 'moodle'));
764             closeicon.on('forceclose|click', this.hide, this);
765             this.commands.append(closeicon);
766         }, dockitem);
767         // Register an event so that when it is removed we can put it back as a block
768         dockitem.on('dockeditem:itemremoved', this.return_to_block, this, dockitem);
769         dock.add(dockitem);
770         
771         if (!this.skipsetposition) {
772             // save the users preference
773             M.util.set_user_preference('docked_block_instance_'+this.id, 1);
774         } else {
775             this.skipsetposition = false;
776         }
778         this.isdocked = true;
779     },
780     /**
781      * This function removes a block from the dock and puts it back into the page
782      * structure.
783      * @param {M.core_dock.class.item}
784      */
785     return_to_block : function(dockitem) {
786         var placeholder = this.Y.one('#content_placeholder_'+this.id);
788         if (this.cachedcontentnode.one('.header')) {
789             this.cachedcontentnode.one('.header').insert(dockitem.contents, 'after');
790         } else {
791             this.cachedcontentnode.insert(dockitem.contents);
792         }
794         placeholder.replace(this.Y.Node.getDOMNode(this.cachedcontentnode));
795         this.cachedcontentnode = this.Y.one('#'+this.cachedcontentnode.get('id'));
797         var commands = this.cachedcontentnode.one('.title .commands');
798         if (commands) {
799             commands.all('.hidepanelicon').remove();
800             commands.all('.moveto').remove();
801             commands.remove();
802         }
803         this.cachedcontentnode.one('.title').append(commands);
804         this.cachedcontentnode = null;
805         M.util.set_user_preference('docked_block_instance_'+this.id, 0);
806         this.isdocked = false;
807         return true;
808     }
809 };
811 /**
812  * This class represents an item in the dock
813  * @class M.core_dock.item
814  * @constructor
815  * @param {YUI} Y The YUI instance to use for this item
816  * @param {int} uid The unique ID for the item
817  * @param {this.Y.Node} title
818  * @param {this.Y.Node} contents
819  * @param {this.Y.Node} commands
820  * @param {string} blockclass
821  */
822 M.core_dock.item = function(Y, uid, title, contents, commands, blockclass){
823     this.Y = Y;
824     this.publish('dockeditem:drawstart', {prefix:'dockeditem'});
825     this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'});
826     this.publish('dockeditem:showstart', {prefix:'dockeditem'});
827     this.publish('dockeditem:showcomplete', {prefix:'dockeditem'});
828     this.publish('dockeditem:hidestart', {prefix:'dockeditem'});
829     this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'});
830     this.publish('dockeditem:itemremoved', {prefix:'dockeditem'});
831     if (uid && this.id==null) {
832         this.id = uid;
833     }
834     if (title && this.title==null) {
835         this.titlestring = title.cloneNode(true);
836         this.title = document.createElement(title.nodeName);
837         M.core_dock.fixTitleOrientation(this, this.title, this.titlestring.firstChild.nodeValue);
838     }
839     if (contents && this.contents==null) {
840         this.contents = contents;
841     }
842     if (commands && this.commands==null) {
843         this.commands = commands;
844     }
845     if (blockclass && this.blockclass==null) {
846         this.blockclass = blockclass;
847     }
848     this.nodes = (function(){
849         return {docktitle : null, dockitem : null, container: null};
850     })();
851 };
852 /**
853  *
854  */
855 M.core_dock.item.prototype = {
856     Y : null,               // The YUI instance to use with this dock item
857     id : null,              // The unique id for the item
858     name : null,            // The name of the item
859     title : null,           // The title of the item
860     titlestring : null,     // The title as a plain string
861     contents : null,        // The content of the item
862     commands : null,        // The commands for the item
863     active : false,         // True if the item is being shown
864     blockclass : null,      // The class of the block this item relates to
865     nodes : null,
866     /**
867      * This function draws the item on the dock
868      */
869     draw : function() {
870         this.fire('dockeditem:drawstart');
872         var Y = this.Y;
873         var css = M.core_dock.css;
875         this.nodes.docktitle = Y.Node.create('<div id="dock_item_'+this.id+'_title" class="'+css.dockedtitle+'"></div>');
876         this.nodes.docktitle.append(this.title);
877         this.nodes.dockitem = Y.Node.create('<div id="dock_item_'+this.id+'" class="'+css.dockeditem+'"></div>');
878         if (M.core_dock.count === 1) {
879             this.nodes.dockitem.addClass('firstdockitem');
880         }
881         this.nodes.dockitem.append(this.nodes.docktitle);
882         M.core_dock.append(this.nodes.dockitem);
883         this.fire('dockeditem:drawcomplete');
884         return true;
885     },
886     /**
887      * This function toggles makes the item active and shows it
888      */
889     show : function() {
890         M.core_dock.hideActive();
891         var Y = this.Y;
892         var css = M.core_dock.css;
893         var panel = M.core_dock.getPanel();
894         this.fire('dockeditem:showstart');
895         panel.setHeader(this.titlestring, this.commands);
896         panel.setBody(Y.Node.create('<div class="'+this.blockclass+' block_docked"></div>').append(this.contents));
897         panel.show();
898         
899         this.active = true;
900         // Add active item class first up
901         this.nodes.docktitle.addClass(css.activeitem);
902         this.fire('dockeditem:showcomplete');
903         M.core_dock.resize();
904         return true;
905     },
906     /**
907      * This function hides the item and makes it inactive
908      */
909     hide : function() {
910         var css = M.core_dock.css;
911         this.fire('dockeditem:hidestart');
912         // No longer active
913         this.active = false;
914         // Remove the active class
915         this.nodes.docktitle.removeClass(css.activeitem);
916         // Hide the panel
917         M.core_dock.getPanel().hide();
918         this.fire('dockeditem:hidecomplete');
919     },
920     /**
921      * This function removes the node and destroys it's bits
922      * @param {Event} e
923      */
924     remove : function () {
925         this.hide();
926         this.nodes.dockitem.remove();
927         this.fire('dockeditem:itemremoved');
928     }
929 };