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