blocks-dock MDL-23891 Avoided use of broken YUI ancestor function in IE
[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     if (options.cssselector == 'body') {
288         if (!this.nodes.dock.contains(e.target)) {
289             if (item) {
290                 item.hide();
291             }
292         }
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         }
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();
327             }
328         } else if (item) {
329             item.hide();
330         }
331     }
332     return true;
333 };
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  */
342 M.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
351         };
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;
361 };
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  */
371 M.core_dock.fixTitleOrientation = function(item, title, text) {
372     var Y = this.Y;
374     var title = Y.one(title);
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     }
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     }
394     if (Y.UA.ie > 7) {
395         // IE8 can flip the text via CSS but not handle SVG
396         title.setContent(text);
397         title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;');
398         title.addClass('filterrotate');
399         return title;
400     }
402     // Cool, we can use SVG!
403     var test = Y.Node.create('<h2><span style="font-size:10px;">'+text+'</span></h2>');
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();
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));
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);
427     title.append(svg);
429     item.on('dockeditem:drawcomplete', function(txt, title){
430         txt.setAttribute('fill', Y.one(title).getStyle('color'));
431     }, item, txt, title);
433     return title;
434 };
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  */
440 M.core_dock.resizeBlockSpace = function(node) {
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     }
478 };
479 /**
480  * Adds a dock item into the dock
481  * @function
482  * @param {M.core_dock.item} item
483  */
484 M.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);
492 };
493 /**
494  * Appends a dock item to the dock
495  * @param {YUI.Node} docknode
496  */
497 M.core_dock.append = function(docknode) {
498     this.nodes.container.append(docknode);
499 };
500 /**
501  * Initialises a generic block object
502  * @param {YUI} Y
503  * @param {int} id
504  */
505 M.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));
510 };
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  */
517 M.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;
527 };
528 /**
529  * Ensures the the first item in the dock has the correct class
530  */
531 M.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     }
536 };
537 /**
538  * Removes all nodes and puts them back into conventional page sturcture
539  * @function
540  * @return {boolean}
541  */
542 M.core_dock.remove_all = function() {
543     for (var i in this.items) {
544         this.remove(i);
545     }
546     return true;
547 };
548 /**
549  * Hides the active item
550  */
551 M.core_dock.hideActive = function() {
552     var item = this.getActiveItem();
553     if (item) {
554         item.hide();
555     }
556 };
557 /**
558  * Checks wether the dock should be shown or hidden
559  */
560 M.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     }
571 };
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  */
581 M.core_dock.on = function(event, callback) {
582     this.earlybinds.push({event:event,callback:callback});
583 };
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  */
588 M.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 = [];
594 };
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  */
599 M.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);
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');
612     panel.contentBody.setStyle('height', 'auto');
613     panel.removeClass('oversized_content');
614     var panelheight = panel.get('offsetHeight');
616     if (panelheight > screenheight) {
617         panel.setStyle('top', (buffer-containerheight)+'px');
618         panel.contentBody.setStyle('height', (screenheight-panel.contentHeader.get('offsetHeight'))+'px');
619         panel.addClass('oversized_content');
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;
628 };
629 /**
630  * Returns the currently active dock item or false
631  */
632 M.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;
639 };
640 /**
641  * This class represents a generic block
642  * @class M.core_dock.genericblock
643  * @constructor
644  */
645 M.core_dock.genericblock = function(id) {
646     // Nothing to actually do here but it needs a constructor!
647     if (id) {
648         this.id = id;
649     }
650 };
651 M.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
657     isdocked : false,           // True if it is docked
658     /**
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
665      * @return {M.core_dock.genericblock}
666      */
667     init : function(Y, node) {
668         M.core_dock.init(Y);
669         
670         this.Y = Y;
671         if (!node) {
672             return false;
673         }
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);
680             }
681         }
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'));
686         moveto.on('movetodock|click', this.move_to_dock, this, commands);
688         var blockaction = node.one('.block_action');
689         if (blockaction) {
690             blockaction.prepend(moveto);
691         } else {
692             commands.append(moveto);
693         }
695         // Move the block straight to the dock if required
696         if (node.hasClass('dock_on_load')) {
697             node.removeClass('dock_on_load');
698             this.skipsetposition = true;
699             this.move_to_dock(null, commands);
700         }
701         return this;
702     },
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         }
714         var Y = this.Y;
715         var dock = M.core_dock;
717         var node = Y.one('#inst'+this.id);
718         var blockcontent = node.one('.content');
719         if (!blockcontent) {
720             return;
721         }
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());
729         this.cachedcontentnode = node;
731         node.replace(Y.Node.getDOMNode(Y.Node.create('<div id="content_placeholder_'+this.id+'" class="block_dock_placeholder"></div>')));
732         M.core_dock.holdingarea.append(node);
733         node = null;
735         var blocktitle = Y.Node.getDOMNode(this.cachedcontentnode.one('.title h2')).cloneNode(true);
737         var blockcommands = this.cachedcontentnode.one('.title .commands');
738         if (!blockcommands) {
739             blockcommands = Y.Node.create('<div class="commands"></div>');
740             this.cachedcontentnode.one('.title').append(blockcommands);
741         }
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);
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);
754         // Create a new dock item for the block
755         var dockitem = new dock.item(Y, this.id, blocktitle, blockcontent, blockcommands, blockclass);
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();
761                 dock.remove(this.id);
762             }, this);
763             // check the commands block [editing=on]
764             this.commands.all('.moveto').on('returntoblock|click', function(e){
765                 e.halt();
766                 dock.remove(this.id);
767             }, this);
768             // Add a close icon
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'));
772             closeicon.on('forceclose|click', this.hide, this);
773             this.commands.append(closeicon);
774         }, dockitem);
775         // Register an event so that when it is removed we can put it back as a block
776         dockitem.on('dockeditem:itemremoved', this.return_to_block, this, dockitem);
777         dock.add(dockitem);
778         
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         }
786         this.isdocked = true;
787     },
788     /**
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}
792      */
793     return_to_block : function(dockitem) {
794         var placeholder = this.Y.one('#content_placeholder_'+this.id);
796         if (this.cachedcontentnode.one('.header')) {
797             this.cachedcontentnode.one('.header').insert(dockitem.contents, 'after');
798         } else {
799             this.cachedcontentnode.insert(dockitem.contents);
800         }
802         placeholder.replace(this.Y.Node.getDOMNode(this.cachedcontentnode));
803         this.cachedcontentnode = this.Y.one('#'+this.cachedcontentnode.get('id'));
805         var commands = this.cachedcontentnode.one('.title .commands');
806         if (commands) {
807             commands.all('.hidepanelicon').remove();
808             commands.all('.moveto').remove();
809             commands.remove();
810         }
811         this.cachedcontentnode.one('.title').append(commands);
812         this.cachedcontentnode = null;
813         M.util.set_user_preference('docked_block_instance_'+this.id, 0);
814         this.isdocked = false;
815         return true;
816     }
817 };
819 /**
820  * This class represents an item in the dock
821  * @class M.core_dock.item
822  * @constructor
823  * @param {YUI} Y The YUI instance to use for this item
824  * @param {int} uid The unique ID for the item
825  * @param {this.Y.Node} title
826  * @param {this.Y.Node} contents
827  * @param {this.Y.Node} commands
828  * @param {string} blockclass
829  */
830 M.core_dock.item = function(Y, uid, title, contents, commands, blockclass){
831     this.Y = Y;
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'});
839     if (uid && this.id==null) {
840         this.id = uid;
841     }
842     if (title && this.title==null) {
843         this.titlestring = title.cloneNode(true);
844         this.title = document.createElement(title.nodeName);
845         M.core_dock.fixTitleOrientation(this, this.title, this.titlestring.firstChild.nodeValue);
846     }
847     if (contents && this.contents==null) {
848         this.contents = contents;
849     }
850     if (commands && this.commands==null) {
851         this.commands = commands;
852     }
853     if (blockclass && this.blockclass==null) {
854         this.blockclass = blockclass;
855     }
856     this.nodes = (function(){
857         return {docktitle : null, dockitem : null, container: null};
858     })();
859 };
860 /**
861  *
862  */
863 M.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
868     titlestring : null,     // The title as a plain string
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
872     blockclass : null,      // The class of the block this item relates to
873     nodes : null,
874     /**
875      * This function draws the item on the dock
876      */
877     draw : function() {
878         this.fire('dockeditem:drawstart');
880         var Y = this.Y;
881         var css = M.core_dock.css;
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');
888         }
889         this.nodes.dockitem.append(this.nodes.docktitle);
890         M.core_dock.append(this.nodes.dockitem);
891         this.fire('dockeditem:drawcomplete');
892         return true;
893     },
894     /**
895      * This function toggles makes the item active and shows it
896      */
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();
902         this.fire('dockeditem:showstart');
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         
907         this.active = true;
908         // Add active item class first up
909         this.nodes.docktitle.addClass(css.activeitem);
910         this.fire('dockeditem:showcomplete');
911         M.core_dock.resize();
912         return true;
913     },
914     /**
915      * This function hides the item and makes it inactive
916      */
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');
927     },
928     /**
929      * This function removes the node and destroys it's bits
930      * @param {Event} e
931      */
932     remove : function () {
933         this.hide();
934         this.nodes.dockitem.remove();
935         this.fire('dockeditem:itemremoved');
936     }
937 };