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