From 84192d78363a26d99331d51eb7c428187844d469 Mon Sep 17 00:00:00 2001 From: Sam Hemelryk Date: Thu, 20 Jun 2013 18:12:28 +1200 Subject: [PATCH] MDL-40167 dock: converted the dock JS to a YUI shifted module. The patch converts the dock into a YUI shifter module and at the same time improves several aspects of its operation. The features of this patch include: * Dock Module conversion. * A loader that ensures we don't include the dock JS or its requirements unless actually required. * We no longer include the dock JS for themes that don't enable it. * Blocks no longer add registration events to the page instead a dockable attribute is added to the html and the loader looks for that. * The dock module is properly documented and running YUIDoc gives good quality documentation. * We no longer need the dock module registration or subcomponent. * All events that can be delegated are now delegated. * Removed unused variables and code left over after fixes. * Support for docking blocks renderered using the new blocks render method. Better support for custom block regions. --- blocks/dock.js | 1141 --------- blocks/moodleblock.class.php | 10 +- blocks/navigation/block_navigation.php | 7 +- ...oodle-block_navigation-navigation-debug.js | 360 ++- .../moodle-block_navigation-navigation-min.js | 4 +- .../moodle-block_navigation-navigation.js | 355 ++- .../yui/src/navigation/js/navigation.js | 347 ++- .../yui/src/navigation/meta/navigation.json | 4 +- blocks/settings/block_settings.php | 11 +- lib/classes/component.php | 1 - lib/outputcomponents.php | 6 + lib/outputrenderers.php | 6 + lib/outputrequirementslib.php | 10 - lib/pagelib.php | 7 +- lib/tests/component_test.php | 2 +- .../moodle-core-dock-debug.js | 2116 +++++++++++++++++ .../moodle-core-dock/moodle-core-dock-min.js | 4 + .../moodle-core-dock/moodle-core-dock.js | 2098 ++++++++++++++++ .../moodle-core-dockloader-debug.js | 126 + .../moodle-core-dockloader-min.js | 1 + .../moodle-core-dockloader.js | 122 + lib/yui/src/dock/build.json | 20 + lib/yui/src/dock/js/actionkey.js | 118 + lib/yui/src/dock/js/block.js | 248 ++ lib/yui/src/dock/js/dock.js | 1077 +++++++++ lib/yui/src/dock/js/dockeditem.js | 315 +++ lib/yui/src/dock/js/dockloader.js | 122 + lib/yui/src/dock/js/panel.js | 233 ++ lib/yui/src/dock/js/tabheightmanager.js | 111 + lib/yui/src/dock/meta/dock.json | 18 + 30 files changed, 7579 insertions(+), 1421 deletions(-) delete mode 100644 blocks/dock.js create mode 100644 lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js create mode 100644 lib/yui/build/moodle-core-dock/moodle-core-dock-min.js create mode 100644 lib/yui/build/moodle-core-dock/moodle-core-dock.js create mode 100644 lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-debug.js create mode 100644 lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-min.js create mode 100644 lib/yui/build/moodle-core-dockloader/moodle-core-dockloader.js create mode 100644 lib/yui/src/dock/build.json create mode 100644 lib/yui/src/dock/js/actionkey.js create mode 100644 lib/yui/src/dock/js/block.js create mode 100644 lib/yui/src/dock/js/dock.js create mode 100644 lib/yui/src/dock/js/dockeditem.js create mode 100644 lib/yui/src/dock/js/dockloader.js create mode 100644 lib/yui/src/dock/js/panel.js create mode 100644 lib/yui/src/dock/js/tabheightmanager.js create mode 100644 lib/yui/src/dock/meta/dock.json diff --git a/blocks/dock.js b/blocks/dock.js deleted file mode 100644 index 95d0a3e80aa..00000000000 --- a/blocks/dock.js +++ /dev/null @@ -1,1141 +0,0 @@ -/** - * The dock namespace: Contains all things dock related - * @namespace - */ -M.core_dock = { - count : 0, // The number of dock items currently - totalcount : 0, // The number of dock items through the page life - items : [], // An array of dock items - earlybinds : [], // Events added before the dock was augmented to support events - Y : null, // The YUI instance to use with dock related code - initialised : false, // True once thedock has been initialised - delayedevent : null, // Will be an object if there is a delayed event in effect - preventevent : null, // Will be an eventtype if there is an eventyoe to prevent - holdingarea : null -}; -/** - * Namespace containing the nodes that relate to the dock - * @namespace - */ -M.core_dock.nodes = { - dock : null, // The dock itself - body : null, // The body of the page - panel : null // The docks panel -}; -/** - * Configuration parameters used during the initialisation and setup - * of dock and dock items. - * This is here specifically so that themers can override core parameters and - * design aspects without having to re-write navigation - * @namespace - */ -M.core_dock.cfg = { - buffer:10, // Buffer used when containing a panel - position:'left', // position of the dock - orientation:'vertical', // vertical || horizontal determines if we change the title - spacebeforefirstitem: 10, // Space between the top of the dock and the first item - removeallicon: M.util.image_url('t/dock_to_block', 'moodle') -}; -/** - * CSS classes to use with the dock - * @namespace - */ -M.core_dock.css = { - dock:'dock', // CSS Class applied to the dock box - dockspacer:'dockspacer', // CSS class applied to the dockspacer - controls:'controls', // CSS class applied to the controls box - body:'has_dock', // CSS class added to the body when there is a dock - buttonscontainer: 'buttons_container', - dockeditem:'dockeditem', // CSS class added to each item in the dock - dockeditemcontainer:'dockeditem_container', - dockedtitle:'dockedtitle', // CSS class added to the item's title in each dock - activeitem:'activeitem' // CSS class added to the active item -}; -/** - * Augments the classes as required and processes early bindings - */ -M.core_dock.init = function(Y) { - if (this.initialised) { - return true; - } - var css = this.css; - this.initialised = true; - this.Y = Y; - this.nodes.body = Y.one(document.body); - - // Give the dock item class the event properties/methods - Y.augment(this.item, Y.EventTarget); - Y.augment(this, Y.EventTarget, true); - /** - * A 'dock:actionkey' Event. - * The event consists of the left arrow, right arrow, enter and space keys. - * More keys can be mapped to action meanings. - * actions: collapse , expand, toggle, enter. - * - * This event is subscribed to by dockitems. - * The on() method to subscribe allows specifying the desired trigger actions as JSON. - * - * This event can also be delegated if needed. - * Todo: This could be centralised, a similar Event is defined in blocks/navigation/yui/navigation/navigation.js - */ - Y.Event.define("dock:actionkey", { - // Webkit and IE repeat keydown when you hold down arrow keys. - // Opera links keypress to page scroll; others keydown. - // Firefox prevents page scroll via preventDefault() on either - // keydown or keypress. - _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress', - - _keys: { - //arrows - '37': 'collapse', - '39': 'expand', - //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings) - '32': 'toggle', - '13': 'enter' - }, - - _keyHandler: function (e, notifier, args) { - if (!args.actions) { - var actObj = {collapse:true, expand:true, toggle:true, enter:true}; - } else { - var actObj = args.actions; - } - if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) { - e.action = this._keys[e.keyCode]; - notifier.fire(e); - } - }, - - on: function (node, sub, notifier) { - // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). - if (sub.args == null) { - //no actions given - sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false}); - } else { - sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]); - } - }, - - detach: function (node, sub, notifier) { - //detach our _detacher handle of the subscription made in on() - sub._detacher.detach(); - }, - - delegate: function (node, sub, notifier, filter) { - // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). - if (sub.args == null) { - //no actions given - sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false}); - } else { - sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]); - } - }, - - detachDelegate: function (node, sub, notifier) { - sub._delegateDetacher.detach(); - } - }); - // Publish the events the dock has - this.publish('dock:beforedraw', {prefix:'dock'}); - this.publish('dock:beforeshow', {prefix:'dock'}); - this.publish('dock:shown', {prefix:'dock'}); - this.publish('dock:hidden', {prefix:'dock'}); - this.publish('dock:initialised', {prefix:'dock'}); - this.publish('dock:itemadded', {prefix:'dock'}); - this.publish('dock:itemremoved', {prefix:'dock'}); - this.publish('dock:itemschanged', {prefix:'dock'}); - this.publish('dock:panelgenerated', {prefix:'dock'}); - this.publish('dock:panelresizestart', {prefix:'dock'}); - this.publish('dock:resizepanelcomplete', {prefix:'dock'}); - this.publish('dock:starting', {prefix: 'dock',broadcast: 2,emitFacade: true}); - this.fire('dock:starting'); - // Re-apply early bindings properly now that we can - this.applyBinds(); - // Check if there is a customisation function - if (typeof(customise_dock_for_theme) === 'function') { - try { - // Run the customisation function - customise_dock_for_theme(); - } catch (exception) { - // Do nothing at the moment - } - } - - var dock = Y.one('#dock'); - if (!dock) { - // Start the construction of the dock - dock = Y.Node.create('') - .append(Y.Node.create('
') - .append(Y.Node.create('
'))); - this.nodes.body.append(dock); - } else { - dock.addClass(css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation); - } - this.holdingarea = Y.Node.create('
').setStyles({display:'none'}); - this.nodes.body.append(this.holdingarea); - if (Y.UA.ie > 0 && Y.UA.ie < 7) { - // Adjust for IE 6 (can't handle fixed pos) - dock.setStyle('height', dock.get('winHeight')+'px'); - } - // Store the dock - this.nodes.dock = dock; - this.nodes.buttons = dock.one('.'+css.buttonscontainer); - this.nodes.container = this.nodes.buttons.one('.'+css.dockeditemcontainer); - - if (Y.all('.block.dock_on_load').size() == 0) { - // Nothing on the dock... hide it using CSS - dock.addClass('nothingdocked'); - } else { - this.nodes.body.addClass(this.css.body).addClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation); - } - - this.fire('dock:beforedraw'); - - // Add a removeall button - // Must set the image src seperatly of we get an error with XML strict headers - var removeall = Y.Node.create(''+M.str.block.undockall+''); - removeall.setAttribute('src',this.cfg.removeallicon); - removeall.on('removeall|click', this.remove_all, this); - removeall.on('dock:actionkey', this.remove_all, this, {actions:{enter:true}}); - this.nodes.buttons.appendChild(Y.Node.create('
').append(removeall)); - - // Create a manager for the height of the tabs. Once set this can be forgotten about - new (function(Y){ - return { - enabled : false, // True if the item_sizer is being used, false otherwise - /** - * Initialises the dock sizer which then attaches itself to the required - * events in order to monitor the dock - * @param {YUI} Y - */ - init : function() { - M.core_dock.on('dock:itemschanged', this.checkSizing, this); - Y.on('windowresize', this.checkSizing, this); - }, - /** - * Check if the size dock items needs to be adjusted - */ - checkSizing : function() { - var dock = M.core_dock; - var possibleheight = dock.nodes.dock.get('offsetHeight') - dock.nodes.dock.one('.controls').get('offsetHeight') - (dock.cfg.buffer*3) - (dock.items.length*2); - var totalheight = 0; - for (var id in dock.items) { - var dockedtitle = Y.one(dock.items[id].title).ancestor('.'+dock.css.dockedtitle); - if (dockedtitle) { - if (this.enabled) { - dockedtitle.setStyle('height', 'auto'); - } - totalheight += dockedtitle.get('offsetHeight') || 0; - } - } - if (totalheight > possibleheight) { - this.enable(possibleheight); - } - }, - /** - * Enables the dock sizer and resizes where required. - */ - enable : function(possibleheight) { - var dock = M.core_dock; - var runningcount = 0; - var usedheight = 0; - this.enabled = true; - for (var id in dock.items) { - var itemtitle = Y.one(dock.items[id].title).ancestor('.'+dock.css.dockedtitle); - if (!itemtitle) { - continue; - } - var itemheight = Math.floor((possibleheight-usedheight) / (dock.count - runningcount)); - var offsetheight = itemtitle.get('offsetHeight'); - itemtitle.setStyle('overflow', 'hidden'); - if (offsetheight > itemheight) { - itemtitle.setStyle('height', itemheight+'px'); - usedheight += itemheight; - } else { - usedheight += offsetheight; - } - runningcount++; - } - } - }; - })(Y).init(); - - // Attach the required event listeners - // We use delegate here as that way a handful of events are created for the dock - // and all items rather than the same number for the dock AND every item individually - Y.delegate('click', this.handleEvent, this.nodes.dock, '.'+this.css.dockedtitle, this, {cssselector:'.'+this.css.dockedtitle, delay:0}); - 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}); - //Y.delegate('mouseleave', this.handleEvent, this.nodes.body, '#dock', this, {cssselector:'#dock', delay:0.5, iscontained:false}); - this.nodes.dock.on('mouseleave', this.handleEvent, this, {cssselector:'#dock', delay:0.5, iscontained:false}); - - this.nodes.body.on('click', this.handleEvent, this, {cssselector:'body', delay:0}); - this.on('dock:itemschanged', this.resizeBlockSpace, this); - this.on('dock:itemschanged', this.checkDockVisibility, this); - this.on('dock:itemschanged', this.resetFirstItem, this); - // Inform everyone the dock has been initialised - this.fire('dock:initialised'); - return true; -}; -/** - * Get the panel docked blocks will be shown in and initialise it if we havn't already. - */ -M.core_dock.getPanel = function() { - if (this.nodes.panel === null) { - // Initialise the dockpanel .. should only happen once - this.nodes.panel = (function(Y, parent){ - var dockpanel = Y.Node.create('
'); - // Give the dockpanel event target properties and methods - Y.augment(dockpanel, Y.EventTarget); - // Publish events for the dock panel - dockpanel.publish('dockpanel:beforeshow', {prefix:'dockpanel'}); - dockpanel.publish('dockpanel:shown', {prefix:'dockpanel'}); - dockpanel.publish('dockpanel:beforehide', {prefix:'dockpanel'}); - dockpanel.publish('dockpanel:hidden', {prefix:'dockpanel'}); - dockpanel.publish('dockpanel:visiblechange', {prefix:'dockpanel'}); - // Cache the content nodes - dockpanel.contentNode = dockpanel.one('.dockeditempanel_content'); - dockpanel.contentHeader = dockpanel.contentNode.one('.dockeditempanel_hd'); - dockpanel.contentBody = dockpanel.contentNode.one('.dockeditempanel_bd'); - // Set the x position of the panel - //dockpanel.setX(parent.get('offsetWidth')); - dockpanel.visible = false; - // Add a show event - dockpanel.show = function() { - this.fire('dockpanel:beforeshow'); - this.visible = true; - this.removeClass('dockitempanel_hidden'); - this.fire('dockpanel:shown'); - this.fire('dockpanel:visiblechange'); - }; - // Add a hide event - dockpanel.hide = function() { - this.fire('dockpanel:beforehide'); - this.visible = false; - this.addClass('dockitempanel_hidden'); - this.fire('dockpanel:hidden'); - this.fire('dockpanel:visiblechange'); - }; - // Add a method to set the header content - dockpanel.setHeader = function(content) { - this.contentHeader.setContent(content); - if (arguments.length > 1) { - for (var i=1;i < arguments.length;i++) { - this.contentHeader.append(arguments[i]); - } - } - }; - // Add a method to set the body content - dockpanel.setBody = function(content) { - this.contentBody.setContent(content); - }; - // Add a method to set the top of the panel position - dockpanel.setTop = function(newtop) { - if (Y.UA.ie > 0 && Y.UA.ie < 7) { - this.setY(newtop); - } else { - this.setStyle('top', newtop.toString()+'px'); - } - return; - }; - /** - * Increases the width of the panel to avoid horizontal scrolling - * if possible. - */ - dockpanel.correctWidth = function() { - var bd = this.one('.dockeditempanel_bd'); - - // Width of content - var w = bd.get('clientWidth'); - // Scrollable width of content - var s = bd.get('scrollWidth'); - // Width of content container with overflow - var ow = this.get('offsetWidth'); - // The new width - var nw = w; - // The max width (80% of screen) - var mw = Math.round(this.get('winWidth') * 0.8); - - // If the scrollable width is more than the visible width - if (s > w) { - // Content width - // + the difference - // + any rendering difference (borders, padding) - // + 10px to make it look nice. - nw = w + (s-w) + ((ow-w)*2) + 10; - } - - // Make sure its not more then the maxwidth - if (nw > mw) { - nw = mw; - } - - // Set the new width if its more than the old width. - if (nw > ow) { - this.setStyle('width', nw+'px'); - } - } - // Put the dockpanel in the body - parent.append(dockpanel); - // Return it - return dockpanel; - })(this.Y, this.nodes.dock); - this.nodes.panel.on('panel:visiblechange', this.resize, this); - this.Y.on('windowresize', this.resize, this); - this.fire('dock:panelgenerated'); - } - return this.nodes.panel; -}; -/** - * Handles a generic event within the dock - * @param {Y.Event} e - * @param {object} options Event configuration object - */ -M.core_dock.handleEvent = function(e, options) { - var item = this.getActiveItem(); - if (options.cssselector == 'body') { - if (!this.nodes.dock.contains(e.target)) { - if (item) { - item.hide(); - } - } - } else { - var target; - if (e.target.test(options.cssselector)) { - target = e.target; - } else { - target = e.target.ancestor(options.cssselector); - } - if (!target) { - return true; - } - if (this.preventevent !== null && e.type === this.preventevent) { - return true; - } - if (options.preventevent) { - this.preventevent = options.preventevent; - if (options.preventdelay) { - setTimeout(function(){M.core_dock.preventevent = null;}, options.preventdelay*1000); - } - } - if (this.delayedevent && this.delayedevent.timeout) { - clearTimeout(this.delayedevent.timeout); - this.delayedevent.event.detach(); - this.delayedevent = null; - } - if (options.delay > 0) { - return this.delayEvent(e, options, target); - } - var targetid = target.get('id'); - if (targetid.match(/^dock_item_(\d+)_title$/)) { - item = this.items[targetid.replace(/^dock_item_(\d+)_title$/, '$1')]; - if (item.active) { - item.hide(); - } else { - item.show(); - } - } else if (item) { - item.hide(); - } - } - return true; -}; -/** - * This function delays an event and then fires it providing the cursor if either - * within or outside of the original target (options.iscontained=true|false) - * @param {Y.Event} event - * @param {object} options - * @param {Y.Node} target - * @return bool - */ -M.core_dock.delayEvent = function(event, options, target) { - var self = this; - self.delayedevent = (function(){ - return { - target : target, - event : self.nodes.body.on('mousemove', function(e){ - self.delayedevent.target = e.target; - }), - timeout : null - }; - })(self); - self.delayedevent.timeout = setTimeout(function(){ - self.delayedevent.timeout = null; - self.delayedevent.event.detach(); - if (options.iscontained == self.nodes.dock.contains(self.delayedevent.target)) { - self.handleEvent(event, {cssselector:options.cssselector, delay:0, iscontained:options.iscontained}); - } - }, options.delay*1000); - return true; -}; -/** - * Corrects the orientation of the title, which for the default - * dock just means making it vertical - * The orientation is determined by M.str.langconfig.thisdirectionvertical: - * ver : Letters are stacked rather than rotated - * ttb : Title is rotated clockwise so the first letter is at the top - * btt : Title is rotated counterclockwise so the first letter is at the bottom. - * @param {string} title - */ -M.core_dock.fixTitleOrientation = function(item, title, text) { - var Y = this.Y; - - var title = Y.one(title); - - if(M.core_dock.cfg.orientation != 'vertical') { - // If the dock isn't vertical don't adjust it! - title.setContent(text); - return title - } - - if (Y.UA.ie > 0 && Y.UA.ie < 8) { - // IE 6/7 can't rotate text so force ver - M.str.langconfig.thisdirectionvertical = 'ver'; - } - - var clockwise = false; - switch (M.str.langconfig.thisdirectionvertical) { - case 'ver': - // Stacked is easy - return title.setContent(text.split('').join('
')); - case 'ttb': - clockwise = true; - break; - case 'btt': - clockwise = false; - break; - } - - if (Y.UA.ie == 8) { - // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute. - title.setContent(text); - title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;'); - title.addClass('filterrotate'); - return title; - } - - // We need to fix a font-size - sorry theme designers. - var fontsize = '11px'; - var transform = (clockwise) ? 'rotate(90deg)' : 'rotate(270deg)'; - var test = Y.Node.create('

'+text+'

'); - this.nodes.body.insert(test, 0); - var width = test.one('span').get('offsetWidth') * 1.2; - var height = test.one('span').get('offsetHeight'); - test.remove(); - - title.setContent(text); - title.addClass('css3transform'); - - // Move the title into position - title.setStyles({ - 'margin' : '0', - 'padding' : '0', - 'position' : 'relative', - 'fontSize' : fontsize, - 'width' : width, - 'top' : width/2 - }); - - // Positioning is different when in RTL mode. - if (right_to_left()) { - title.setStyle('left', width/2 - height); - } else { - title.setStyle('right', width/2 - height); - } - - // Rotate the text - title.setStyles({ - 'transform' : transform, - '-ms-transform' : transform, - '-moz-transform' : transform, - '-webkit-transform' : transform, - '-o-transform' : transform - }); - - var container = Y.Node.create('
'); - container.append(title); - container.setStyle('height', width + (width / 4)); - container.setStyle('position', 'relative'); - return container; - - return title; -}; -/** - * Resizes the space that contained blocks if there were no blocks left in - * it. e.g. if all blocks have been moved to the dock - * @param {Y.Node} node - */ -M.core_dock.resizeBlockSpace = function(node) { - - if (this.Y.all('.block.dock_on_load').size()>0) { - // Do not resize during initial load - return; - } - var blockregions = []; - var populatedblockregions = 0; - this.Y.all('.block-region').each(function(region){ - var hasblocks = (region.all('.block').size() > 0); - if (hasblocks) { - populatedblockregions++; - } - blockregions[region.get('id')] = {hasblocks: hasblocks, bodyclass: region.get('id').replace(/^region\-/, 'side-')+'-only'}; - }); - var bodynode = M.core_dock.nodes.body; - var showregions = false; - if (bodynode.hasClass('blocks-moving')) { - // open up blocks during blocks positioning - showregions = true; - } - - var noblocksbodyclass = 'content-only'; - var i = null; - if (populatedblockregions==0 && showregions==false) { - bodynode.addClass(noblocksbodyclass); - for (i in blockregions) { - bodynode.removeClass(blockregions[i].bodyclass); - } - } else if (populatedblockregions==1 && showregions==false) { - bodynode.removeClass(noblocksbodyclass); - for (i in blockregions) { - if (!blockregions[i].hasblocks) { - bodynode.removeClass(blockregions[i].bodyclass); - } else { - bodynode.addClass(blockregions[i].bodyclass); - } - } - } else { - bodynode.removeClass(noblocksbodyclass); - for (i in blockregions) { - bodynode.removeClass(blockregions[i].bodyclass); - } - } -}; -/** - * Adds a dock item into the dock - * @function - * @param {M.core_dock.item} item - */ -M.core_dock.add = function(item) { - item.id = this.totalcount; - this.count++; - this.totalcount++; - this.items[item.id] = item; - this.items[item.id].draw(); - this.fire('dock:itemadded', item); - this.fire('dock:itemschanged', item); -}; -/** - * Appends a dock item to the dock - * @param {YUI.Node} docknode - */ -M.core_dock.append = function(docknode) { - this.nodes.container.append(docknode); -}; -/** - * Initialises a generic block object - * @param {YUI} Y - * @param {int} id - */ -M.core_dock.init_genericblock = function(Y, id) { - if (!this.initialised) { - this.init(Y); - } - new this.genericblock(id).initialise_block(Y, Y.one('#inst'+id)); -}; -/** - * Removes the node at the given index and puts it back into conventional page sturcture - * @function - * @param {int} uid Unique identifier for the block - * @return {boolean} - */ -M.core_dock.remove = function(uid) { - if (!this.items[uid]) { - return false; - } - this.items[uid].remove(); - delete this.items[uid]; - this.count--; - this.fire('dock:itemremoved', uid); - this.fire('dock:itemschanged', uid); - return true; -}; -/** - * Ensures the the first item in the dock has the correct class - */ -M.core_dock.resetFirstItem = function() { - this.nodes.dock.all('.'+this.css.dockeditem+'.firstdockitem').removeClass('firstdockeditem'); - if (this.nodes.dock.one('.'+this.css.dockeditem)) { - this.nodes.dock.one('.'+this.css.dockeditem).addClass('firstdockitem'); - } -}; -/** - * Removes all nodes and puts them back into conventional page sturcture - * @function - * @return {boolean} - */ -M.core_dock.remove_all = function(e) { - for (var i in this.items) { - this.remove(i); - } - return true; -}; -/** - * Hides the active item - */ -M.core_dock.hideActive = function() { - var item = this.getActiveItem(); - if (item) { - item.hide(); - } -}; -/** - * Checks wether the dock should be shown or hidden - */ -M.core_dock.checkDockVisibility = function() { - if (!this.count) { - this.nodes.dock.addClass('nothingdocked'); - this.nodes.body.removeClass(this.css.body) - .removeClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation); - this.fire('dock:hidden'); - } else { - this.fire('dock:beforeshow'); - this.nodes.dock.removeClass('nothingdocked'); - this.nodes.body.addClass(this.css.body) - .addClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation); - this.fire('dock:shown'); - } -}; -/** - * This smart little function allows developers to attach event listeners before - * the dock has been augmented to allows event listeners. - * Once the augmentation is complete this function will be replaced with the proper - * on method for handling event listeners. - * Finally applyBinds needs to be called in order to properly bind events. - * @param {string} event - * @param {function} callback - */ -M.core_dock.on = function(event, callback) { - this.earlybinds.push({event:event,callback:callback}); -}; -/** - * This function takes all early binds and attaches them as listeners properly - * This should only be called once augmentation is complete. - */ -M.core_dock.applyBinds = function() { - for (var i in this.earlybinds) { - var bind = this.earlybinds[i]; - this.on(bind.event, bind.callback); - } - this.earlybinds = []; -}; -/** - * This function checks the size and position of the panel and moves/resizes if - * required to keep it within the bounds of the window. - */ -M.core_dock.resize = function() { - this.fire('dock:panelresizestart'); - var panel = this.getPanel(); - var item = this.getActiveItem(); - if (!panel.visible || !item) { - return; - } - - if (this.cfg.orientation=='vertical') { - var buffer = this.cfg.buffer; - var screenheight = parseInt(this.nodes.body.get('winHeight'))-(buffer*2); - var docky = this.nodes.dock.getY(); - var titletop = item.nodes.docktitle.getY()-docky-buffer; - var containery = this.nodes.container.getY(); - var containerheight = containery-docky+this.nodes.buttons.get('offsetHeight'); - var scrolltop = panel.contentBody.get('scrollTop'); - panel.contentBody.setStyle('height', 'auto'); - panel.removeClass('oversized_content'); - var panelheight = panel.get('offsetHeight'); - - if (this.Y.UA.ie > 0 && this.Y.UA.ie < 7) { - panel.setTop(item.nodes.docktitle.getY()); - } else if (panelheight > screenheight) { - panel.setTop(buffer-containerheight); - panel.contentBody.setStyle('height', (screenheight-panel.contentHeader.get('offsetHeight'))+'px'); - panel.addClass('oversized_content'); - } else if (panelheight > (screenheight-(titletop-buffer))) { - var difference = panelheight - (screenheight-titletop); - panel.setTop(titletop-containerheight-difference+buffer); - } else { - panel.setTop(titletop-containerheight+buffer); - } - - if (scrolltop) { - panel.contentBody.set('scrollTop', scrolltop); - } - } - - if (this.cfg.position=='right') { - panel.setStyle('left', -panel.get('offsetWidth')+'px'); - - } else if (this.cfg.position=='top') { - var dockx = this.nodes.dock.getX(); - var titleleft = item.nodes.docktitle.getX()-dockx; - panel.setStyle('left', titleleft+'px'); - } - - this.fire('dock:resizepanelcomplete'); - return; -}; -/** - * Returns the currently active dock item or false - */ -M.core_dock.getActiveItem = function() { - for (var i in this.items) { - if (this.items[i].active) { - return this.items[i]; - } - } - return false; -}; -/** - * This class represents a generic block - * @class M.core_dock.genericblock - * @constructor - */ -M.core_dock.genericblock = function(id) { - // Nothing to actually do here but it needs a constructor! - if (id) { - this.id = id; - } -}; -M.core_dock.genericblock.prototype = { - Y : null, // A YUI instance to use with the block - id : null, // The block instance id - cachedcontentnode : null, // The cached content node for the actual block - blockspacewidth : null, // The width of the block's original container - skipsetposition : false, // If true the user preference isn't updated - isdocked : false, // True if it is docked - /** - * This function should be called within the block's constructor and is used to - * set up the initial controls for swtiching block position as well as an initial - * moves that may be required. - * - * @param {YUI} Y - * @param {YUI.Node} node The node that contains all of the block's content - * @return {M.core_dock.genericblock} - */ - initialise_block : function(Y, node) { - M.core_dock.init(Y); - - this.Y = Y; - if (!node) { - return false; - } - - var commands = node.one('.header .title .commands'); - if (!commands) { - commands = this.Y.Node.create('
'); - if (node.one('.header .title')) { - node.one('.header .title').append(commands); - } - } - - // Must set the image src seperatly of we get an error with XML strict headers - var moveto = Y.Node.create(''); - var header = node.one('.header .title h2'); - moveto.setAttribute('alt', Y.Escape.html(M.util.get_string('addtodock', 'block'))); - if (header) { - moveto.setAttribute('title', Y.Escape.html(M.util.get_string('dockblock', 'block', header.getHTML()))); - } else { - moveto.setAttribute('title', Y.Escape.html(M.util.get_string('addtodock', 'block'))); - } - - var icon = 't/block_to_dock'; - if (right_to_left()) { - icon = 't/block_to_dock_rtl'; - } - moveto.setAttribute('src', M.util.image_url(icon, 'moodle')); - moveto.on('movetodock|click', this.move_to_dock, this, commands); - - var blockaction = node.one('.block_action'); - if (blockaction) { - blockaction.prepend(moveto); - } else { - commands.append(moveto); - } - - // Move the block straight to the dock if required - if (node.hasClass('dock_on_load')) { - node.removeClass('dock_on_load'); - this.skipsetposition = true; - this.move_to_dock(null, commands); - } - return this; - }, - - /** - * This function is reponsible for moving a block from the page structure onto the - * dock - * @param {event} - */ - move_to_dock : function(e, commands) { - if (e) { - e.halt(true); - } - - var Y = this.Y; - var dock = M.core_dock; - - var node = Y.one('#inst'+this.id); - var blockcontent = node.one('.content'); - if (!blockcontent) { - return; - } - - // Disable the skip anchor when docking - var skipanchor = node.previous(); - if (skipanchor.hasClass('skip-block')) { - skipanchor.hide(); - } - - var blockclass = (function(classes){ - var r = /(^|\s)(block_[a-zA-Z0-9_]+)(\s|$)/; - var m = r.exec(classes); - return (m)?m[2]:m; - })(node.getAttribute('className').toString()); - - this.cachedcontentnode = node; - - node.replace(Y.Node.getDOMNode(Y.Node.create('
'))); - M.core_dock.holdingarea.append(node); - node = null; - - var blocktitle = Y.Node.getDOMNode(this.cachedcontentnode.one('.title h2')).cloneNode(true); - - var blockcommands = this.cachedcontentnode.one('.title .commands'); - if (!blockcommands) { - blockcommands = Y.Node.create('
'); - this.cachedcontentnode.one('.title').append(blockcommands); - } - - // Must set the image src seperatly of we get an error with XML strict headers - var movetoimg = Y.Node.create(''+Y.Escape.html(M.str.block.undockitem)+''); - var icon = 't/dock_to_block'; - if (right_to_left()) { - icon = 't/dock_to_block_rtl'; - } - movetoimg.setAttribute('src', M.util.image_url(icon, 'moodle')); - var moveto = Y.Node.create('').append(movetoimg); - if (location.href.match(/\?/)) { - moveto.set('href', location.href+'&dock='+this.id); - } else { - moveto.set('href', location.href+'?dock='+this.id); - } - blockcommands.append(moveto); - - // Create a new dock item for the block - var dockitem = new dock.item(Y, this.id, blocktitle, blockcontent, blockcommands, blockclass); - // Wire the draw events to register remove events - dockitem.on('dockeditem:drawcomplete', function(e){ - // check the contents block [editing=off] - this.contents.all('.moveto').on('returntoblock|click', function(e){ - e.halt(); - dock.remove(this.id); - }, this); - // check the commands block [editing=on] - this.commands.all('.moveto').on('returntoblock|click', function(e){ - e.halt(); - dock.remove(this.id); - }, this); - // Add a close icon - // Must set the image src seperatly of we get an error with XML strict headers - var closeicon = Y.Node.create(''+M.str.block.hidepanel+''); - closeicon.one('img').setAttribute('src', M.util.image_url('t/dockclose', 'moodle')); - closeicon.on('forceclose|click', this.hide, this); - closeicon.on('dock:actionkey',this.hide, this, {actions:{enter:true,toggle:true}}); - this.commands.append(closeicon); - }, dockitem); - // Register an event so that when it is removed we can put it back as a block - dockitem.on('dockeditem:itemremoved', this.return_to_block, this, dockitem); - dock.add(dockitem); - - if (!this.skipsetposition) { - // save the users preference - M.util.set_user_preference('docked_block_instance_'+this.id, 1); - } else { - this.skipsetposition = false; - } - - this.isdocked = true; - }, - /** - * This function removes a block from the dock and puts it back into the page - * structure. - * @param {M.core_dock.class.item} - */ - return_to_block : function(dockitem) { - var placeholder = this.Y.one('#content_placeholder_'+this.id); - - // Enable the skip anchor when going back to block mode - var skipanchor = placeholder.previous(); - if (skipanchor.hasClass('skip-block')) { - skipanchor.show(); - } - - if (this.cachedcontentnode.one('.header')) { - this.cachedcontentnode.one('.header').insert(dockitem.contents, 'after'); - } else { - this.cachedcontentnode.insert(dockitem.contents); - } - - placeholder.replace(this.Y.Node.getDOMNode(this.cachedcontentnode)); - this.cachedcontentnode = this.Y.one('#'+this.cachedcontentnode.get('id')); - - var commands = dockitem.commands; - if (commands) { - commands.all('.hidepanelicon').remove(); - commands.all('.moveto').remove(); - commands.remove(); - } - this.cachedcontentnode.one('.title').append(commands); - this.cachedcontentnode = null; - M.util.set_user_preference('docked_block_instance_'+this.id, 0); - this.isdocked = false; - return true; - } -}; - -/** - * This class represents an item in the dock - * @class M.core_dock.item - * @constructor - * @param {YUI} Y The YUI instance to use for this item - * @param {int} uid The unique ID for the item - * @param {this.Y.Node} title - * @param {this.Y.Node} contents - * @param {this.Y.Node} commands - * @param {string} blockclass - */ -M.core_dock.item = function(Y, uid, title, contents, commands, blockclass){ - this.Y = Y; - this.publish('dockeditem:drawstart', {prefix:'dockeditem'}); - this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'}); - this.publish('dockeditem:showstart', {prefix:'dockeditem'}); - this.publish('dockeditem:showcomplete', {prefix:'dockeditem'}); - this.publish('dockeditem:hidestart', {prefix:'dockeditem'}); - this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'}); - this.publish('dockeditem:itemremoved', {prefix:'dockeditem'}); - if (uid && this.id==null) { - this.id = uid; - } - if (title && this.title==null) { - this.titlestring = title.cloneNode(true); - this.title = document.createElement(title.nodeName); - this.title = M.core_dock.fixTitleOrientation(this, this.title, this.titlestring.firstChild.nodeValue); - } - if (contents && this.contents==null) { - this.contents = contents; - } - if (commands && this.commands==null) { - this.commands = commands; - } - if (blockclass && this.blockclass==null) { - this.blockclass = blockclass; - } - this.nodes = (function(){ - return {docktitle : null, dockitem : null, container: null}; - })(); -}; -/** - * - */ -M.core_dock.item.prototype = { - Y : null, // The YUI instance to use with this dock item - id : null, // The unique id for the item - name : null, // The name of the item - title : null, // The title of the item - titlestring : null, // The title as a plain string - contents : null, // The content of the item - commands : null, // The commands for the item - active : false, // True if the item is being shown - blockclass : null, // The class of the block this item relates to - nodes : null, - /** - * This function draws the item on the dock - */ - draw : function() { - this.fire('dockeditem:drawstart'); - - var Y = this.Y; - var css = M.core_dock.css; - - this.nodes.docktitle = Y.Node.create(''); - this.nodes.docktitle.append(this.title); - this.nodes.dockitem = Y.Node.create('
'); - this.nodes.dockitem.on('dock:actionkey', this.toggle, this); - if (M.core_dock.count === 1) { - this.nodes.dockitem.addClass('firstdockitem'); - } - this.nodes.dockitem.append(this.nodes.docktitle); - M.core_dock.append(this.nodes.dockitem); - this.fire('dockeditem:drawcomplete'); - return true; - }, - /** - * This function toggles makes the item active and shows it - */ - show : function() { - M.core_dock.hideActive(); - var Y = this.Y; - var css = M.core_dock.css; - var panel = M.core_dock.getPanel(); - this.fire('dockeditem:showstart'); - panel.setHeader(this.titlestring, this.commands); - panel.setBody(Y.Node.create('
').append(this.contents)); - panel.show(); - panel.correctWidth(); - - this.active = true; - // Add active item class first up - this.nodes.docktitle.addClass(css.activeitem); - // Set aria-exapanded property to true. - this.nodes.docktitle.set('aria-expanded', "true"); - this.fire('dockeditem:showcomplete'); - M.core_dock.resize(); - return true; - }, - /** - * This function hides the item and makes it inactive - */ - hide : function() { - var css = M.core_dock.css; - this.fire('dockeditem:hidestart'); - // No longer active - this.active = false; - // Remove the active class - this.nodes.docktitle.removeClass(css.activeitem); - // Hide the panel - M.core_dock.getPanel().hide(); - // Set aria-exapanded property to false - this.nodes.docktitle.set('aria-expanded', "false"); - this.fire('dockeditem:hidecomplete'); - }, - /** - * A toggle between calling show and hide functions based on css.activeitem - * Applies rules to key press events (dock:actionkey) - * @param {Event} e - */ - toggle : function(e) { - var css = M.core_dock.css; - if (this.nodes.docktitle.hasClass(css.activeitem) && !(e.type == 'dock:actionkey' && e.action=='expand')) { - this.hide(); - } else if (!this.nodes.docktitle.hasClass(css.activeitem) && !(e.type == 'dock:actionkey' && e.action=='collapse')) { - this.show(); - } - }, - /** - * This function removes the node and destroys it's bits - * @param {Event} e - */ - remove : function () { - this.hide(); - this.nodes.dockitem.remove(); - this.fire('dockeditem:itemremoved'); - } -}; diff --git a/blocks/moodleblock.class.php b/blocks/moodleblock.class.php index 1aaaa1d3fc9..f3b9e062c87 100644 --- a/blocks/moodleblock.class.php +++ b/blocks/moodleblock.class.php @@ -271,6 +271,10 @@ class block_base { $bc->collapsible = block_contents::VISIBLE; } + if ($this->instance_can_be_docked() && !$this->hide_header()) { + $bc->dockable = true; + } + $bc->annotation = ''; // TODO MDL-19398 need to work out what to say here. return $bc; @@ -435,9 +439,13 @@ class block_base { $this->specialization(); } + /** + * Allows the block to load any JS it requires into the page. + * + * By default this function simply permits the user to dock the block if it is dockable. + */ function get_required_javascript() { if ($this->instance_can_be_docked() && !$this->hide_header()) { - $this->page->requires->js_init_call('M.core_dock.init_genericblock', array($this->instance->id)); user_preference_allow_ajax_update('docked_block_instance_'.$this->instance->id, PARAM_INT); } } diff --git a/blocks/navigation/block_navigation.php b/blocks/navigation/block_navigation.php index d987e4c1ba5..2252e85a45b 100644 --- a/blocks/navigation/block_navigation.php +++ b/blocks/navigation/block_navigation.php @@ -56,7 +56,6 @@ class block_navigation extends block_base { * Set the initial properties for the block */ function init() { - global $CFG; $this->blockname = get_class($this); $this->title = get_string('pluginname', $this->blockname); } @@ -109,8 +108,7 @@ class block_navigation extends block_base { */ function get_required_javascript() { global $CFG; - user_preference_allow_ajax_update('docked_block_instance_'.$this->instance->id, PARAM_INT); - $this->page->requires->js_module('core_dock'); + parent::get_required_javascript(); $limit = 20; if (!empty($CFG->navcourselimit)) { $limit = $CFG->navcourselimit; @@ -127,7 +125,7 @@ class block_navigation extends block_base { 'expansionlimit' => $expansionlimit ); $this->page->requires->string_for_js('viewallcourses', 'moodle'); - $this->page->requires->yui_module(array('core_dock', 'moodle-block_navigation-navigation'), 'M.block_navigation.init_add_tree', array($arguments)); + $this->page->requires->yui_module('moodle-block_navigation-navigation', 'M.block_navigation.init_add_tree', array($arguments)); } /** @@ -136,7 +134,6 @@ class block_navigation extends block_base { * @return object $this->content */ function get_content() { - global $CFG, $OUTPUT; // First check if we have already generated, don't waste cycles if ($this->contentgenerated === true) { return $this->content; diff --git a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js index e6bc95409da..5c9aa7d76d1 100644 --- a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js +++ b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js @@ -1,5 +1,51 @@ YUI.add('moodle-block_navigation-navigation', function (Y, NAME) { +/** + * Navigation block JS. + * + * This file contains the Navigation block JS.. + * + * @module moodle-block_navigation-navigation + */ + +/** + * This namespace will contain all of the contents of the navigation blocks + * global navigation and settings. + * @namespace M + * @class block_navigation + * @static + */ +M.block_navigation = M.block_navigation || {}; +/** + * The number of expandable branches in existence. + * + * @property expandablebranchcount + * @protected + * @static + */ +M.block_navigation.expandablebranchcount = 1; +/** + * The maximum number of courses to show as part of a branch. + * + * @property courselimit + * @protected + * @static + */ +M.block_navigation.courselimit = 20; +/** + * Add new instance of navigation tree to tree collection + * + * @method init_add_tree + * @static + * @param {Object} properties + */ +M.block_navigation.init_add_tree = function(properties) { + if (properties.courselimit) { + this.courselimit = properties.courselimit; + } + new TREE(properties); +}; + /** * A 'actionkey' Event to help with Y.delegate(). * The event consists of the left arrow, right arrow, enter and space keys. @@ -9,24 +55,35 @@ YUI.add('moodle-block_navigation-navigation', function (Y, NAME) { * This event is delegated to branches in the navigation tree. * The on() method to subscribe allows specifying the desired trigger actions as JSON. * - * Todo: This could be centralised, a similar Event is defined in blocks/dock.js + * @namespace M.block_navigation + * @class ActionKey */ Y.Event.define("actionkey", { - // Webkit and IE repeat keydown when you hold down arrow keys. + // Webkit and IE repeat keydown when you hold down arrow keys. // Opera links keypress to page scroll; others keydown. // Firefox prevents page scroll via preventDefault() on either // keydown or keypress. _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress', + /** + * The keys to trigger on. + * @method _keys + */ _keys: { //arrows '37': 'collapse', '39': 'expand', - //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings) '32': 'toggle', '13': 'enter' }, + /** + * Handles key events + * @method _keyHandler + * @param {EventFacade} e + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {Object} args + */ _keyHandler: function (e, notifier, args) { var actObj; if (!args.actions) { @@ -40,6 +97,13 @@ Y.Event.define("actionkey", { } }, + /** + * Subscribes to events. + * @method on + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + */ on: function (node, sub, notifier) { // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). if (sub.args === null) { @@ -50,11 +114,23 @@ Y.Event.define("actionkey", { } }, + /** + * Detaches an event listener + * @method detach + */ detach: function (node, sub) { //detach our _detacher handle of the subscription made in on() sub._detacher.detach(); }, + /** + * Creates a delegated event listener. + * @method delegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ delegate: function (node, sub, notifier, filter) { // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). if (sub.args === null) { @@ -65,45 +141,50 @@ Y.Event.define("actionkey", { } }, + /** + * Detaches a delegated event listener. + * @method detachDelegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ detachDelegate: function (node, sub) { sub._delegateDetacher.detach(); } }); var EXPANSIONLIMIT_EVERYTHING = 0, - //EXPANSIONLIMIT_COURSE = 20, - //EXPANSIONLIMIT_SECTION = 30, + EXPANSIONLIMIT_COURSE = 20, + EXPANSIONLIMIT_SECTION = 30, EXPANSIONLIMIT_ACTIVITY = 40; -/** - * Mappings for the different types of nodes coming from the navigation. - * Copied from lib/navigationlib.php navigation_node constants. - * @type object - */ +// Mappings for the different types of nodes coming from the navigation. +// Copied from lib/navigationlib.php navigation_node constants. var NODETYPE = { - /** @type int Root node = 0 */ + // @type int Root node = 0 ROOTNODE : 0, - /** @type int System context = 1 */ + // @type int System context = 1 SYSTEM : 1, - /** @type int Course category = 10 */ + // @type int Course category = 10 CATEGORY : 10, - /** @type int MYCATEGORY = 11 */ + // @type int MYCATEGORY = 11 MYCATEGORY : 11, - /** @type int Course = 20 */ + // @type int Course = 20 COURSE : 20, - /** @type int Course section = 30 */ + // @type int Course section = 30 SECTION : 30, - /** @type int Activity (course module) = 40 */ + // @type int Activity (course module) = 40 ACTIVITY : 40, - /** @type int Resource (course module = 50 */ + // @type int Resource (course module = 50 RESOURCE : 50, - /** @type int Custom node (could be anything) = 60 */ + // @type int Custom node (could be anything) = 60 CUSTOM : 60, - /** @type int Setting = 70 */ + // @type int Setting = 70 SETTING : 70, - /** @type int User context = 80 */ + // @type int User context = 80 USER : 80, - /** @type int Container = 90 */ + // @type int Container = 90 CONTAINER : 90 }; @@ -112,6 +193,11 @@ var NODETYPE = { * * This class establishes the tree initially, creating expandable branches as * required, and delegating the expand/collapse event. + * + * @namespace M.block_navigation + * @class Tree + * @constructor + * @extends Y.Base */ var TREE = function() { TREE.superclass.constructor.apply(this, arguments); @@ -119,17 +205,27 @@ var TREE = function() { TREE.prototype = { /** * The tree's ID, normally its block instance id. + * @property id + * @type Int + * @protected */ id : null, /** * An array of initialised branches. + * @property branches + * @type Array + * @protected */ branches : [], /** * Initialise the tree object when its first created. + * @method initializer + * @param {Object} config */ initializer : function(config) { - this.id = config.id; + Y.log('Initialising navigation block tree', 'note', 'moodle-block_navigation'); + + this.id = parseInt(config.id, 10); var node = Y.one('#inst'+config.id); @@ -168,14 +264,11 @@ TREE.prototype = { Y.delegate('click', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this); Y.delegate('actionkey', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this); } - - // Call the generic blocks init method to add all the generic stuff - if (this.get('candock')) { - this.initialise_block(Y, node); - } }, /** * Fire actions for a branch when an event occurs. + * @method fire_branch_action + * @param {EventFacade} event */ fire_branch_action : function(event) { var id = event.currentTarget.getAttribute('id'); @@ -185,6 +278,9 @@ TREE.prototype = { /** * This is a callback function responsible for expanding and collapsing the * branches of the tree. It is delegated to rather than multiple event handles. + * @method toggleExpansion + * @param {EventFacade} e + * @return Boolean */ toggleExpansion : function(e) { // First check if they managed to click on the li iteslf, then find the closest @@ -240,46 +336,66 @@ TREE.prototype = { // If this block can dock tell the dock to resize if required and check // the width on the dock panel in case it is presently in use. - if (this.get('candock')) { - M.core_dock.resize(); - var panel = M.core_dock.getPanel(); - if (panel.visible) { - panel.correctWidth(); - } + if (this.get('candock') && M.core.dock.notifyBlockChange) { + M.core.dock.notifyBlockChange(this.id); } + return true; + } }; // The tree extends the YUI base foundation. Y.extend(TREE, Y.Base, TREE.prototype, { NAME : 'navigation-tree', ATTRS : { - instance : { - value : null - }, + /** + * True if the block can dock. + * @attribute candock + * @type Boolean + */ candock : { validator : Y.Lang.isBool, value : false }, + /** + * If set to true nodes will be opened/closed in an accordian fashion. + * @attribute accordian + * @type Boolean + */ accordian : { validator : Y.Lang.isBool, value : false }, + /** + * The nodes that get shown. + * @attribute expansionlimit + * @type Integer + */ expansionlimit : { value : 0, setter : function(val) { - return parseInt(val, 10); + val = parseInt(val, 10); + if (val !== EXPANSIONLIMIT_EVERYTHING && + val !== EXPANSIONLIMIT_COURSE && + val !== EXPANSIONLIMIT_SECTION && + val !== EXPANSIONLIMIT_ACTIVITY) { + val = EXPANSIONLIMIT_EVERYTHING; + } + return val; } } } }); -if (M.core_dock && M.core_dock.genericblock) { - Y.augment(TREE, M.core_dock.genericblock); -} /** - * The tree branch class. + * The Branch class. + * * This class is used to manage a tree branch, in particular its ability to load * its contents by AJAX. + * + * @namespace M.block_navigation + * @class Branch + * @constructor + * @extends Y.Base */ BRANCH = function() { BRANCH.superclass.constructor.apply(this, arguments); @@ -287,10 +403,15 @@ BRANCH = function() { BRANCH.prototype = { /** * The node for this branch (p) + * @property node + * @type Node + * @protected */ node : null, /** * Initialises the branch when it is first created. + * @method initializer + * @param {Object} config */ initializer : function(config) { var i, @@ -310,8 +431,7 @@ BRANCH.prototype = { } } // Get the node for this branch - this.node = Y.one('#', this.get('id')); - // Now check whether the branch is not expandable because of the expansionlimit + this.node = Y.one('#'+this.get('id')); var expansionlimit = this.get('tree').get('expansionlimit'); var type = this.get('type'); if (expansionlimit !== EXPANSIONLIMIT_EVERYTHING && type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) { @@ -324,6 +444,11 @@ BRANCH.prototype = { * * This function creates a DOM structure for the branch and then injects * it into the navigation tree at the correct point. + * + * @method draw + * @chainable + * @param {Node} element + * @return Branch */ draw : function(element) { @@ -345,7 +470,7 @@ BRANCH.prototype = { // Prepare the icon, should be an object representing a pix_icon var branchicon = false; var icon = this.get('icon'); - if (icon && (!isbranch || this.get('type') == NODETYPE.ACTIVITY)) { + if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY)) { branchicon = Y.Node.create(''); branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component)); branchli.addClass('item_with_icon'); @@ -409,6 +534,8 @@ BRANCH.prototype = { }, /** * Gets the UL element that children for this branch should be inserted into. + * @method getChildrenUL + * @return Node */ getChildrenUL : function() { var ul = this.node.next('ul'); @@ -423,6 +550,10 @@ BRANCH.prototype = { * * This function calls ajaxProcessResponse with the result of the AJAX * request made here. + * + * @method ajaxLoad + * @param {EventFacade} e + * @return Bool */ ajaxLoad : function(e) { if (e.type === 'actionkey' && e.action !== 'enter') { @@ -446,6 +577,7 @@ BRANCH.prototype = { // We've already loaded this stuff. return true; } + Y.log('Loading navigation branch via AJAX: '+this.get('key'), 'note', 'moodle-block_navigation'); this.node.addClass('loadingbranch'); var params = { @@ -469,6 +601,11 @@ BRANCH.prototype = { /** * Processes an AJAX request to load the content of this branch through * AJAX. + * + * @method ajaxProcessResponse + * @param {Int} tid The transaction id. + * @param {Object} outcome + * @return Boolean */ ajaxProcessResponse : function(tid, outcome) { this.node.removeClass('loadingbranch'); @@ -479,20 +616,28 @@ BRANCH.prototype = { var coursecount = 0; for (var i in object.children) { if (typeof(object.children[i])==='object') { - if (object.children[i].type == NODETYPE.COURSE) { + if (object.children[i].type === NODETYPE.COURSE) { coursecount++; } this.addChild(object.children[i]); } } - if ((this.get('type') == NODETYPE.CATEGORY || this.get('type') == NODETYPE.ROOTNODE || this.get('type') == NODETYPE.MYCATEGORY) + if ((this.get('type') === NODETYPE.CATEGORY || this.get('type') === NODETYPE.ROOTNODE || this.get('type') === NODETYPE.MYCATEGORY) && coursecount >= M.block_navigation.courselimit) { this.addViewAllCoursesChild(this); } + Y.log('AJAX loading complete.', 'note', 'moodle-block_navigation'); + // If this block can dock tell the dock to resize if required and check + // the width on the dock panel in case it is presently in use. + if (this.get('tree').get('candock') && M.core.dock.notifyBlockChange) { + M.core.dock.notifyBlockChange(this.get('tree').id); + } return true; } + Y.log('AJAX loading complete but there were no children.', 'note', 'moodle-block_navigation'); } catch (ex) { - // If we got here then there was an error parsing the result + // If we got here then there was an error parsing the result. + Y.log('Error parsing AJAX response or adding branches to the navigation tree', 'error', 'moodle-block_navigation'); } // The branch is empty so class it accordingly this.node.replaceClass('branch', 'emptybranch'); @@ -501,6 +646,10 @@ BRANCH.prototype = { /** * Turns the branch object passed to the method into a proper branch object * and then adds it as a child of this branch. + * + * @method addChild + * @param {Object} branchobj + * @return Boolean */ addChild : function(branchobj) { // Make the new branch into an object @@ -511,14 +660,14 @@ BRANCH.prototype = { var count = 0, i, children = branch.get('children'); for (i in children) { // Add each branch to the tree - if (children[i].type == NODETYPE.COURSE) { + if (children[i].type === NODETYPE.COURSE) { count++; } if (typeof(children[i]) === 'object') { branch.addChild(children[i]); } } - if ((branch.get('type') == NODETYPE.CATEGORY || branch.get('type') == NODETYPE.MYCATEGORY) + if ((branch.get('type') === NODETYPE.CATEGORY || branch.get('type') === NODETYPE.MYCATEGORY) && count >= M.block_navigation.courselimit) { this.addViewAllCoursesChild(branch); } @@ -528,10 +677,13 @@ BRANCH.prototype = { /** * Add a link to view all courses in a category + * + * @method addViewAllCoursesChild + * @param {BRANCH} branch */ addViewAllCoursesChild: function(branch) { var url = null; - if (branch.get('type') == NODETYPE.ROOTNODE) { + if (branch.get('type') === NODETYPE.ROOTNODE) { if (branch.get('key') === 'mycourses') { url = M.cfg.wwwroot + '/my'; } else { @@ -552,9 +704,22 @@ BRANCH.prototype = { Y.extend(BRANCH, Y.Base, BRANCH.prototype, { NAME : 'navigation-branch', ATTRS : { + /** + * The Tree this branch belongs to. + * @attribute tree + * @type TREE + * @required + * @writeOnce + */ tree : { + writeOnce : 'initOnly', validator : Y.Lang.isObject }, + /** + * The name of this branch. + * @attribute name + * @type String + */ name : { value : '', validator : Y.Lang.isString, @@ -562,10 +727,21 @@ Y.extend(BRANCH, Y.Base, BRANCH.prototype, { return val.replace(/\n/g, '
'); } }, + /** + * The title to use for this branch. + * @attribute title + * @type String + */ title : { value : '', validator : Y.Lang.isString }, + /** + * The ID of this branch. + * The ID and Type should always form a unique pair. + * @attribute id + * @type String + */ id : { value : '', validator : Y.Lang.isString, @@ -577,31 +753,74 @@ Y.extend(BRANCH, Y.Base, BRANCH.prototype, { return val; } }, + /** + * The key used to identify this branch easily if there is one. + * @attribute key + * @type String + */ key : { value : null }, + /** + * The type of this branch. + * @attribute type + * @type Int + */ type : { - value : null + value : null, + setter : function(value) { + return parseInt(value, 10); + } }, + /** + * The link to use for this branch. + * @attribute link + * @type String + */ link : { value : false }, + /** + * The Icon to add when displaying this branch. + * @attribute icon + * @type Object + */ icon : { value : false, validator : Y.Lang.isObject }, + /** + * True if this branch is expandable. + * @attribute expandable + * @type Boolean + */ expandable : { value : false, validator : Y.Lang.isBool }, + /** + * True if this branch is hidden and should be displayed greyed out. + * @attribute hidden + * @type Boolean + */ hidden : { value : false, validator : Y.Lang.isBool }, + /** + * True if this branch has any children. + * @attribute haschildren + * @type Boolean + */ haschildren : { value : false, validator : Y.Lang.isBool }, + /** + * An array of other branches that appear as children of this branch. + * @attribute children + * @type Array + */ children : { value : [], validator : Y.Lang.isArray @@ -609,40 +828,5 @@ Y.extend(BRANCH, Y.Base, BRANCH.prototype, { } }); -/** - * This namespace will contain all of the contents of the navigation blocks - * global navigation and settings. - * @namespace - */ -M.block_navigation = M.block_navigation || { - /** The number of expandable branches in existence */ - expandablebranchcount:1, - courselimit : 20, - instance : null, - /** - * Add new instance of navigation tree to tree collection - */ - init_add_tree:function(properties) { - if (properties.courselimit) { - this.courselimit = properties.courselimit; - } - if (M.core_dock) { - M.core_dock.init(Y); - } - new TREE(properties); - } -}; - -}, '@VERSION@', { - "requires": [ - "base", - "core_dock", - "io-base", - "node", - "dom", - "event-custom", - "event-delegate", - "json-parse" - ] -}); +}, '@VERSION@', {"requires": ["base", "io-base", "node", "event-synthetic", "event-delegate", "json-parse"]}); diff --git a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js index b9cd9b24870..190c18039c6 100644 --- a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js +++ b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js @@ -1,2 +1,2 @@ -YUI.add("moodle-block_navigation-navigation",function(e,t){e.Event.define("actionkey",{_event:e.UA.webkit||e.UA.ie?"keydown":"keypress",_keys:{37:"collapse",39:"expand",32:"toggle",13:"enter"},_keyHandler:function(e,t,n){var r;n.actions?r=n.actions:r={collapse:!0,expand:!0,toggle:!0,enter:!0},this._keys[e.keyCode]&&r[this._keys[e.keyCode]]&&(e.action=this._keys[e.keyCode],t.fire(e))},on:function(e,t,n){t.args===null?t._detacher=e.on(this._event,this._keyHandler,this,n,{actions:!1}):t._detacher=e.on(this._event,this._keyHandler,this,n,t.args[0])},detach:function(e,t){t._detacher.detach()},delegate:function(e,t,n,r){t.args===null?t._delegateDetacher=e.delegate(this._event,this._keyHandler,r,this,n,{actions:!1}):t._delegateDetacher=e.delegate(this._event,this._keyHandler,r,this,n,t.args[0])},detachDelegate:function(e,t){t._delegateDetacher.detach()}});var n=0,r=40,i={ROOTNODE:0,SYSTEM:1,CATEGORY:10,MYCATEGORY:11,COURSE:20,SECTION:30,ACTIVITY:40,RESOURCE:50,CUSTOM:60,SETTING:70,USER:80,CONTAINER:90},s=function(){s.superclass.constructor.apply(this,arguments)};s.prototype={id:null,branches:[],initializer:function(t){this.id=t.id;var n=e.one("#inst"+t.id);if(n===null)return;e.delegate("click",this.toggleExpansion,n.one(".block_tree"),".tree_item.branch",this),e.delegate("actionkey",this.toggleExpansion,n.one(".block_tree"),".tree_item.branch",this);var r=[];t.expansions?r=t.expansions:window["navtreeexpansions"+t.id]&&(r=window["navtreeexpansions"+t.id]);for(var i in r){var s=(new BRANCH({tree:this,branchobj:r[i],overrides:{expandable:!0,children:[],haschildren:!0}})).wire();M.block_navigation.expandablebranchcount++,this.branches[s.get("id")]=s}M.block_navigation.expandablebranchcount>0&&(e.delegate("click",this.fire_branch_action,n.one(".block_tree"),".tree_item.branch[data-expandable]",this),e.delegate("actionkey",this.fire_branch_action,n.one(".block_tree"),".tree_item.branch[data-expandable]",this)),this.get("candock")&&this.initialise_block(e,n)},fire_branch_action:function(e){var t=e.currentTarget.getAttribute("id"),n=this.branches[t];n.ajaxLoad(e)},toggleExpansion:function(e){if(!(!e.target.test("a")||e.keyCode!==0&&e.keyCode!==13)){e.stopPropagation();return}var t=e.target;t.test("li")||(t=t.ancestor("li"));if(!t)return;if(!t.hasClass("depth_1"))if(e.type==="actionkey"){switch(e.action){case"expand":t.removeClass("collapsed"),t.set("aria-expanded",!0);break;case"collapse":t.addClass("collapsed"),t.set("aria-expanded",!1);break;default:t.toggleClass("collapsed"),t.set("aria-expanded",!t.hasClass("collapsed"))}e.halt()}else t.toggleClass("collapsed"),t.set("aria-expanded",!t.hasClass("collapsed"));this.get("accordian")&&t.siblings("li").each(function(){this.get("id")!==t.get("id")&&!this.hasClass("collapsed")&&(this.addClass("collapsed"),this.set("aria-expanded",!1))});if(this.get("candock")){M.core_dock.resize();var n=M.core_dock.getPanel();n.visible&&n.correctWidth()}}},e.extend(s,e.Base,s.prototype,{NAME:"navigation-tree",ATTRS:{instance:{value:null},candock:{validator:e.Lang.isBool,value:!1},accordian:{validator:e.Lang.isBool,value:!1},expansionlimit:{value:0,setter:function(e){return parseInt(e,10)}}}}),M.core_dock&&M.core_dock.genericblock&&e.augment(s,M.core_dock.genericblock),BRANCH=function(){BRANCH.superclass.constructor.apply(this,arguments)},BRANCH.prototype={node:null,initializer:function(t){var i,s;if(t.branchobj!==null){for(i in t.branchobj)this.set(i,t.branchobj[i]);s=this.get("children"),this.set("haschildren",s.length>0)}if(t.overrides!==null)for(i in t.overrides)this.set(i,t.overrides[i]);this.node=e.one("#",this.get("id"));var o=this.get("tree").get("expansionlimit"),u=this.get("type");o!==n&&u>=o&&u<=r&&(this.set("expandable",!1),this.set("haschildren",!1))},draw:function(t){var n=this.get("expandable")||this.get("haschildren"),r=e.Node.create("
  • "),s=this.get("link"),o=e.Node.create('

    ').setAttribute("id",this.get("id"));s||o.setAttribute("tabindex","0"),n&&(r.addClass("collapsed").addClass("contains_branch"),r.set("aria-expanded",!1),o.addClass("branch"));var u=!1,a=this.get("icon");if(a&&(!n||this.get("type")==i.ACTIVITY)){u=e.Node.create(''),u.setAttribute("src",M.util.image_url(a.pix,a.component)),r.addClass("item_with_icon"),a.alt&&u.setAttribute("alt",a.alt),a.title&&u.setAttribute("title",a.title);if(a.classes)for(var f in a.classes)u.addClass(a.classes[f])}if(!s){var l=e.Node.create("");u&&l.appendChild(u),l.append(this.get("name")),this.get("hidden")&&l.addClass("dimmed_text"),o.appendChild(l)}else{var c=e.Node.create('');u&&c.appendChild(u),c.append(this.get("name")),this.get("hidden")&&c.addClass("dimmed"),o.appendChild(c)}return r.appendChild(o),t.appendChild(r),this.node=o,this},wire:function(){return this.node=this.node||e.one("#"+this.get("id")),this.node?(this.get("expandable")&&(this.node.setAttribute("data-expandable","1"),this.node.setAttribute("data-loaded","0")),this):this},getChildrenUL:function(){var t=this.node.next("ul");return t||(t=e.Node.create(""),this.node.ancestor().append(t)),t},ajaxLoad:function(t){t.type==="actionkey"&&t.action!=="enter"?t.halt():t.stopPropagation();if(t.type==="actionkey"&&t.action==="enter"&&t.target.test("A"))return this.node.setAttribute("data-expandable","0"),this.node.setAttribute("data-loaded","1"),!0;if(this.node.hasClass("loadingbranch"))return!0;if(this.node.getAttribute("data-loaded")==="1")return!0;this.node.addClass("loadingbranch");var n={elementid:this.get("id"),id:this.get("key"),type:this.get("type"),sesskey:M.cfg.sesskey,instance:this.get("tree").get("instance")};return e.io(M.cfg.wwwroot+"/lib/ajax/getnavbranch.php",{method:"POST",data:build_querystring(n),on:{complete:this.ajaxProcessResponse},context:this}),!0},ajaxProcessResponse:function(t,n){this.node.removeClass("loadingbranch"),this.node.setAttribute("data-loaded","1");try{var r=e.JSON.parse(n.responseText);if(r.children&&r.children -.length>0){var s=0;for(var o in r.children)typeof r.children[o]=="object"&&(r.children[o].type==i.COURSE&&s++,this.addChild(r.children[o]));return(this.get("type")==i.CATEGORY||this.get("type")==i.ROOTNODE||this.get("type")==i.MYCATEGORY)&&s>=M.block_navigation.courselimit&&this.addViewAllCoursesChild(this),!0}}catch(u){}return this.node.replaceClass("branch","emptybranch"),!0},addChild:function(e){var t=new BRANCH({tree:this.get("tree"),branchobj:e});if(t.draw(this.getChildrenUL())){this.get("tree").branches[t.get("id")]=t,t.wire();var n=0,r,s=t.get("children");for(r in s)s[r].type==i.COURSE&&n++,typeof s[r]=="object"&&t.addChild(s[r]);(t.get("type")==i.CATEGORY||t.get("type")==i.MYCATEGORY)&&n>=M.block_navigation.courselimit&&this.addViewAllCoursesChild(t)}return!0},addViewAllCoursesChild:function(e){var t=null;e.get("type")==i.ROOTNODE?e.get("key")==="mycourses"?t=M.cfg.wwwroot+"/my":t=M.cfg.wwwroot+"/course/index.php":t=M.cfg.wwwroot+"/course/index.php?categoryid="+e.get("key"),e.addChild({name:M.str.moodle.viewallcourses,title:M.str.moodle.viewallcourses,link:t,haschildren:!1,icon:{pix:"i/navigationitem",component:"moodle"}})}},e.extend(BRANCH,e.Base,BRANCH.prototype,{NAME:"navigation-branch",ATTRS:{tree:{validator:e.Lang.isObject},name:{value:"",validator:e.Lang.isString,setter:function(e){return e.replace(/\n/g,"
    ")}},title:{value:"",validator:e.Lang.isString},id:{value:"",validator:e.Lang.isString,getter:function(e){return e===""&&(e="expandable_branch_"+M.block_navigation.expandablebranchcount,M.block_navigation.expandablebranchcount++),e}},key:{value:null},type:{value:null},link:{value:!1},icon:{value:!1,validator:e.Lang.isObject},expandable:{value:!1,validator:e.Lang.isBool},hidden:{value:!1,validator:e.Lang.isBool},haschildren:{value:!1,validator:e.Lang.isBool},children:{value:[],validator:e.Lang.isArray}}}),M.block_navigation=M.block_navigation||{expandablebranchcount:1,courselimit:20,instance:null,init_add_tree:function(t){t.courselimit&&(this.courselimit=t.courselimit),M.core_dock&&M.core_dock.init(e),new s(t)}}},"@VERSION@",{requires:["base","core_dock","io-base","node","dom","event-custom","event-delegate","json-parse"]}); +YUI.add("moodle-block_navigation-navigation",function(e,t){M.block_navigation=M.block_navigation||{},M.block_navigation.expandablebranchcount=1,M.block_navigation.courselimit=20,M.block_navigation.init_add_tree=function(e){e.courselimit&&(this.courselimit=e.courselimit),new u(e)},e.Event.define("actionkey",{_event:e.UA.webkit||e.UA.ie?"keydown":"keypress",_keys:{37:"collapse",39:"expand",32:"toggle",13:"enter"},_keyHandler:function(e,t,n){var r;n.actions?r=n.actions:r={collapse:!0,expand:!0,toggle:!0,enter:!0},this._keys[e.keyCode]&&r[this._keys[e.keyCode]]&&(e.action=this._keys[e.keyCode],t.fire(e))},on:function(e,t,n){t.args===null?t._detacher=e.on(this._event,this._keyHandler,this,n,{actions:!1}):t._detacher=e.on(this._event,this._keyHandler,this,n,t.args[0])},detach:function(e,t){t._detacher.detach()},delegate:function(e,t,n,r){t.args===null?t._delegateDetacher=e.delegate(this._event,this._keyHandler,r,this,n,{actions:!1}):t._delegateDetacher=e.delegate(this._event,this._keyHandler,r,this,n,t.args[0])},detachDelegate:function(e,t){t._delegateDetacher.detach()}});var n=0,r=20,i=30,s=40,o={ROOTNODE:0,SYSTEM:1,CATEGORY:10,MYCATEGORY:11,COURSE:20,SECTION:30,ACTIVITY:40,RESOURCE:50,CUSTOM:60,SETTING:70,USER:80,CONTAINER:90},u=function(){u.superclass.constructor.apply(this,arguments)};u.prototype={id:null,branches:[],initializer:function(t){this.id=parseInt(t.id,10);var n=e.one("#inst"+t.id);if(n===null)return;e.delegate("click",this.toggleExpansion,n.one(".block_tree"),".tree_item.branch",this),e.delegate("actionkey",this.toggleExpansion,n.one(".block_tree"),".tree_item.branch",this);var r=[];t.expansions?r=t.expansions:window["navtreeexpansions"+t.id]&&(r=window["navtreeexpansions"+t.id]);for(var i in r){var s=(new BRANCH({tree:this,branchobj:r[i],overrides:{expandable:!0,children:[],haschildren:!0}})).wire();M.block_navigation.expandablebranchcount++,this.branches[s.get("id")]=s}M.block_navigation.expandablebranchcount>0&&(e.delegate("click",this.fire_branch_action,n.one(".block_tree"),".tree_item.branch[data-expandable]",this),e.delegate("actionkey",this.fire_branch_action,n.one(".block_tree"),".tree_item.branch[data-expandable]",this))},fire_branch_action:function(e){var t=e.currentTarget.getAttribute("id"),n=this.branches[t];n.ajaxLoad(e)},toggleExpansion:function(e){if(!e.target.test("a")||e.keyCode!==0&&e.keyCode!==13){var t=e.target;t.test("li")||(t=t.ancestor("li"));if(!t)return;if(!t.hasClass("depth_1"))if(e.type==="actionkey"){switch(e.action){case"expand":t.removeClass("collapsed"),t.set("aria-expanded",!0);break;case"collapse":t.addClass("collapsed"),t.set("aria-expanded",!1);break;default:t.toggleClass("collapsed"),t.set("aria-expanded",!t.hasClass("collapsed"))}e.halt()}else t.toggleClass("collapsed"),t.set("aria-expanded",!t.hasClass("collapsed"));return this.get("accordian")&&t.siblings("li").each(function(){this.get("id")!==t.get("id")&&!this.hasClass("collapsed")&&(this.addClass("collapsed"),this.set("aria-expanded",!1))}),this.get("candock")&&M.core.dock.notifyBlockChange&&M.core.dock.notifyBlockChange(this.id),!0}e.stopPropagation();return}},e.extend(u,e.Base,u.prototype,{NAME:"navigation-tree",ATTRS:{candock:{validator:e.Lang.isBool,value:!1},accordian:{validator:e.Lang.isBool,value:!1},expansionlimit:{value:0,setter:function(e){return e=parseInt(e,10),e!==n&&e!==r&&e!==i&&e!==s&&(e=n),e}}}}),BRANCH=function(){BRANCH.superclass.constructor.apply(this,arguments)},BRANCH.prototype={node:null,initializer:function(t){var r,i;if(t.branchobj!==null){for(r in t.branchobj)this.set(r,t.branchobj[r]);i=this.get("children"),this.set("haschildren",i.length>0)}if(t.overrides!==null)for(r in t.overrides)this.set(r,t.overrides[r]);this.node=e.one("#"+this.get("id"));var o=this.get("tree").get("expansionlimit"),u=this.get("type");o!==n&&u>=o&&u<=s&&(this.set("expandable",!1),this.set("haschildren",!1))},draw:function(t){var n=this.get("expandable")||this.get("haschildren"),r=e.Node.create("
  • "),i=this.get("link"),s=e.Node.create('

    ').setAttribute("id",this.get("id"));i||s.setAttribute("tabindex","0"),n&&(r.addClass("collapsed").addClass("contains_branch"),r.set("aria-expanded",!1),s.addClass("branch"));var u=!1,a=this.get("icon");if(a&&(!n||this.get("type")===o.ACTIVITY)){u=e.Node.create(''),u.setAttribute("src",M.util.image_url(a.pix,a.component)),r.addClass("item_with_icon"),a.alt&&u.setAttribute("alt",a.alt),a.title&&u.setAttribute("title",a.title);if(a.classes)for(var f in a.classes)u.addClass(a.classes[f])}if(!i){var l=e.Node.create("");u&&l.appendChild(u),l.append(this.get("name")),this.get("hidden")&&l.addClass("dimmed_text"),s.appendChild(l)}else{var c=e.Node.create('');u&&c.appendChild(u),c.append(this.get("name")),this.get("hidden")&&c.addClass("dimmed"),s.appendChild(c)}return r.appendChild(s),t.appendChild(r),this.node=s,this},wire:function(){return this.node=this.node||e.one("#"+this.get("id")),this.node?(this.get("expandable")&&(this.node.setAttribute("data-expandable","1"),this.node.setAttribute("data-loaded","0")),this):this},getChildrenUL:function(){var t=this.node.next("ul");return t||(t=e.Node.create(""),this.node.ancestor().append(t)),t},ajaxLoad:function(t){t.type==="actionkey"&&t.action!=="enter"?t.halt():t.stopPropagation();if(t.type==="actionkey"&&t.action==="enter"&&t.target.test("A"))return this.node.setAttribute("data-expandable","0"),this.node.setAttribute("data-loaded","1"),!0;if(this.node.hasClass("loadingbranch"))return!0;if(this.node.getAttribute("data-loaded")==="1")return!0;this.node.addClass("loadingbranch");var n={elementid:this.get("id"),id:this.get("key"),type:this.get("type"),sesskey:M.cfg.sesskey,instance:this.get("tree").get("instance")};return e.io(M.cfg.wwwroot+"/lib/ajax/getnavbranch.php",{method:"POST",data:build_querystring(n),on:{complete:this.ajaxProcessResponse},context:this}),!0},ajaxProcessResponse:function(t,n){this.node +.removeClass("loadingbranch"),this.node.setAttribute("data-loaded","1");try{var r=e.JSON.parse(n.responseText);if(r.children&&r.children.length>0){var i=0;for(var s in r.children)typeof r.children[s]=="object"&&(r.children[s].type===o.COURSE&&i++,this.addChild(r.children[s]));return(this.get("type")===o.CATEGORY||this.get("type")===o.ROOTNODE||this.get("type")===o.MYCATEGORY)&&i>=M.block_navigation.courselimit&&this.addViewAllCoursesChild(this),this.get("tree").get("candock")&&M.core.dock.notifyBlockChange&&M.core.dock.notifyBlockChange(this.get("tree").id),!0}}catch(u){}return this.node.replaceClass("branch","emptybranch"),!0},addChild:function(e){var t=new BRANCH({tree:this.get("tree"),branchobj:e});if(t.draw(this.getChildrenUL())){this.get("tree").branches[t.get("id")]=t,t.wire();var n=0,r,i=t.get("children");for(r in i)i[r].type===o.COURSE&&n++,typeof i[r]=="object"&&t.addChild(i[r]);(t.get("type")===o.CATEGORY||t.get("type")===o.MYCATEGORY)&&n>=M.block_navigation.courselimit&&this.addViewAllCoursesChild(t)}return!0},addViewAllCoursesChild:function(e){var t=null;e.get("type")===o.ROOTNODE?e.get("key")==="mycourses"?t=M.cfg.wwwroot+"/my":t=M.cfg.wwwroot+"/course/index.php":t=M.cfg.wwwroot+"/course/index.php?categoryid="+e.get("key"),e.addChild({name:M.str.moodle.viewallcourses,title:M.str.moodle.viewallcourses,link:t,haschildren:!1,icon:{pix:"i/navigationitem",component:"moodle"}})}},e.extend(BRANCH,e.Base,BRANCH.prototype,{NAME:"navigation-branch",ATTRS:{tree:{writeOnce:"initOnly",validator:e.Lang.isObject},name:{value:"",validator:e.Lang.isString,setter:function(e){return e.replace(/\n/g,"
    ")}},title:{value:"",validator:e.Lang.isString},id:{value:"",validator:e.Lang.isString,getter:function(e){return e===""&&(e="expandable_branch_"+M.block_navigation.expandablebranchcount,M.block_navigation.expandablebranchcount++),e}},key:{value:null},type:{value:null,setter:function(e){return parseInt(e,10)}},link:{value:!1},icon:{value:!1,validator:e.Lang.isObject},expandable:{value:!1,validator:e.Lang.isBool},hidden:{value:!1,validator:e.Lang.isBool},haschildren:{value:!1,validator:e.Lang.isBool},children:{value:[],validator:e.Lang.isArray}}})},"@VERSION@",{requires:["base","io-base","node","event-synthetic","event-delegate","json-parse"]}); diff --git a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js index e6bc95409da..bed94d78f85 100644 --- a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js +++ b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js @@ -1,5 +1,51 @@ YUI.add('moodle-block_navigation-navigation', function (Y, NAME) { +/** + * Navigation block JS. + * + * This file contains the Navigation block JS.. + * + * @module moodle-block_navigation-navigation + */ + +/** + * This namespace will contain all of the contents of the navigation blocks + * global navigation and settings. + * @namespace M + * @class block_navigation + * @static + */ +M.block_navigation = M.block_navigation || {}; +/** + * The number of expandable branches in existence. + * + * @property expandablebranchcount + * @protected + * @static + */ +M.block_navigation.expandablebranchcount = 1; +/** + * The maximum number of courses to show as part of a branch. + * + * @property courselimit + * @protected + * @static + */ +M.block_navigation.courselimit = 20; +/** + * Add new instance of navigation tree to tree collection + * + * @method init_add_tree + * @static + * @param {Object} properties + */ +M.block_navigation.init_add_tree = function(properties) { + if (properties.courselimit) { + this.courselimit = properties.courselimit; + } + new TREE(properties); +}; + /** * A 'actionkey' Event to help with Y.delegate(). * The event consists of the left arrow, right arrow, enter and space keys. @@ -9,24 +55,35 @@ YUI.add('moodle-block_navigation-navigation', function (Y, NAME) { * This event is delegated to branches in the navigation tree. * The on() method to subscribe allows specifying the desired trigger actions as JSON. * - * Todo: This could be centralised, a similar Event is defined in blocks/dock.js + * @namespace M.block_navigation + * @class ActionKey */ Y.Event.define("actionkey", { - // Webkit and IE repeat keydown when you hold down arrow keys. + // Webkit and IE repeat keydown when you hold down arrow keys. // Opera links keypress to page scroll; others keydown. // Firefox prevents page scroll via preventDefault() on either // keydown or keypress. _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress', + /** + * The keys to trigger on. + * @method _keys + */ _keys: { //arrows '37': 'collapse', '39': 'expand', - //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings) '32': 'toggle', '13': 'enter' }, + /** + * Handles key events + * @method _keyHandler + * @param {EventFacade} e + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {Object} args + */ _keyHandler: function (e, notifier, args) { var actObj; if (!args.actions) { @@ -40,6 +97,13 @@ Y.Event.define("actionkey", { } }, + /** + * Subscribes to events. + * @method on + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + */ on: function (node, sub, notifier) { // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). if (sub.args === null) { @@ -50,11 +114,23 @@ Y.Event.define("actionkey", { } }, + /** + * Detaches an event listener + * @method detach + */ detach: function (node, sub) { //detach our _detacher handle of the subscription made in on() sub._detacher.detach(); }, + /** + * Creates a delegated event listener. + * @method delegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ delegate: function (node, sub, notifier, filter) { // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). if (sub.args === null) { @@ -65,45 +141,50 @@ Y.Event.define("actionkey", { } }, + /** + * Detaches a delegated event listener. + * @method detachDelegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ detachDelegate: function (node, sub) { sub._delegateDetacher.detach(); } }); var EXPANSIONLIMIT_EVERYTHING = 0, - //EXPANSIONLIMIT_COURSE = 20, - //EXPANSIONLIMIT_SECTION = 30, + EXPANSIONLIMIT_COURSE = 20, + EXPANSIONLIMIT_SECTION = 30, EXPANSIONLIMIT_ACTIVITY = 40; -/** - * Mappings for the different types of nodes coming from the navigation. - * Copied from lib/navigationlib.php navigation_node constants. - * @type object - */ +// Mappings for the different types of nodes coming from the navigation. +// Copied from lib/navigationlib.php navigation_node constants. var NODETYPE = { - /** @type int Root node = 0 */ + // @type int Root node = 0 ROOTNODE : 0, - /** @type int System context = 1 */ + // @type int System context = 1 SYSTEM : 1, - /** @type int Course category = 10 */ + // @type int Course category = 10 CATEGORY : 10, - /** @type int MYCATEGORY = 11 */ + // @type int MYCATEGORY = 11 MYCATEGORY : 11, - /** @type int Course = 20 */ + // @type int Course = 20 COURSE : 20, - /** @type int Course section = 30 */ + // @type int Course section = 30 SECTION : 30, - /** @type int Activity (course module) = 40 */ + // @type int Activity (course module) = 40 ACTIVITY : 40, - /** @type int Resource (course module = 50 */ + // @type int Resource (course module = 50 RESOURCE : 50, - /** @type int Custom node (could be anything) = 60 */ + // @type int Custom node (could be anything) = 60 CUSTOM : 60, - /** @type int Setting = 70 */ + // @type int Setting = 70 SETTING : 70, - /** @type int User context = 80 */ + // @type int User context = 80 USER : 80, - /** @type int Container = 90 */ + // @type int Container = 90 CONTAINER : 90 }; @@ -112,6 +193,11 @@ var NODETYPE = { * * This class establishes the tree initially, creating expandable branches as * required, and delegating the expand/collapse event. + * + * @namespace M.block_navigation + * @class Tree + * @constructor + * @extends Y.Base */ var TREE = function() { TREE.superclass.constructor.apply(this, arguments); @@ -119,17 +205,26 @@ var TREE = function() { TREE.prototype = { /** * The tree's ID, normally its block instance id. + * @property id + * @type Int + * @protected */ id : null, /** * An array of initialised branches. + * @property branches + * @type Array + * @protected */ branches : [], /** * Initialise the tree object when its first created. + * @method initializer + * @param {Object} config */ initializer : function(config) { - this.id = config.id; + + this.id = parseInt(config.id, 10); var node = Y.one('#inst'+config.id); @@ -168,14 +263,11 @@ TREE.prototype = { Y.delegate('click', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this); Y.delegate('actionkey', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this); } - - // Call the generic blocks init method to add all the generic stuff - if (this.get('candock')) { - this.initialise_block(Y, node); - } }, /** * Fire actions for a branch when an event occurs. + * @method fire_branch_action + * @param {EventFacade} event */ fire_branch_action : function(event) { var id = event.currentTarget.getAttribute('id'); @@ -185,6 +277,9 @@ TREE.prototype = { /** * This is a callback function responsible for expanding and collapsing the * branches of the tree. It is delegated to rather than multiple event handles. + * @method toggleExpansion + * @param {EventFacade} e + * @return Boolean */ toggleExpansion : function(e) { // First check if they managed to click on the li iteslf, then find the closest @@ -240,46 +335,66 @@ TREE.prototype = { // If this block can dock tell the dock to resize if required and check // the width on the dock panel in case it is presently in use. - if (this.get('candock')) { - M.core_dock.resize(); - var panel = M.core_dock.getPanel(); - if (panel.visible) { - panel.correctWidth(); - } + if (this.get('candock') && M.core.dock.notifyBlockChange) { + M.core.dock.notifyBlockChange(this.id); } + return true; + } }; // The tree extends the YUI base foundation. Y.extend(TREE, Y.Base, TREE.prototype, { NAME : 'navigation-tree', ATTRS : { - instance : { - value : null - }, + /** + * True if the block can dock. + * @attribute candock + * @type Boolean + */ candock : { validator : Y.Lang.isBool, value : false }, + /** + * If set to true nodes will be opened/closed in an accordian fashion. + * @attribute accordian + * @type Boolean + */ accordian : { validator : Y.Lang.isBool, value : false }, + /** + * The nodes that get shown. + * @attribute expansionlimit + * @type Integer + */ expansionlimit : { value : 0, setter : function(val) { - return parseInt(val, 10); + val = parseInt(val, 10); + if (val !== EXPANSIONLIMIT_EVERYTHING && + val !== EXPANSIONLIMIT_COURSE && + val !== EXPANSIONLIMIT_SECTION && + val !== EXPANSIONLIMIT_ACTIVITY) { + val = EXPANSIONLIMIT_EVERYTHING; + } + return val; } } } }); -if (M.core_dock && M.core_dock.genericblock) { - Y.augment(TREE, M.core_dock.genericblock); -} /** - * The tree branch class. + * The Branch class. + * * This class is used to manage a tree branch, in particular its ability to load * its contents by AJAX. + * + * @namespace M.block_navigation + * @class Branch + * @constructor + * @extends Y.Base */ BRANCH = function() { BRANCH.superclass.constructor.apply(this, arguments); @@ -287,10 +402,15 @@ BRANCH = function() { BRANCH.prototype = { /** * The node for this branch (p) + * @property node + * @type Node + * @protected */ node : null, /** * Initialises the branch when it is first created. + * @method initializer + * @param {Object} config */ initializer : function(config) { var i, @@ -310,8 +430,7 @@ BRANCH.prototype = { } } // Get the node for this branch - this.node = Y.one('#', this.get('id')); - // Now check whether the branch is not expandable because of the expansionlimit + this.node = Y.one('#'+this.get('id')); var expansionlimit = this.get('tree').get('expansionlimit'); var type = this.get('type'); if (expansionlimit !== EXPANSIONLIMIT_EVERYTHING && type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) { @@ -324,6 +443,11 @@ BRANCH.prototype = { * * This function creates a DOM structure for the branch and then injects * it into the navigation tree at the correct point. + * + * @method draw + * @chainable + * @param {Node} element + * @return Branch */ draw : function(element) { @@ -345,7 +469,7 @@ BRANCH.prototype = { // Prepare the icon, should be an object representing a pix_icon var branchicon = false; var icon = this.get('icon'); - if (icon && (!isbranch || this.get('type') == NODETYPE.ACTIVITY)) { + if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY)) { branchicon = Y.Node.create(''); branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component)); branchli.addClass('item_with_icon'); @@ -409,6 +533,8 @@ BRANCH.prototype = { }, /** * Gets the UL element that children for this branch should be inserted into. + * @method getChildrenUL + * @return Node */ getChildrenUL : function() { var ul = this.node.next('ul'); @@ -423,6 +549,10 @@ BRANCH.prototype = { * * This function calls ajaxProcessResponse with the result of the AJAX * request made here. + * + * @method ajaxLoad + * @param {EventFacade} e + * @return Bool */ ajaxLoad : function(e) { if (e.type === 'actionkey' && e.action !== 'enter') { @@ -469,6 +599,11 @@ BRANCH.prototype = { /** * Processes an AJAX request to load the content of this branch through * AJAX. + * + * @method ajaxProcessResponse + * @param {Int} tid The transaction id. + * @param {Object} outcome + * @return Boolean */ ajaxProcessResponse : function(tid, outcome) { this.node.removeClass('loadingbranch'); @@ -479,20 +614,25 @@ BRANCH.prototype = { var coursecount = 0; for (var i in object.children) { if (typeof(object.children[i])==='object') { - if (object.children[i].type == NODETYPE.COURSE) { + if (object.children[i].type === NODETYPE.COURSE) { coursecount++; } this.addChild(object.children[i]); } } - if ((this.get('type') == NODETYPE.CATEGORY || this.get('type') == NODETYPE.ROOTNODE || this.get('type') == NODETYPE.MYCATEGORY) + if ((this.get('type') === NODETYPE.CATEGORY || this.get('type') === NODETYPE.ROOTNODE || this.get('type') === NODETYPE.MYCATEGORY) && coursecount >= M.block_navigation.courselimit) { this.addViewAllCoursesChild(this); } + // If this block can dock tell the dock to resize if required and check + // the width on the dock panel in case it is presently in use. + if (this.get('tree').get('candock') && M.core.dock.notifyBlockChange) { + M.core.dock.notifyBlockChange(this.get('tree').id); + } return true; } } catch (ex) { - // If we got here then there was an error parsing the result + // If we got here then there was an error parsing the result. } // The branch is empty so class it accordingly this.node.replaceClass('branch', 'emptybranch'); @@ -501,6 +641,10 @@ BRANCH.prototype = { /** * Turns the branch object passed to the method into a proper branch object * and then adds it as a child of this branch. + * + * @method addChild + * @param {Object} branchobj + * @return Boolean */ addChild : function(branchobj) { // Make the new branch into an object @@ -511,14 +655,14 @@ BRANCH.prototype = { var count = 0, i, children = branch.get('children'); for (i in children) { // Add each branch to the tree - if (children[i].type == NODETYPE.COURSE) { + if (children[i].type === NODETYPE.COURSE) { count++; } if (typeof(children[i]) === 'object') { branch.addChild(children[i]); } } - if ((branch.get('type') == NODETYPE.CATEGORY || branch.get('type') == NODETYPE.MYCATEGORY) + if ((branch.get('type') === NODETYPE.CATEGORY || branch.get('type') === NODETYPE.MYCATEGORY) && count >= M.block_navigation.courselimit) { this.addViewAllCoursesChild(branch); } @@ -528,10 +672,13 @@ BRANCH.prototype = { /** * Add a link to view all courses in a category + * + * @method addViewAllCoursesChild + * @param {BRANCH} branch */ addViewAllCoursesChild: function(branch) { var url = null; - if (branch.get('type') == NODETYPE.ROOTNODE) { + if (branch.get('type') === NODETYPE.ROOTNODE) { if (branch.get('key') === 'mycourses') { url = M.cfg.wwwroot + '/my'; } else { @@ -552,9 +699,22 @@ BRANCH.prototype = { Y.extend(BRANCH, Y.Base, BRANCH.prototype, { NAME : 'navigation-branch', ATTRS : { + /** + * The Tree this branch belongs to. + * @attribute tree + * @type TREE + * @required + * @writeOnce + */ tree : { + writeOnce : 'initOnly', validator : Y.Lang.isObject }, + /** + * The name of this branch. + * @attribute name + * @type String + */ name : { value : '', validator : Y.Lang.isString, @@ -562,10 +722,21 @@ Y.extend(BRANCH, Y.Base, BRANCH.prototype, { return val.replace(/\n/g, '
    '); } }, + /** + * The title to use for this branch. + * @attribute title + * @type String + */ title : { value : '', validator : Y.Lang.isString }, + /** + * The ID of this branch. + * The ID and Type should always form a unique pair. + * @attribute id + * @type String + */ id : { value : '', validator : Y.Lang.isString, @@ -577,31 +748,74 @@ Y.extend(BRANCH, Y.Base, BRANCH.prototype, { return val; } }, + /** + * The key used to identify this branch easily if there is one. + * @attribute key + * @type String + */ key : { value : null }, + /** + * The type of this branch. + * @attribute type + * @type Int + */ type : { - value : null + value : null, + setter : function(value) { + return parseInt(value, 10); + } }, + /** + * The link to use for this branch. + * @attribute link + * @type String + */ link : { value : false }, + /** + * The Icon to add when displaying this branch. + * @attribute icon + * @type Object + */ icon : { value : false, validator : Y.Lang.isObject }, + /** + * True if this branch is expandable. + * @attribute expandable + * @type Boolean + */ expandable : { value : false, validator : Y.Lang.isBool }, + /** + * True if this branch is hidden and should be displayed greyed out. + * @attribute hidden + * @type Boolean + */ hidden : { value : false, validator : Y.Lang.isBool }, + /** + * True if this branch has any children. + * @attribute haschildren + * @type Boolean + */ haschildren : { value : false, validator : Y.Lang.isBool }, + /** + * An array of other branches that appear as children of this branch. + * @attribute children + * @type Array + */ children : { value : [], validator : Y.Lang.isArray @@ -609,40 +823,5 @@ Y.extend(BRANCH, Y.Base, BRANCH.prototype, { } }); -/** - * This namespace will contain all of the contents of the navigation blocks - * global navigation and settings. - * @namespace - */ -M.block_navigation = M.block_navigation || { - /** The number of expandable branches in existence */ - expandablebranchcount:1, - courselimit : 20, - instance : null, - /** - * Add new instance of navigation tree to tree collection - */ - init_add_tree:function(properties) { - if (properties.courselimit) { - this.courselimit = properties.courselimit; - } - if (M.core_dock) { - M.core_dock.init(Y); - } - new TREE(properties); - } -}; - -}, '@VERSION@', { - "requires": [ - "base", - "core_dock", - "io-base", - "node", - "dom", - "event-custom", - "event-delegate", - "json-parse" - ] -}); +}, '@VERSION@', {"requires": ["base", "io-base", "node", "event-synthetic", "event-delegate", "json-parse"]}); diff --git a/blocks/navigation/yui/src/navigation/js/navigation.js b/blocks/navigation/yui/src/navigation/js/navigation.js index 2141a95f4c3..c7ed3700619 100644 --- a/blocks/navigation/yui/src/navigation/js/navigation.js +++ b/blocks/navigation/yui/src/navigation/js/navigation.js @@ -1,3 +1,49 @@ +/** + * Navigation block JS. + * + * This file contains the Navigation block JS.. + * + * @module moodle-block_navigation-navigation + */ + +/** + * This namespace will contain all of the contents of the navigation blocks + * global navigation and settings. + * @namespace M + * @class block_navigation + * @static + */ +M.block_navigation = M.block_navigation || {}; +/** + * The number of expandable branches in existence. + * + * @property expandablebranchcount + * @protected + * @static + */ +M.block_navigation.expandablebranchcount = 1; +/** + * The maximum number of courses to show as part of a branch. + * + * @property courselimit + * @protected + * @static + */ +M.block_navigation.courselimit = 20; +/** + * Add new instance of navigation tree to tree collection + * + * @method init_add_tree + * @static + * @param {Object} properties + */ +M.block_navigation.init_add_tree = function(properties) { + if (properties.courselimit) { + this.courselimit = properties.courselimit; + } + new TREE(properties); +}; + /** * A 'actionkey' Event to help with Y.delegate(). * The event consists of the left arrow, right arrow, enter and space keys. @@ -7,24 +53,35 @@ * This event is delegated to branches in the navigation tree. * The on() method to subscribe allows specifying the desired trigger actions as JSON. * - * Todo: This could be centralised, a similar Event is defined in blocks/dock.js + * @namespace M.block_navigation + * @class ActionKey */ Y.Event.define("actionkey", { - // Webkit and IE repeat keydown when you hold down arrow keys. + // Webkit and IE repeat keydown when you hold down arrow keys. // Opera links keypress to page scroll; others keydown. // Firefox prevents page scroll via preventDefault() on either // keydown or keypress. _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress', + /** + * The keys to trigger on. + * @method _keys + */ _keys: { //arrows '37': 'collapse', '39': 'expand', - //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings) '32': 'toggle', '13': 'enter' }, + /** + * Handles key events + * @method _keyHandler + * @param {EventFacade} e + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {Object} args + */ _keyHandler: function (e, notifier, args) { var actObj; if (!args.actions) { @@ -38,6 +95,13 @@ Y.Event.define("actionkey", { } }, + /** + * Subscribes to events. + * @method on + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + */ on: function (node, sub, notifier) { // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). if (sub.args === null) { @@ -48,11 +112,23 @@ Y.Event.define("actionkey", { } }, + /** + * Detaches an event listener + * @method detach + */ detach: function (node, sub) { //detach our _detacher handle of the subscription made in on() sub._detacher.detach(); }, + /** + * Creates a delegated event listener. + * @method delegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ delegate: function (node, sub, notifier, filter) { // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). if (sub.args === null) { @@ -63,45 +139,50 @@ Y.Event.define("actionkey", { } }, + /** + * Detaches a delegated event listener. + * @method detachDelegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ detachDelegate: function (node, sub) { sub._delegateDetacher.detach(); } }); var EXPANSIONLIMIT_EVERYTHING = 0, - //EXPANSIONLIMIT_COURSE = 20, - //EXPANSIONLIMIT_SECTION = 30, + EXPANSIONLIMIT_COURSE = 20, + EXPANSIONLIMIT_SECTION = 30, EXPANSIONLIMIT_ACTIVITY = 40; -/** - * Mappings for the different types of nodes coming from the navigation. - * Copied from lib/navigationlib.php navigation_node constants. - * @type object - */ +// Mappings for the different types of nodes coming from the navigation. +// Copied from lib/navigationlib.php navigation_node constants. var NODETYPE = { - /** @type int Root node = 0 */ + // @type int Root node = 0 ROOTNODE : 0, - /** @type int System context = 1 */ + // @type int System context = 1 SYSTEM : 1, - /** @type int Course category = 10 */ + // @type int Course category = 10 CATEGORY : 10, - /** @type int MYCATEGORY = 11 */ + // @type int MYCATEGORY = 11 MYCATEGORY : 11, - /** @type int Course = 20 */ + // @type int Course = 20 COURSE : 20, - /** @type int Course section = 30 */ + // @type int Course section = 30 SECTION : 30, - /** @type int Activity (course module) = 40 */ + // @type int Activity (course module) = 40 ACTIVITY : 40, - /** @type int Resource (course module = 50 */ + // @type int Resource (course module = 50 RESOURCE : 50, - /** @type int Custom node (could be anything) = 60 */ + // @type int Custom node (could be anything) = 60 CUSTOM : 60, - /** @type int Setting = 70 */ + // @type int Setting = 70 SETTING : 70, - /** @type int User context = 80 */ + // @type int User context = 80 USER : 80, - /** @type int Container = 90 */ + // @type int Container = 90 CONTAINER : 90 }; @@ -110,6 +191,11 @@ var NODETYPE = { * * This class establishes the tree initially, creating expandable branches as * required, and delegating the expand/collapse event. + * + * @namespace M.block_navigation + * @class Tree + * @constructor + * @extends Y.Base */ var TREE = function() { TREE.superclass.constructor.apply(this, arguments); @@ -117,17 +203,27 @@ var TREE = function() { TREE.prototype = { /** * The tree's ID, normally its block instance id. + * @property id + * @type Int + * @protected */ id : null, /** * An array of initialised branches. + * @property branches + * @type Array + * @protected */ branches : [], /** * Initialise the tree object when its first created. + * @method initializer + * @param {Object} config */ initializer : function(config) { - this.id = config.id; + Y.log('Initialising navigation block tree', 'note', 'moodle-block_navigation'); + + this.id = parseInt(config.id, 10); var node = Y.one('#inst'+config.id); @@ -166,14 +262,11 @@ TREE.prototype = { Y.delegate('click', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this); Y.delegate('actionkey', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this); } - - // Call the generic blocks init method to add all the generic stuff - if (this.get('candock')) { - this.initialise_block(Y, node); - } }, /** * Fire actions for a branch when an event occurs. + * @method fire_branch_action + * @param {EventFacade} event */ fire_branch_action : function(event) { var id = event.currentTarget.getAttribute('id'); @@ -183,6 +276,9 @@ TREE.prototype = { /** * This is a callback function responsible for expanding and collapsing the * branches of the tree. It is delegated to rather than multiple event handles. + * @method toggleExpansion + * @param {EventFacade} e + * @return Boolean */ toggleExpansion : function(e) { // First check if they managed to click on the li iteslf, then find the closest @@ -238,46 +334,66 @@ TREE.prototype = { // If this block can dock tell the dock to resize if required and check // the width on the dock panel in case it is presently in use. - if (this.get('candock')) { - M.core_dock.resize(); - var panel = M.core_dock.getPanel(); - if (panel.visible) { - panel.correctWidth(); - } + if (this.get('candock') && M.core.dock.notifyBlockChange) { + M.core.dock.notifyBlockChange(this.id); } + return true; + } }; // The tree extends the YUI base foundation. Y.extend(TREE, Y.Base, TREE.prototype, { NAME : 'navigation-tree', ATTRS : { - instance : { - value : null - }, + /** + * True if the block can dock. + * @attribute candock + * @type Boolean + */ candock : { validator : Y.Lang.isBool, value : false }, + /** + * If set to true nodes will be opened/closed in an accordian fashion. + * @attribute accordian + * @type Boolean + */ accordian : { validator : Y.Lang.isBool, value : false }, + /** + * The nodes that get shown. + * @attribute expansionlimit + * @type Integer + */ expansionlimit : { value : 0, setter : function(val) { - return parseInt(val, 10); + val = parseInt(val, 10); + if (val !== EXPANSIONLIMIT_EVERYTHING && + val !== EXPANSIONLIMIT_COURSE && + val !== EXPANSIONLIMIT_SECTION && + val !== EXPANSIONLIMIT_ACTIVITY) { + val = EXPANSIONLIMIT_EVERYTHING; + } + return val; } } } }); -if (M.core_dock && M.core_dock.genericblock) { - Y.augment(TREE, M.core_dock.genericblock); -} /** - * The tree branch class. + * The Branch class. + * * This class is used to manage a tree branch, in particular its ability to load * its contents by AJAX. + * + * @namespace M.block_navigation + * @class Branch + * @constructor + * @extends Y.Base */ BRANCH = function() { BRANCH.superclass.constructor.apply(this, arguments); @@ -285,10 +401,15 @@ BRANCH = function() { BRANCH.prototype = { /** * The node for this branch (p) + * @property node + * @type Node + * @protected */ node : null, /** * Initialises the branch when it is first created. + * @method initializer + * @param {Object} config */ initializer : function(config) { var i, @@ -308,8 +429,7 @@ BRANCH.prototype = { } } // Get the node for this branch - this.node = Y.one('#', this.get('id')); - // Now check whether the branch is not expandable because of the expansionlimit + this.node = Y.one('#'+this.get('id')); var expansionlimit = this.get('tree').get('expansionlimit'); var type = this.get('type'); if (expansionlimit !== EXPANSIONLIMIT_EVERYTHING && type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) { @@ -322,6 +442,11 @@ BRANCH.prototype = { * * This function creates a DOM structure for the branch and then injects * it into the navigation tree at the correct point. + * + * @method draw + * @chainable + * @param {Node} element + * @return Branch */ draw : function(element) { @@ -343,7 +468,7 @@ BRANCH.prototype = { // Prepare the icon, should be an object representing a pix_icon var branchicon = false; var icon = this.get('icon'); - if (icon && (!isbranch || this.get('type') == NODETYPE.ACTIVITY)) { + if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY)) { branchicon = Y.Node.create(''); branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component)); branchli.addClass('item_with_icon'); @@ -407,6 +532,8 @@ BRANCH.prototype = { }, /** * Gets the UL element that children for this branch should be inserted into. + * @method getChildrenUL + * @return Node */ getChildrenUL : function() { var ul = this.node.next('ul'); @@ -421,6 +548,10 @@ BRANCH.prototype = { * * This function calls ajaxProcessResponse with the result of the AJAX * request made here. + * + * @method ajaxLoad + * @param {EventFacade} e + * @return Bool */ ajaxLoad : function(e) { if (e.type === 'actionkey' && e.action !== 'enter') { @@ -444,6 +575,7 @@ BRANCH.prototype = { // We've already loaded this stuff. return true; } + Y.log('Loading navigation branch via AJAX: '+this.get('key'), 'note', 'moodle-block_navigation'); this.node.addClass('loadingbranch'); var params = { @@ -467,6 +599,11 @@ BRANCH.prototype = { /** * Processes an AJAX request to load the content of this branch through * AJAX. + * + * @method ajaxProcessResponse + * @param {Int} tid The transaction id. + * @param {Object} outcome + * @return Boolean */ ajaxProcessResponse : function(tid, outcome) { this.node.removeClass('loadingbranch'); @@ -477,20 +614,28 @@ BRANCH.prototype = { var coursecount = 0; for (var i in object.children) { if (typeof(object.children[i])==='object') { - if (object.children[i].type == NODETYPE.COURSE) { + if (object.children[i].type === NODETYPE.COURSE) { coursecount++; } this.addChild(object.children[i]); } } - if ((this.get('type') == NODETYPE.CATEGORY || this.get('type') == NODETYPE.ROOTNODE || this.get('type') == NODETYPE.MYCATEGORY) + if ((this.get('type') === NODETYPE.CATEGORY || this.get('type') === NODETYPE.ROOTNODE || this.get('type') === NODETYPE.MYCATEGORY) && coursecount >= M.block_navigation.courselimit) { this.addViewAllCoursesChild(this); } + Y.log('AJAX loading complete.', 'note', 'moodle-block_navigation'); + // If this block can dock tell the dock to resize if required and check + // the width on the dock panel in case it is presently in use. + if (this.get('tree').get('candock') && M.core.dock.notifyBlockChange) { + M.core.dock.notifyBlockChange(this.get('tree').id); + } return true; } + Y.log('AJAX loading complete but there were no children.', 'note', 'moodle-block_navigation'); } catch (ex) { - // If we got here then there was an error parsing the result + // If we got here then there was an error parsing the result. + Y.log('Error parsing AJAX response or adding branches to the navigation tree', 'error', 'moodle-block_navigation'); } // The branch is empty so class it accordingly this.node.replaceClass('branch', 'emptybranch'); @@ -499,6 +644,10 @@ BRANCH.prototype = { /** * Turns the branch object passed to the method into a proper branch object * and then adds it as a child of this branch. + * + * @method addChild + * @param {Object} branchobj + * @return Boolean */ addChild : function(branchobj) { // Make the new branch into an object @@ -509,14 +658,14 @@ BRANCH.prototype = { var count = 0, i, children = branch.get('children'); for (i in children) { // Add each branch to the tree - if (children[i].type == NODETYPE.COURSE) { + if (children[i].type === NODETYPE.COURSE) { count++; } if (typeof(children[i]) === 'object') { branch.addChild(children[i]); } } - if ((branch.get('type') == NODETYPE.CATEGORY || branch.get('type') == NODETYPE.MYCATEGORY) + if ((branch.get('type') === NODETYPE.CATEGORY || branch.get('type') === NODETYPE.MYCATEGORY) && count >= M.block_navigation.courselimit) { this.addViewAllCoursesChild(branch); } @@ -526,10 +675,13 @@ BRANCH.prototype = { /** * Add a link to view all courses in a category + * + * @method addViewAllCoursesChild + * @param {BRANCH} branch */ addViewAllCoursesChild: function(branch) { var url = null; - if (branch.get('type') == NODETYPE.ROOTNODE) { + if (branch.get('type') === NODETYPE.ROOTNODE) { if (branch.get('key') === 'mycourses') { url = M.cfg.wwwroot + '/my'; } else { @@ -550,9 +702,22 @@ BRANCH.prototype = { Y.extend(BRANCH, Y.Base, BRANCH.prototype, { NAME : 'navigation-branch', ATTRS : { + /** + * The Tree this branch belongs to. + * @attribute tree + * @type TREE + * @required + * @writeOnce + */ tree : { + writeOnce : 'initOnly', validator : Y.Lang.isObject }, + /** + * The name of this branch. + * @attribute name + * @type String + */ name : { value : '', validator : Y.Lang.isString, @@ -560,10 +725,21 @@ Y.extend(BRANCH, Y.Base, BRANCH.prototype, { return val.replace(/\n/g, '
    '); } }, + /** + * The title to use for this branch. + * @attribute title + * @type String + */ title : { value : '', validator : Y.Lang.isString }, + /** + * The ID of this branch. + * The ID and Type should always form a unique pair. + * @attribute id + * @type String + */ id : { value : '', validator : Y.Lang.isString, @@ -575,58 +751,77 @@ Y.extend(BRANCH, Y.Base, BRANCH.prototype, { return val; } }, + /** + * The key used to identify this branch easily if there is one. + * @attribute key + * @type String + */ key : { value : null }, + /** + * The type of this branch. + * @attribute type + * @type Int + */ type : { - value : null + value : null, + setter : function(value) { + return parseInt(value, 10); + } }, + /** + * The link to use for this branch. + * @attribute link + * @type String + */ link : { value : false }, + /** + * The Icon to add when displaying this branch. + * @attribute icon + * @type Object + */ icon : { value : false, validator : Y.Lang.isObject }, + /** + * True if this branch is expandable. + * @attribute expandable + * @type Boolean + */ expandable : { value : false, validator : Y.Lang.isBool }, + /** + * True if this branch is hidden and should be displayed greyed out. + * @attribute hidden + * @type Boolean + */ hidden : { value : false, validator : Y.Lang.isBool }, + /** + * True if this branch has any children. + * @attribute haschildren + * @type Boolean + */ haschildren : { value : false, validator : Y.Lang.isBool }, + /** + * An array of other branches that appear as children of this branch. + * @attribute children + * @type Array + */ children : { value : [], validator : Y.Lang.isArray } } }); - -/** - * This namespace will contain all of the contents of the navigation blocks - * global navigation and settings. - * @namespace - */ -M.block_navigation = M.block_navigation || { - /** The number of expandable branches in existence */ - expandablebranchcount:1, - courselimit : 20, - instance : null, - /** - * Add new instance of navigation tree to tree collection - */ - init_add_tree:function(properties) { - if (properties.courselimit) { - this.courselimit = properties.courselimit; - } - if (M.core_dock) { - M.core_dock.init(Y); - } - new TREE(properties); - } -}; diff --git a/blocks/navigation/yui/src/navigation/meta/navigation.json b/blocks/navigation/yui/src/navigation/meta/navigation.json index b4c37600ab5..c544b511070 100644 --- a/blocks/navigation/yui/src/navigation/meta/navigation.json +++ b/blocks/navigation/yui/src/navigation/meta/navigation.json @@ -2,11 +2,9 @@ "moodle-block_navigation-navigation": { "requires": [ "base", - "core_dock", "io-base", "node", - "dom", - "event-custom", + "event-synthetic", "event-delegate", "json-parse" ] diff --git a/blocks/settings/block_settings.php b/blocks/settings/block_settings.php index 516673ef813..7540a724c9e 100644 --- a/blocks/settings/block_settings.php +++ b/blocks/settings/block_settings.php @@ -91,10 +91,13 @@ class block_settings extends block_base { } function get_required_javascript() { - global $CFG; - $arguments = array('id' => $this->instance->id, 'instance' => $this->instance->id, 'candock' => $this->instance_can_be_docked()); - $this->page->requires->yui_module(array('core_dock', 'moodle-block_navigation-navigation'), 'M.block_navigation.init_add_tree', array($arguments)); - user_preference_allow_ajax_update('docked_block_instance_'.$this->instance->id, PARAM_INT); + parent::get_required_javascript(); + $arguments = array( + 'id' => $this->instance->id, + 'instance' => $this->instance->id, + 'candock' => $this->instance_can_be_docked() + ); + $this->page->requires->yui_module('moodle-block_navigation-navigation', 'M.block_navigation.init_add_tree', array($arguments)); } /** diff --git a/lib/classes/component.php b/lib/classes/component.php index 0c7a8fa7bc8..9237738f151 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -231,7 +231,6 @@ $cache = '.var_export($cache, true).'; 'currencies' => null, 'dbtransfer' => null, 'debug' => null, - 'dock' => null, 'editor' => $CFG->dirroot.'/lib/editor', 'edufields' => null, 'enrol' => $CFG->dirroot.'/enrol', diff --git a/lib/outputcomponents.php b/lib/outputcomponents.php index 344d4df486a..df1ec179265 100644 --- a/lib/outputcomponents.php +++ b/lib/outputcomponents.php @@ -2438,6 +2438,12 @@ class block_contents { */ public $collapsible = self::NOT_HIDEABLE; + /** + * Set this to true if the block is dockable. + * @var bool + */ + public $dockable = false; + /** * @var array A (possibly empty) array of editing controls. Each element of * this array should be an array('url' => $url, 'icon' => $icon, 'caption' => $caption). diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 0e5c9599fcc..3c90a272ed8 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -1070,12 +1070,18 @@ class core_renderer extends renderer_base { if (empty($bc->blockinstanceid) || !strip_tags($bc->title)) { $bc->collapsible = block_contents::NOT_HIDEABLE; } + if (!empty($bc->blockinstanceid)) { + $bc->attributes['data-instanceid'] = $bc->blockinstanceid; + } $skiptitle = strip_tags($bc->title); if ($bc->blockinstanceid && !empty($skiptitle)) { $bc->attributes['aria-labelledby'] = 'instance-'.$bc->blockinstanceid.'-header'; } else if (!empty($bc->arialabel)) { $bc->attributes['aria-label'] = $bc->arialabel; } + if ($bc->dockable) { + $bc->attributes['data-dockable'] = 1; + } if ($bc->collapsible == block_contents::HIDDEN) { $bc->add_class('hidden'); } diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php index 76310d94a2f..d750713a411 100644 --- a/lib/outputrequirementslib.php +++ b/lib/outputrequirementslib.php @@ -245,7 +245,6 @@ class page_requirements_manager { // Every page should include definition of following modules. $this->js_module($this->find_module('core_filepicker')); - $this->js_module($this->find_module('core_dock')); } /** @@ -673,15 +672,6 @@ class page_requirements_manager { $module = array('name' => 'core_completion', 'fullpath' => '/course/completion.js'); break; - case 'core_dock': - $module = array('name' => 'core_dock', - 'fullpath' => '/blocks/dock.js', - 'requires' => array('base', 'node', 'event-custom', 'event-mouseenter', 'event-resize', 'escape'), - 'strings' => array(array('addtodock', 'block'),array('undockitem', 'block'),array('dockblock', 'block'), - array('undockblock', 'block'),array('undockall', 'block'),array('thisdirectionvertical', 'langconfig'), - array('hidedockpanel', 'block'),array('hidepanel', 'block') - )); - break; case 'core_message': $module = array('name' => 'core_message', 'requires' => array('base', 'node', 'event', 'node-event-simulate'), diff --git a/lib/pagelib.php b/lib/pagelib.php index fd29d8f5438..558b8749169 100644 --- a/lib/pagelib.php +++ b/lib/pagelib.php @@ -1444,7 +1444,7 @@ class moodle_page { * @return void */ public function initialise_theme_and_output() { - global $OUTPUT, $PAGE, $SITE; + global $OUTPUT, $PAGE, $SITE, $CFG; if (!empty($this->_wherethemewasinitialised)) { return; @@ -1465,6 +1465,11 @@ class moodle_page { } $this->_theme->setup_blocks($this->pagelayout, $this->blocks); + if ($this->_theme->enable_dock && !empty($CFG->allowblockstodock)) { + $this->requires->strings_for_js(array('addtodock', 'undockitem', 'dockblock', 'undockblock', 'undockall', 'hidedockpanel', 'hidepanel'), 'block'); + $this->requires->string_for_js('thisdirectionvertical', 'langconfig'); + $this->requires->yui_module('moodle-core-dockloader', 'M.core.dock.loader.initLoader'); + } if ($this === $PAGE) { $OUTPUT = $this->get_renderer('core'); diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index 3380b404657..79b26ac0718 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -47,7 +47,7 @@ class core_component_testcase extends advanced_testcase { foreach($subsystems as $subsystem => $fulldir) { $this->assertFalse(strpos($subsystem, '_'), 'Core subsystems must be one work without underscores'); if ($fulldir === null) { - if ($subsystem === 'dock' or $subsystem === 'filepicker' or $subsystem === 'help') { + if ($subsystem === 'filepicker' or $subsystem === 'help') { // Arrgghh, let's not introduce more subsystems for no real reason... } else { // Lang strings. diff --git a/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js b/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js new file mode 100644 index 00000000000..c0fb9817562 --- /dev/null +++ b/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js @@ -0,0 +1,2116 @@ +YUI.add('moodle-core-dock', function (Y, NAME) { + +/** + * Dock JS. + * + * This file contains the DOCK object and all dock related global namespace methods and properties. + * + * @module moodle-core-dock + */ + + +var LOGNS = 'moodle-core-dock'; +var BODY = Y.one(document.body); +var CSS = { + dock: 'dock', // CSS Class applied to the dock box + dockspacer: 'dockspacer', // CSS class applied to the dockspacer + controls: 'controls', // CSS class applied to the controls box + body: 'has_dock', // CSS class added to the body when there is a dock + buttonscontainer: 'buttons_container', + dockeditem: 'dockeditem', // CSS class added to each item in the dock + dockeditemcontainer: 'dockeditem_container', + dockedtitle: 'dockedtitle', // CSS class added to the item's title in each dock + activeitem: 'activeitem', // CSS class added to the active item + contentonly: 'content-only', + dockonload: 'dock_on_load' +}; +var SELECTOR = { + dockableblock: '.block[data-instanceid][data-dockable]', + blockmoveto: '.block[data-instanceid][data-dockable] .moveto', + panelmoveto: '#dockeditempanel .commands a.moveto', + dockonload: '.block.'+CSS.dockonload, + blockregion: '[data-blockregion]' +}; + +/** + * Core namespace. + * + * @static + * @namespace M + * @class core + */ +M.core = M.core || {}; + +/** + * Dock namespace. + * + * @static + * @namespace M.core + * @class dock + */ +M.core.dock = M.core.dock || {}; + +/** + * The dock - once initialised. + * + * @private + * @property _dock + * @type DOCK + */ +M.core.dock._dock = null; + +/** + * An associative array of dockable blocks. + * @property _dockableblocks + * @type {Array} An array of BLOCK objects organised by instanceid. + * @private + */ +M.core.dock._dockableblocks = {}; + +/** + * Initialises the dock. + * This method registers dockable blocks, and creates delegations to dock them. + * @static + * @method init + */ +M.core.dock.init = function() { + Y.all(SELECTOR.dockableblock).each(M.core.dock.registerDockableBlock); + BODY.delegate('click', M.core.dock.dockBlock, SELECTOR.blockmoveto); + BODY.delegate('key', M.core.dock.dockBlock, SELECTOR.blockmoveto, 'enter'); +}; + +/** + * Returns an instance of the dock. + * Initialises one if one hasn't already being initialised. + * + * @static + * @method get + * @return DOCK + */ +M.core.dock.get = function() { + if (this._dock === null) { + this._dock = new DOCK(); + } + return this._dock; +}; + +/** + * Registers a dockable block with the dock. + * + * @static + * @method registerDockableBlock + * @param {int} id The block instance ID. + * @return void + */ +M.core.dock.registerDockableBlock = function(id) { + if (typeof id === 'object' && typeof id.getData === 'function') { + id = id.getData('instanceid'); + } + M.core.dock._dockableblocks[id] = new BLOCK({id : id}); +}; + +/** + * Docks a block given either its instanceid, its node, or an event fired from within the block. + * @static + * @method dockBlockByInstanceID + * @param id + * @return void + */ +M.core.dock.dockBlock = function(id) { + if (typeof id === 'object' && id.target !== 'undefined') { + id = id.target; + } + if (typeof id === "object") { + if (!id.test(SELECTOR.dockableblock)) { + id = id.ancestor(SELECTOR.dockableblock); + } + if (typeof id === 'object' && typeof id.getData === 'function' && !id.ancestor('.'+CSS.dock)) { + id = id.getData('instanceid'); + } else { + Y.log('Invalid instanceid given to dockBlockByInstanceID', 'warn', LOADERNAME); + return; + } + } + var block = M.core.dock._dockableblocks[id]; + if (block) { + block.moveToDock(); + } +}; + +/** + * Fixes the title orientation. Rotating it if required. + * + * @static + * @method fixTitleOrientation + * @param {Node} title The title node we are looking at. + * @param {String} text The string to use as the title. + * @return {Node} The title node to use. + */ +M.core.dock.fixTitleOrientation = function(title, text) { + var dock = M.core.dock.get(); + title = Y.one(title); + + if (dock.get('orientation') !== 'vertical') { + // If the dock isn't vertical don't adjust it! + title.setContent(text); + return title; + } + + if (Y.UA.ie > 0 && Y.UA.ie < 8) { + // IE 6/7 can't rotate text so force ver + M.str.langconfig.thisdirectionvertical = 'ver'; + } + + var clockwise = false; + switch (M.str.langconfig.thisdirectionvertical) { + case 'ver': + // Stacked is easy + return title.setContent(text.split('').join('
    ')); + case 'ttb': + clockwise = true; + break; + case 'btt': + clockwise = false; + break; + } + + if (Y.UA.ie === 8) { + // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute. + title.setContent(text); + title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;'); + title.addClass('filterrotate'); + return title; + } + + // We need to fix a font-size - sorry theme designers. + var fontsize = '11px'; + var transform = (clockwise) ? 'rotate(90deg)' : 'rotate(270deg)'; + var test = Y.Node.create('

    '+text+'

    '); + BODY.insert(test, 0); + var width = test.one('span').get('offsetWidth') * 1.2; + var height = test.one('span').get('offsetHeight'); + test.remove(); + + title.setContent(text); + title.addClass('css3transform'); + + // Move the title into position + title.setStyles({ + 'margin' : '0', + 'padding' : '0', + 'position' : 'relative', + 'fontSize' : fontsize, + 'width' : width, + 'top' : width/2 + }); + + // Positioning is different when in RTL mode. + if (right_to_left()) { + title.setStyle('left', width/2 - height); + } else { + title.setStyle('right', width/2 - height); + } + + // Rotate the text + title.setStyles({ + 'transform' : transform, + '-ms-transform' : transform, + '-moz-transform' : transform, + '-webkit-transform' : transform, + '-o-transform' : transform + }); + + var container = Y.Node.create('
    '); + container.append(title); + container.setStyle('height', width + (width / 4)); + container.setStyle('position', 'relative'); + return container; +}; + +/** + * Informs the dock that the content of the block has changed. + * This should be called by the blocks JS code if its content has been updated dynamically. + * This method ensure the dock resizes if need be. + * + * @static + * @method notifyBlockChange + * @param {Number} instanceid + * @return void + */ +M.core.dock.notifyBlockChange = function(instanceid) { + if (this._dock !== null) { + var dock = M.core.dock.get(), + activeitem = dock.getActiveItem(); + if (activeitem && activeitem.get('blockinstanceid') === parseInt(instanceid, 10)) { + dock.resizePanelIfRequired(); + } + } +}; + +/** + * The Dock. + * + * @namespace M.core.dock + * @class Dock + * @constructor + * @extends Y.Base + * @uses Y.EventTarget + */ +var DOCK = function() { + DOCK.superclass.constructor.apply(this, arguments); +}; +DOCK.prototype = { + /** + * Tab height manager used to ensure tabs are always visible. + * @protected + * @property tabheightmanager + * @type TABHEIGHTMANAGER + */ + tabheightmanager : null, + /** + * Will be an eventtype if there is an eventype to prevent. + * @protected + * @property preventevent + * @type String + */ + preventevent : null, + /** + * Will be an object if there is a delayed event in effect. + * @protected + * @property delayedevent + * @type {Object} + */ + delayedevent : null, + /** + * An array of currently docked items. + * @protected + * @property dockeditems + * @type Array + */ + dockeditems : [], + /** + * Set to true once the dock has been drawn. + * @protected + * @property dockdrawn + * @type Boolean + */ + dockdrawn : false, + /** + * The number of blocks that are currently docked. + * @protected + * @property count + * @type Number + */ + count : 0, + /** + * The total number of blocks that have been docked. + * @protected + * @property totalcount + * @type Number + */ + totalcount : 0, + /** + * A hidden node used as a holding area for DOM objects used by blocks that have been docked. + * @protected + * @property holdingareanode + * @type Node + */ + holdingareanode : null, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + Y.log('Dock initialising', 'note', LOGNS); + + // Publish the events the dock has + /** + * Fired when the dock first starts initialising. + * @event dock:starting + */ + this.publish('dock:starting', {prefix: 'dock',broadcast: 2,emitFacade: true, fireOnce:true}); + /** + * Fired after the dock is initialised for the first time. + * @event dock:initialised + */ + this.publish('dock:initialised', {prefix: 'dock',broadcast: 2,emitFacade: true, fireOnce:true}); + /** + * Fired before the dock structure and content is first created. + * @event dock:beforedraw + */ + this.publish('dock:beforedraw', {prefix:'dock', fireOnce:true}); + /** + * Fired before the dock is changed from hidden to visible. + * @event dock:beforeshow + */ + this.publish('dock:beforeshow', {prefix:'dock'}); + /** + * Fires after the dock has been changed from hidden to visible. + * @event dock:shown + */ + this.publish('dock:shown', {prefix:'dock'}); + /** + * Fired after the dock has been changed from visible to hidden. + * @event dock:hidden + */ + this.publish('dock:hidden', {prefix:'dock'}); + /** + * Fires when an item is added to the dock. + * @event dock:itemadded + */ + this.publish('dock:itemadded', {prefix:'dock'}); + /** + * Fires when an item is removed from the dock. + * @event dock:itemremoved + */ + this.publish('dock:itemremoved', {prefix:'dock'}); + /** + * Fires when a block is added or removed from the dock. + * This happens after the itemadded and itemremoved events have been called. + * @event dock:itemschanged + */ + this.publish('dock:itemschanged', {prefix:'dock'}); + /** + * Fires once when the docks panel is first initialised. + * @event dock:panelgenerated + */ + this.publish('dock:panelgenerated', {prefix:'dock', fireOnce:true}); + /** + * Fires when the dock panel is about to be resized. + * @event dock:panelresizestart + */ + this.publish('dock:panelresizestart', {prefix:'dock'}); + /** + * Fires after the dock panel has been resized. + * @event dock:resizepanelcomplete + */ + this.publish('dock:resizepanelcomplete', {prefix:'dock'}); + + // Apply theme customisations here before we do any real work. + this._applyThemeCustomisation(); + // Inform everyone we are now about to initialise. + this.fire('dock:starting'); + this._ensureDockDrawn(); + // Inform everyone the dock has been initialised + this.fire('dock:initialised'); + }, + /** + * Ensures that the dock has been drawn. + * @private + * @method _ensureDockDrawn + * @return {Boolean} + */ + _ensureDockDrawn : function() { + if (this.dockdrawn === true) { + return true; + } + var dock = this._initialiseDockNode(); + if (Y.UA.ie > 0 && Y.UA.ie < 7) { + // Adjust for IE 6 (can't handle fixed pos) + dock.setStyle('height', dock.get('winHeight')+'px'); + } + + this.fire('dock:beforedraw'); + + this._initialiseDockControls(); + + this.tabheightmanager = new TABHEIGHTMANAGER({dock:this}); + + var clickargs = {cssselector:'.'+CSS.dockedtitle, delay:0}; + var mouseenterargs = {cssselector:'.'+CSS.dockedtitle, delay:0.5, iscontained:true, preventevent:'click', preventdelay:3}; + + // Attach the required event listeners + // We use delegate here as that way a handful of events are created for the dock + // and all items rather than the same number for the dock AND every item individually + Y.delegate('click', this.handleEvent, this.get('dockNode'), '.'+CSS.dockedtitle, this, clickargs); + Y.delegate('mouseenter', this.handleEvent, this.get('dockNode'), '.'+CSS.dockedtitle, this, mouseenterargs); + this.get('dockNode').on('mouseleave', this.handleEvent, this, {cssselector:'#dock', delay:0.5, iscontained:false}); + + Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this); + Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this); + Y.delegate('dock:actionkey', this.handleDockedItemEvent, this.get('dockNode'), '.'+CSS.dockeditem, this); + + BODY.on('click', this.handleEvent, this, {cssselector:'body', delay:0}); + this.on('dock:itemschanged', this.resizeBlockSpace, this); + this.on('dock:itemschanged', this.checkDockVisibility, this); + this.on('dock:itemschanged', this.resetFirstItem, this); + this.dockdrawn = true; + return true; + }, + /** + * Handles an actionkey event on the dock. + * @param {EventFacade} e + * @method handleDockedItemEvent + * @return {Boolean} + */ + handleDockedItemEvent : function(e) { + if (e.type !== 'dock:actionkey') { + return false; + } + var target = e.target, + dockeditem = '.'+CSS.dockeditem; + if (!target.test(dockeditem)) { + target = target.ancestor(dockeditem); + } + if (!target) { + return false; + } + e.halt(); + var id = target.getAttribute('rel'); + this.dockeditems[id].toggle(e.action); + }, + /** + * Call the theme customisation method "customise_dock_for_theme" if it exists. + * @private + * @method _applyThemeCustomisation + */ + _applyThemeCustomisation : function() { + // Check if there is a customisation function + if (typeof(customise_dock_for_theme) === 'function') { + // First up pre the legacy object. + M.core_dock = this; + M.core_dock.cfg = { + buffer : null, + orientation : null, + position : null, + spacebeforefirstitem : null, + removeallicon : null + }; + M.core_dock.css = { + dock : null, + dockspacer : null, + controls : null, + body : null, + buttonscontainer : null, + dockeditem : null, + dockeditemcontainer : null, + dockedtitle : null, + activeitem : null + }; + try { + // Run the customisation function + customise_dock_for_theme(this); + } catch (exception) { + // Do nothing at the moment. + Y.log('Exception while attempting to apply theme customisations.', 'error', LOGNS); + } + // Now to work out what they did. + var key, value, + warned = false, + cfgmap = { + buffer : 'bufferPanel', + orientation : 'orientation', + position : 'position', + spacebeforefirstitem : 'bufferBeforeFirstItem', + removeallicon : 'undockAllIconUrl' + }; + // Check for and apply any legacy configuration. + for (key in M.core_dock.cfg) { + value = M.core_dock.cfg[key]; + if (value === null) { + continue; + } + if (!warned) { + Y.log('Warning: customise_dock_for_theme has changed. Please update your code.', 'warn', LOGNS); + warned = true; + } + // Damn, the've set something. + Y.log('Note for customise_dock_for_theme code: M.core_dock.cfg.'+key+' is now dock.set(\''+key+'\', value)', 'info', LOGNS); + this.set(cfgmap[key], value); + } + // Check for and apply any legacy CSS changes.. + for (key in M.core_dock.css) { + value = M.core_dock.css[key]; + if (value === null) { + continue; + } + if (!warned) { + Y.log('Warning: customise_dock_for_theme has changed. Please update your code.', 'warn', LOGNS); + warned = true; + } + // Damn, they've set something. + Y.log('Note for customise_dock_for_theme code: M.core_dock.css.'+key+' is now CSS.'+key+' = value', 'info', LOGNS); + CSS[key] = value; + } + } + }, + /** + * Initialises the dock node, creating it and its content if required. + * + * @private + * @method _initialiseDockNode + * @return {Node} The dockNode + */ + _initialiseDockNode : function() { + var dock = this.get('dockNode'), + positionorientationclass = CSS.dock+'_'+this.get('position')+'_'+this.get('orientation'), + holdingarea = Y.Node.create('
    ').setStyles({display:'none'}), + buttons = this.get('buttonsNode'), + container = this.get('itemContainerNode'); + + if (!dock) { + dock = Y.one('#'+CSS.dock); + } + if (!dock) { + dock = Y.Node.create('
    '); + BODY.append(dock); + } + dock.setAttribute('role', 'menubar').addClass(positionorientationclass); + if (Y.all(SELECTOR.dockonload).size() === 0) { + // Nothing on the dock... hide it using CSS + dock.addClass('nothingdocked'); + } else { + positionorientationclass = CSS.body+'_'+this.get('position')+'_'+this.get('orientation'); + BODY.addClass(CSS.body).addClass(); + } + + if (!buttons) { + buttons = dock.one('.'+CSS.buttonscontainer); + } + if (!buttons) { + buttons = Y.Node.create('
    '); + dock.append(buttons); + } + + if (!container) { + container = dock.one('.'+CSS.dockeditemcontainer); + } + if (!container) { + container = Y.Node.create('
    '); + buttons.append(container); + } + + BODY.append(holdingarea); + this.holdingareanode = holdingarea; + + this.set('dockNode', dock); + this.set('buttonsNode', buttons); + this.set('itemContainerNode', container); + + return dock; + }, + /** + * Initialises the dock controls. + * + * @private + * @method _initialiseDockControls + */ + _initialiseDockControls : function() { + // Add a removeall button + // Must set the image src seperatly of we get an error with XML strict headers + + var removeall = Y.Node.create(''+M.util.get_string('undockall', 'block')+''); + removeall.setAttribute('src',this.get('undockAllIconUrl')); + removeall.on('removeall|click', this.removeAll, this); + removeall.on('dock:actionkey', this.removeAll, this, {actions:{enter:true}}); + this.get('buttonsNode').append(Y.Node.create('
    ').append(removeall)); + }, + /** + * Returns the dock panel. Initialising it if it hasn't already been initialised. + * @method getPanel + * @return {DOCKPANEL} + */ + getPanel : function() { + var panel = this.get('panel'); + if (!panel) { + panel = new DOCKPANEL({dock:this}); + panel.on('panel:visiblechange', this.resize, this); + Y.on('windowresize', this.resize, this); + // Initialise the dockpanel .. should only happen once + this.set('panel', panel); + this.fire('dock:panelgenerated'); + } + return panel; + }, + /** + * Resizes the dock panel if required. + * @method resizePanelIfRequired + */ + resizePanelIfRequired : function() { + this.resize(); + var panel = this.get('panel'); + if (panel) { + panel.correctWidth(); + } + }, + /** + * Handles a dock event sending it to the right place. + * + * @method handleEvent + * @param {EventFacade} e + * @param {Object} options + * @return {Boolean} + */ + handleEvent : function(e, options) { + var item = this.getActiveItem(); + if (options.cssselector === 'body') { + if (!this.get('dockNode').contains(e.target)) { + if (item) { + item.hide(); + } + } + } else { + var target; + if (e.target.test(options.cssselector)) { + target = e.target; + } else { + target = e.target.ancestor(options.cssselector); + } + if (!target) { + return true; + } + if (this.preventevent !== null && e.type === this.preventevent) { + return true; + } + if (options.preventevent) { + this.preventevent = options.preventevent; + if (options.preventdelay) { + var self = this; + setTimeout(function(){ + self.preventevent = null; + }, options.preventdelay * 1000); + } + } + if (this.delayedevent && this.delayedevent.timeout) { + clearTimeout(this.delayedevent.timeout); + this.delayedevent.event.detach(); + this.delayedevent = null; + } + if (options.delay > 0) { + return this.delayEvent(e, options, target); + } + var targetid = target.get('id'); + var regex = /^dock_item_(\d+)_title$/; + if (targetid.match(regex)) { + item = this.dockeditems[targetid.replace(regex, '$1')]; + if (item.active) { + item.hide(); + } else { + item.show(); + } + } else if (item) { + item.hide(); + } + } + return true; + }, + /** + * Delays an event. + * + * @method delayEvent + * @param {EventFacade} event + * @param {Object} options + * @param {Node} target + * @return {Boolean} + */ + delayEvent : function(event, options, target) { + var self = this; + self.delayedevent = (function(){ + return { + target : target, + event : BODY.on('mousemove', function(e){ + self.delayedevent.target = e.target; + }), + timeout : null + }; + })(self); + self.delayedevent.timeout = setTimeout(function(){ + self.delayedevent.timeout = null; + self.delayedevent.event.detach(); + if (options.iscontained === self.get('dockNode').contains(self.delayedevent.target)) { + self.handleEvent(event, {cssselector:options.cssselector, delay:0, iscontained:options.iscontained}); + } + }, options.delay*1000); + return true; + }, + /** + * Resizes block spaces. + * @method resizeBlockSpace + */ + resizeBlockSpace : function() { + if (Y.all(SELECTOR.dockonload).size() > 0) { + // Do not resize during initial load + return; + } + var blockregions = [], + populatedblockregions = 0, + allnewregions = true, + showregions = false; + // First look for understood regions. + Y.all(SELECTOR.blockregion).each(function(){ + var regionname = region.getData('blockregion'); + if (region.all('.block').size() > 0) { + populatedblockregions++; + BODY.addClass('used-region-'+regionname); + BODY.removeClass('empty-region-'+regionname); + } else { + BODY.addClass('empty-region-'+regionname); + BODY.removeClass('used-region-'+regionname); + } + }); + // Next check for legacy regions. + Y.all('.block-region').each(function(region){ + if (region.test(SELECTOR.blockregion)) { + // This is a new region, we've already processed it. + return; + } + var hasblocks = (region.all('.block').size() > 0); + if (hasblocks) { + populatedblockregions++; + } + allnewregions = false; + blockregions[region.get('id')] = { + hasblocks : hasblocks, + bodyclass : region.get('id').replace(/^region\-/, 'side-')+'-only' + }; + }); + if (BODY.hasClass('blocks-moving')) { + // open up blocks during blocks positioning + showregions = true; + } + if (populatedblockregions === 0 && showregions === false) { + BODY.addClass(CSS.contentonly); + } else { + BODY.removeClass(CSS.contentonly); + } + + if (!allnewregions) { + var i = null; + if (populatedblockregions === 0 && showregions === false) { + for (i in blockregions) { + BODY.removeClass(blockregions[i].bodyclass); + } + } else if (populatedblockregions === 1 && showregions === false) { + for (i in blockregions) { + if (!blockregions[i].hasblocks) { + BODY.removeClass(blockregions[i].bodyclass); + } else { + BODY.addClass(blockregions[i].bodyclass); + } + } + } else { + for (i in blockregions) { + BODY.removeClass(blockregions[i].bodyclass); + } + } + } + }, + /** + * Adds an item to the dock. + * @method add + * @param {DOCKEDITEM} item + */ + add : function(item) { + // Set the dockitem id to the total count and then increment it. + item.set('id', this.totalcount); + Y.log('Adding block '+item._getLogDescription()+' to the dock.', 'info', LOGNS); + this.count++; + this.totalcount++; + this.dockeditems[item.get('id')] = item; + this.dockeditems[item.get('id')].draw(); + this.fire('dock:itemadded', item); + this.fire('dock:itemschanged', item); + }, + /** + * Appends an item to the dock (putting it in the item container. + * @method append + * @param {Node} docknode + */ + append : function(docknode) { + this.get('itemContainerNode').append(docknode); + }, + /** + * Handles events that require a docked block to be returned to the page./ + * @method handleReturnToBlock + * @param {EventFacade} e + */ + handleReturnToBlock : function(e) { + e.halt(); + this.remove(this.getActiveItem().get('id')); + }, + /** + * Removes a docked item from the dock. + * @method remove + * @param {Number} id The docked item id. + * @return {Boolean} + */ + remove : function(id) { + if (!this.dockeditems[id]) { + return false; + } + Y.log('Removing block '+this.dockeditems[id]._getLogDescription()+' from the dock.', 'info', LOGNS); + this.dockeditems[id].remove(); + delete this.dockeditems[id]; + this.count--; + this.fire('dock:itemremoved', id); + this.fire('dock:itemschanged', id); + return true; + }, + /** + * Ensures the the first item in the dock has the correct class. + * @method resetFirstItem + */ + resetFirstItem : function() { + this.get('dockNode').all('.'+CSS.dockeditem+'.firstdockitem').removeClass('firstdockitem'); + if (this.get('dockNode').one('.'+CSS.dockeditem)) { + this.get('dockNode').one('.'+CSS.dockeditem).addClass('firstdockitem'); + } + }, + /** + * Removes all docked blocks returning them to the page. + * @method removeAll + * @return {Boolean} + */ + removeAll : function() { + Y.log('Undocking all '+this.dockeditems.length+' blocks', 'note', LOGNS); + for (var i in this.dockeditems) { + this.remove(i); + } + return true; + }, + /** + * Hides the active item. + * @method hideActive + */ + hideActive : function() { + var item = this.getActiveItem(); + if (item) { + item.hide(); + } + }, + /** + * Checks wether the dock should be shown or hidden + * @method checkDockVisibility + */ + checkDockVisibility : function() { + var bodyclass = CSS.body+'_'+this.get('position')+'_'+this.get('orientation'); + if (!this.count) { + this.get('dockNode').addClass('nothingdocked'); + BODY.removeClass(CSS.body).removeClass(); + this.fire('dock:hidden'); + } else { + this.fire('dock:beforeshow'); + this.get('dockNode').removeClass('nothingdocked'); + BODY.addClass(CSS.body).addClass(bodyclass); + this.fire('dock:shown'); + } + }, + /** + * This function checks the size and position of the panel and moves/resizes if + * required to keep it within the bounds of the window. + * @method resize + * @return {Boolean} + */ + resize : function() { + var panel = this.getPanel(); + var item = this.getActiveItem(); + if (!panel.get('visible') || !item) { + return true; + } + + this.fire('dock:panelresizestart'); + if (this.get('orientation') === 'vertical') { + var buffer = this.get('bufferPanel'); + var screenheight = parseInt(BODY.get('winHeight'), 10)-(buffer*2); + var docky = this.get('dockNode').getY(); + var titletop = item.get('dockTitleNode').getY()-docky-buffer; + var containery = this.get('itemContainerNode').getY(); + var containerheight = containery-docky+this.get('buttonsNode').get('offsetHeight'); + var scrolltop = panel.get('bodyNode').get('scrollTop'); + panel.get('bodyNode').setStyle('height', 'auto'); + panel.get('node').removeClass('oversized_content'); + var panelheight = panel.get('node').get('offsetHeight'); + + if (Y.UA.ie > 0 && Y.UA.ie < 7) { + panel.setTop(item.get('dockTitleNode').getY()); + } else if (panelheight > screenheight) { + panel.setTop(buffer-containerheight); + panel.get('bodyNode').setStyle('height', (screenheight-panel.get('headerNode').get('offsetHeight'))+'px'); + panel.get('node').addClass('oversized_content'); + } else if (panelheight > (screenheight-(titletop-buffer))) { + var difference = panelheight - (screenheight-titletop); + panel.setTop(titletop-containerheight-difference+buffer); + } else { + panel.setTop(titletop-containerheight+buffer); + } + + if (scrolltop) { + panel.get('bodyNode').set('scrollTop', scrolltop); + } + } + + if (this.get('position') === 'right') { + panel.get('node').setStyle('left', -panel.get('offsetWidth')+'px'); + + } else if (this.get('position') === 'top') { + var dockx = this.get('dockNode').getX(); + var titleleft = item.get('dockTitleNode').getX()-dockx; + panel.get('node').setStyle('left', titleleft+'px'); + } + + this.fire('dock:resizepanelcomplete'); + return true; + }, + /** + * Returns the currently active dock item or false + * @method getActiveItem + * @return {DOCKEDITEM} + */ + getActiveItem : function() { + for (var i in this.dockeditems) { + if (this.dockeditems[i].active) { + return this.dockeditems[i]; + } + } + return false; + }, + /** + * Adds an item to the holding area. + * @method addToHoldingArea + * @param {Node} node + */ + addToHoldingArea : function(node) { + this.holdingareanode.append(node); + } +}; + +Y.extend(DOCK, Y.Base, DOCK.prototype, { + NAME : 'moodle-core-dock', + ATTRS : { + /** + * The dock itself. #dock. + * @attribute dockNode + * @type Node + * @writeOnce + */ + dockNode : { + writeOnce : true + }, + /** + * The docks panel. + * @attribute panel + * @type DOCKPANEL + * @writeOnce + */ + panel : { + writeOnce : true + }, + /** + * A container within the dock used for buttons. + * @attribute buttonsNode + * @type Node + * @writeOnce + */ + buttonsNode : { + writeOnce : true + }, + /** + * A container within the dock used for docked blocks. + * @attribute itemContainerNode + * @type Node + * @writeOnce + */ + itemContainerNode : { + writeOnce : true + }, + + /** + * Buffer used when containing a panel. + * @attribute bufferPanel + * @type Number + * @default 10 + */ + bufferPanel : { + value : 10, + validator : Y.Lang.isNumber + }, + + /** + * Position of the dock. + * @attribute position + * @type String + * @default left + */ + position : { + value : 'left', + validator : Y.Lang.isString + }, + + /** + * vertical || horizontal determines if we change the title + * @attribute orientation + * @type String + * @default vertical + */ + orientation : { + value : 'vertical', + validator : Y.Lang.isString, + setter : function(value) { + if (value.match(/^vertical$/i)) { + return 'vertical'; + } + return 'horizontal'; + } + }, + + /** + * Space between the top of the dock and the first item. + * @attribute bufferBeforeFirstItem + * @type Number + * @default 10 + */ + bufferBeforeFirstItem : { + value : 10, + validator : Y.Lang.isNumber + }, + + /** + * Icon URL for the icon to undock all blocks + * @attribute undockAllIconUrl + * @type String + * @default t/dock_to_block + */ + undockAllIconUrl : { + value : M.util.image_url('t/dock_to_block', 'moodle'), + validator : Y.Lang.isString + } + } +}); +Y.augment(DOCK, Y.EventTarget); +/** + * Dock JS. + * + * This file contains the panel class used by the dock to display the content of docked blocks. + * + * @module moodle-core-dock + */ + +/** + * Panel. + * + * @namespace M.core.dock + * @class Panel + * @constructor + * @extends Y.Base + * @uses Y.EventTarget + */ +var DOCKPANEL = function() { + DOCKPANEL.superclass.constructor.apply(this, arguments); +}; +DOCKPANEL.prototype = { + /** + * True once the panel has been created. + * @property created + * @protected + * @type {Boolean} + */ + created : false, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + Y.log('Panel initialising', 'note', LOGNS); + /** + * Fired before the panel is shown. + * @event dockpane::beforeshow + */ + this.publish('dockpanel:beforeshow', {prefix:'dockpanel'}); + /** + * Fired after the panel is shown. + * @event dockpanel:shown + */ + this.publish('dockpanel:shown', {prefix:'dockpanel'}); + /** + * Fired before the panel is hidden. + * @event dockpane::beforehide + */ + this.publish('dockpanel:beforehide', {prefix:'dockpanel'}); + /** + * Fired after the panel is hidden. + * @event dockpanel:hidden + */ + this.publish('dockpanel:hidden', {prefix:'dockpanel'}); + /** + * Fired when ever the dock panel is either hidden or shown. + * Always fired after the shown or hidden events. + * @event dockpanel:visiblechange + */ + this.publish('dockpanel:visiblechange', {prefix:'dockpanel'}); + }, + /** + * Creates the Panel if it has not already been created. + * @method create + * @return {Boolean} + */ + create : function() { + if (this.created) { + return true; + } + this.created = true; + var dock = this.get('dock'), + node = dock.get('dockNode'); + this.set('node', Y.Node.create('
    ')); + this.set('contentNode', Y.Node.create('
    ')); + this.set('headerNode', Y.Node.create('
    ')); + this.set('bodyNode', Y.Node.create('
    ')); + node.append( + this.get('node').append(this.get('contentNode').append(this.get('headerNode')).append(this.get('bodyNode'))) + ); + }, + /** + * Displays the panel. + * @method show + */ + show : function() { + this.create(); + this.fire('dockpanel:beforeshow'); + this.set('visible', true); + this.get('node').removeClass('dockitempanel_hidden'); + this.fire('dockpanel:shown'); + this.fire('dockpanel:visiblechange'); + }, + /** + * Hides the panel + * @method hide + */ + hide : function() { + this.fire('dockpanel:beforehide'); + this.set('visible', false); + this.get('node').addClass('dockitempanel_hidden'); + this.fire('dockpanel:hidden'); + this.fire('dockpanel:visiblechange'); + }, + /** + * Sets the panel header. + * @method setHeader + * @param {Node|String} content + */ + setHeader : function(content) { + this.create(); + var header = this.get('headerNode'); + header.setContent(content); + if (arguments.length > 1) { + for (var i=1;i < arguments.length;i++) { + header.append(arguments[i]); + } + } + }, + /** + * Sets the panel body. + * @method setBody + * @param {Node|String} content + */ + setBody : function(content) { + this.create(); + this.get('bodyNode').setContent(content); + }, + /** + * Sets the new top mark of the panel. + * + * @method setTop + * @param {Number} newtop + */ + setTop : function(newtop) { + if (Y.UA.ie > 0 && Y.UA.ie < 7) { + this.get('node').setY(newtop); + } else { + this.get('node').setStyle('top', newtop.toString()+'px'); + } + }, + /** + * Corrects the width of the panel. + * @method correctWidth + */ + correctWidth : function() { + var bodyNode = this.get('bodyNode'), + // Width of content. + width = bodyNode.get('clientWidth'), + // Scrollable width of content. + scroll = bodyNode.get('scrollWidth'), + // Width of content container with overflow. + offsetWidth = bodyNode.get('offsetWidth'), + // The new width - defaults to the current width. + newWidth = width, + // The max width (80% of screen). + maxWidth = Math.round(bodyNode.get('winWidth') * 0.8); + + // If the scrollable width is more than the visible width + if (scroll > width) { + // Content width + // + the difference + // + any rendering difference (borders, padding) + // + 10px to make it look nice. + newWidth = width + (scroll - width) + ((offsetWidth - width)*2) + 10; + } + + // Make sure its not more then the maxwidth + if (newWidth > maxWidth) { + newWidth = maxWidth; + } + + // Set the new width if its more than the old width. + if (newWidth > offsetWidth) { + this.get('node').setStyle('width', newWidth+'px'); + } + } +}; +Y.extend(DOCKPANEL, Y.Base, DOCKPANEL.prototype, { + NAME : 'moodle-core-dock-panel', + ATTRS : { + /** + * The dock itself. + * @attribute dock + * @type DOCK + * @writeonce + */ + dock : { + writeOnce : 'initOnly' + }, + /** + * The node that contains the whole panel. + * @attribute node + * @type Node + */ + node : { + value : null + }, + /** + * The node that contains the header, body and footer. + * @attribute contentNode + * @type Node + */ + contentNode : { + value : null + }, + /** + * The node that contains the header + * @attribute headerNode + * @type Node + */ + headerNode : { + value : null + }, + /** + * The node that contains the body + * @attribute bodyNode + * @type Node + */ + bodyNode : { + value : null + }, + /** + * True if the panel is currently visible. + * @attribute visible + * @type Boolean + */ + visible : { + value : false + } + } +}); +Y.augment(DOCKPANEL, Y.EventTarget); +/** + * Dock JS. + * + * This file contains the tab height manager. + * The tab height manager is responsible for ensure all tabs are visible all the time. + * + * @module moodle-core-dock + */ + +/** + * Tab height manager. + * + * @namespace M.core.dock + * @class TabHeightManager + * @constructor + * @extends Y.Base + */ +var TABHEIGHTMANAGER = function() { + TABHEIGHTMANAGER.superclass.constructor.apply(this, arguments); +}; +TABHEIGHTMANAGER.prototype = { + /** + * Initialises the dock sizer which then attaches itself to the required + * events in order to monitor the dock + * @method initializer + */ + initializer : function() { + var dock = this.get('dock'); + dock.on('dock:itemschanged', this.checkSizing, this); + Y.on('windowresize', this.checkSizing, this); + }, + /** + * Check if the size dock items needs to be adjusted + * @method checkSizing + */ + checkSizing : function() { + var dock = this.get('dock'), + node = dock.get('dockNode'), + items = dock.dockeditems, + possibleheight = node.get('offsetHeight') - node.one('.controls').get('offsetHeight') - (dock.get('bufferPanel')*3) - (items.length*2), + totalheight = 0, + id, dockedtitle; + if (items.length > 0) { + for (id in items) { + dockedtitle = Y.one(items[id].get('title')).ancestor('.'+CSS.dockedtitle); + if (dockedtitle) { + if (this.get('enabled')) { + dockedtitle.setStyle('height', 'auto'); + } + totalheight += dockedtitle.get('offsetHeight') || 0; + } + } + if (totalheight > possibleheight) { + this.enable(possibleheight); + } + } + }, + /** + * Enables the dock sizer and resizes where required. + * @method enable + * @param {Number} possibleheight + */ + enable : function(possibleheight) { + var dock = this.get('dock'), + items = dock.dockeditems, + count = dock.count, + runningcount = 0, + usedheight = 0, + id, itemtitle, itemheight, offsetheight; + Y.log('Enabling the dock tab sizer.', 'note', LOGNS); + this.set('enabled', true); + for (id in items) { + itemtitle = Y.one(items[id].get('title')).ancestor('.'+CSS.dockedtitle); + if (!itemtitle) { + continue; + } + itemheight = Math.floor((possibleheight-usedheight) / (count - runningcount)); + offsetheight = itemtitle.get('offsetHeight'); + itemtitle.setStyle('overflow', 'hidden'); + if (offsetheight > itemheight) { + itemtitle.setStyle('height', itemheight+'px'); + usedheight += itemheight; + } else { + usedheight += offsetheight; + } + runningcount++; + } + } +}; +Y.extend(TABHEIGHTMANAGER, Y.Base, TABHEIGHTMANAGER.prototype, { + NAME : 'moodle-core-tabheightmanager', + ATTRS : { + /** + * The dock. + * @attribute dock + * @type DOCK + * @writeOnce + */ + dock : { + writeOnce : 'initOnly' + }, + /** + * True if the item_sizer is being used, false otherwise. + * @attribute enabled + * @type Bool + */ + enabled : { + value : false + } + } +}); +/** + * Dock JS. + * + * This file contains the action key event definition that is used for accessibility handling within the Dock. + * + * @module moodle-core-dock + */ + +/** + * A 'dock:actionkey' Event. + * The event consists of the left arrow, right arrow, enter and space keys. + * More keys can be mapped to action meanings. + * actions: collapse , expand, toggle, enter. + * + * This event is subscribed to by dockitems. + * The on() method to subscribe allows specifying the desired trigger actions as JSON. + * + * This event can also be delegated if needed. + * + * @namespace M.core.dock + * @class ActionKey + */ +Y.Event.define("dock:actionkey", { + // Webkit and IE repeat keydown when you hold down arrow keys. + // Opera links keypress to page scroll; others keydown. + // Firefox prevents page scroll via preventDefault() on either + // keydown or keypress. + _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress', + + /** + * The keys to trigger on. + * @method _keys + */ + _keys: { + //arrows + '37': 'collapse', + '39': 'expand', + //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings) + '32': 'toggle', + '13': 'enter' + }, + + /** + * Handles key events + * @method _keyHandler + * @param {EventFacade} e + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {Object} args + */ + _keyHandler: function (e, notifier, args) { + var actObj; + if (!args.actions) { + actObj = {collapse:true, expand:true, toggle:true, enter:true}; + } else { + actObj = args.actions; + } + if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) { + e.action = this._keys[e.keyCode]; + notifier.fire(e); + } + }, + + /** + * Subscribes to events. + * @method on + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + */ + on: function (node, sub, notifier) { + // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). + if (sub.args === null) { + //no actions given + sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false}); + } else { + sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]); + } + }, + + /** + * Detaches an event listener + * @method detach + */ + detach: function (node, sub, notifier) { + //detach our _detacher handle of the subscription made in on() + sub._detacher.detach(); + }, + + /** + * Creates a delegated event listener. + * @method delegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ + delegate: function (node, sub, notifier, filter) { + // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). + if (sub.args === null) { + //no actions given + sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false}); + } else { + sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]); + } + }, + + /** + * Detaches a delegated event listener. + * @method detachDelegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ + detachDelegate: function (node, sub, notifier, filter) { + sub._delegateDetacher.detach(); + } +}); +/** + * Dock JS. + * + * This file contains the block class used to manage blocks (both docked and not) for the dock. + * + * @module moodle-core-dock + */ + +/** + * Block. + * + * @namespace M.core.dock + * @class Block + * @constructor + * @extends Y.Base + */ +var BLOCK = function() { + BLOCK.superclass.constructor.apply(this, arguments); +}; +BLOCK.prototype = { + /** + * A content place holder used when the block has been docked. + * @property contentplaceholder + * @protected + * @type Node + */ + contentplaceholder : null, + /** + * The skip link associated with this block. + * @property contentskipanchor + * @protected + * @type Node + */ + contentskipanchor : null, + /** + * The cached content node for the actual block + * @property cachedcontentnode + * @protected + * @type Node + */ + cachedcontentnode : null, + /** + * If true the user preference isn't updated + * @property skipsetposition + * @protected + * @type Boolean + */ + skipsetposition : true, + /** + * The dock item associated with this block + * @property dockitem + * @protected + * @type DOCKITEM + */ + dockitem : null, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + var node = Y.one('#inst'+this.get('id')); + if (!node) { + return false; + } + + Y.log('Initialised block with instance id:'+this.get('id'), 'note', LOGNS); + + M.core.dock.ensureMoveToIconExists(node); + + // Move the block straight to the dock if required + if (node.hasClass(CSS.dockonload)) { + node.removeClass(CSS.dockonload); + var commands = node.one('.header .title .commands'); + if (!commands) { + commands = Y.Node.create('
    '); + if (node.one('.header .title')) { + node.one('.header .title').append(commands); + } + } + this.moveToDock(null, commands); + } + this.skipsetposition = false; + return true; + }, + /** + * Returns the class associated with this block. + * @method _getBlockClass + * @private + * @param {Node} node + * @return String + */ + _getBlockClass : function(node) { + var classes = node.getAttribute('className').toString(), + regex = /(^|\s)(block_[a-zA-Z0-9_]+)(\s|$)/, + matches = regex.exec(classes); + if (matches) { + return matches[2]; + } + return matches; + }, + + /** + * This function is reponsible for moving a block from the page structure onto the dock. + * @method moveToDock + * @param {EventFacade} e + */ + moveToDock : function(e) { + if (e) { + e.halt(true); + } + + var dock = M.core.dock.get(), + id = this.get('id'), + blockcontent = Y.one('#inst'+id).one('.content'); + + if (!blockcontent) { + return; + } + + Y.log('Moving block to the dock:'+this.get('id'), 'note', LOGNS); + + var icon = (right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block', + char = (location.href.match(/\?/)) ? '&' : '?', + blocktitle, + blockcommands, + movetoimg, + moveto; + + this.recordBlockState(); + + blocktitle = this.cachedcontentnode.one('.title h2').cloneNode(true); + blockcommands = this.cachedcontentnode.one('.title .commands').cloneNode(true); + + // Must set the image src seperatly of we get an error with XML strict headers + movetoimg = Y.Node.create(''+Y.Escape.html(M.str.block.undockitem)+''); + movetoimg.setAttribute('src', M.util.image_url(icon, 'moodle')); + moveto = Y.Node.create('').append(movetoimg); + moveto.set('href', location.href + char + 'dock='+id); + blockcommands.append(moveto); + + // Create a new dock item for the block + this.dockitem = new DOCKEDITEM({ + block : this, + dock : dock, + blockinstanceid : id, + title : blocktitle, + contents : blockcontent, + commands : blockcommands, + blockclass : this._getBlockClass(Y.one('#inst'+id)) + }); + // Register an event so that when it is removed we can put it back as a block + dock.add(this.dockitem); + + if (!this.skipsetposition) { + // save the users preference + M.util.set_user_preference('docked_block_instance_'+id, 1); + } + + this.set('idDocked', true); + }, + /** + * Records the block state and adds it to the docks holding area. + * @method recordBlockState + */ + recordBlockState : function() { + var id = this.get('id'), + dock = M.core.dock.get(), + node = Y.one('#inst'+id), + skipanchor = node.previous(); + // Disable the skip anchor when docking + if (skipanchor.hasClass('skip-block')) { + this.contentskipanchor = skipanchor; + this.contentskipanchor.hide(); + } + this.cachedcontentnode = node; + this.contentplaceholder = Y.Node.create('
    '); + node.replace(this.contentplaceholder); + dock.addToHoldingArea(node); + node = null; + if (!this.cachedcontentnode.one('.title .commands')) { + this.cachedcontentnode.one('.title').append(Y.Node.create('
    ')); + } + }, + + /** + * This function removes a block from the dock and puts it back into the page structure. + * @method returnToBlock + * @return {Boolean} + */ + returnToBlock : function() { + var id = this.get('id'); + + Y.log('Moving block out of the dock:'+this.get('id'), 'note', LOGNS); + + // Enable the skip anchor when going back to block mode + if (this.contentskipanchor) { + this.contentskipanchor.show(); + } + + if (this.cachedcontentnode.one('.header')) { + this.cachedcontentnode.one('.header').insert(this.dockitem.get('contents'), 'after'); + } else { + this.cachedcontentnode.insert(this.dockitem.get('contents')); + } + + this.contentplaceholder.replace(this.cachedcontentnode); + this.cachedcontentnode = Y.one('#'+this.cachedcontentnode.get('id')); + + var commands = this.dockitem.get('commands'); + if (commands) { + commands.all('.hidepanelicon').remove(); + commands.all('.moveto').remove(); + commands.remove(); + } + this.cachedcontentnode.one('.title').append(commands); + this.cachedcontentnode = null; + M.util.set_user_preference('docked_block_instance_'+id, 0); + this.set('idDocked', false); + return true; + } +}; +Y.extend(BLOCK, Y.Base, BLOCK.prototype, { + NAME : 'moodle-core-dock-block', + ATTRS : { + /** + * The block instance ID + * @attribute id + * @writeOnce + * @type Number + */ + id : { + writeOnce : 'initOnly', + setter : function(value) { + return parseInt(value, 10); + } + }, + /** + * True if the block has been docked. + * @attribute isDocked + * @default false + * @type Boolean + */ + isDocked : { + value : false + } + } +}); +/** + * Dock JS. + * + * This file contains the docked item class. + * + * @module moodle-core-dock + */ + +/** + * Docked item. + * + * @namespace M.core.dock + * @class DockedItem + * @constructor + * @extends Y.Base + * @uses Y.EventTarget + */ +var DOCKEDITEM = function() { + DOCKEDITEM.superclass.constructor.apply(this, arguments); +}; +DOCKEDITEM.prototype = { + /** + * Set to true if this item is currently being displayed. + * @property active + * @protected + * @type Boolean + */ + active : false, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + var title = this.get('title'), + titlestring; + /** + * Fired before the docked item has been drawn. + * @event dockeditem:drawstart + */ + this.publish('dockeditem:drawstart', {prefix:'dockeditem'}); + /** + * Fired after the docked item has been drawn. + * @event dockeditem:drawcomplete + */ + this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'}); + /** + * Fired before the docked item is to be shown. + * @event dockeditem:showstart + */ + this.publish('dockeditem:showstart', {prefix:'dockeditem'}); + /** + * Fired after the docked item has been shown. + * @event dockeditem:showcomplete + */ + this.publish('dockeditem:showcomplete', {prefix:'dockeditem'}); + /** + * Fired before the docked item has been hidden. + * @event dockeditem:hidestart + */ + this.publish('dockeditem:hidestart', {prefix:'dockeditem'}); + /** + * Fired after the docked item has been hidden. + * @event dockeditem:hidecomplete + */ + this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'}); + /** + * Fired when the docked item is removed from the dock. + * @event dockeditem:itemremoved + */ + this.publish('dockeditem:itemremoved', {prefix:'dockeditem'}); + if (title) { + var type = title.get('nodeName'); + titlestring = title.cloneNode(true); + title = Y.Node.create('<'+type+'>'); + title = M.core.dock.fixTitleOrientation(title, titlestring.get('text')); + this.set('title', title); + this.set('titlestring', titlestring); + } + Y.log('Initialised dockeditem for block with title "'+this._getLogDescription(), 'note', LOGNS); + }, + /** + * This function draws the item on the dock. + * @method draw + * @return Boolean + */ + draw : function() { + var create = Y.Node.create, + dock = this.get('dock'), + count = dock.count, + docktitle, + dockitem, + closeicon, + closeiconimg, + id = this.get('id'); + + this.fire('dockeditem:drawstart'); + + docktitle = create(''); + docktitle.append(this.get('title')); + dockitem = create('
    '); + if (count === 1) { + dockitem.addClass('firstdockitem'); + } + dockitem.append(docktitle); + dock.append(dockitem); + + closeiconimg = create(''+M.str.block.hidepanel+''); + closeiconimg.setAttribute('src', M.util.image_url('t/dockclose', 'moodle')); + closeicon = create('').append(closeiconimg); + closeicon.on('forceclose|click', this.hide, this); + closeicon.on('dock:actionkey',this.hide, this, {actions:{enter:true,toggle:true}}); + this.get('commands').append(closeicon); + + this.set('dockTitleNode', docktitle); + this.set('dockItemNode', dockitem); + + this.fire('dockeditem:drawcomplete'); + return true; + }, + /** + * This function toggles makes the item active and shows it. + * @method show + * @return Boolean + */ + show : function() { + var dock = this.get('dock'), + panel = dock.getPanel(), + docktitle = this.get('dockTitleNode'); + + dock.hideActive(); + this.fire('dockeditem:showstart'); + Y.log('Showing '+this._getLogDescription(), 'info', LOGNS); + panel.setHeader(this.get('titlestring'), this.get('commands')); + panel.setBody(Y.Node.create('
    ').append(this.get('contents'))); + panel.show(); + panel.correctWidth(); + + this.active = true; + // Add active item class first up + docktitle.addClass(CSS.activeitem); + // Set aria-exapanded property to true. + docktitle.set('aria-expanded', "true"); + this.fire('dockeditem:showcomplete'); + dock.resize(); + return true; + }, + /** + * This function hides the item and makes it inactive. + * @method hide + */ + hide : function() { + this.fire('dockeditem:hidestart'); + Y.log('Hiding "'+this._getLogDescription(), 'info', LOGNS); + if (this.active) { + // No longer active + this.active = false; + // Hide the panel + this.get('dock').getPanel().hide(); + } + // Remove the active class + // Set aria-exapanded property to false + this.get('dockTitleNode').removeClass(CSS.activeitem).set('aria-expanded', "false"); + this.fire('dockeditem:hidecomplete'); + }, + /** + * A toggle between calling show and hide functions based on css.activeitem + * Applies rules to key press events (dock:actionkey) + * @method toggle + * @param {String} action + */ + toggle : function(action) { + var docktitle = this.get('dockTitleNode'); + if (docktitle.hasClass(CSS.activeitem) && action !== 'expand') { + this.hide(); + } else if (!docktitle.hasClass(CSS.activeitem) && action !== 'collapse') { + this.show(); + } + }, + /** + * This function removes the node and destroys it's bits. + * @method remove. + */ + remove : function () { + this.hide(); + // Return the block to its original position. + this.get('block').returnToBlock(); + // Remove the dock item node. + this.get('dockItemNode').remove(); + this.fire('dockeditem:itemremoved'); + }, + /** + * Returns the description of this item to use for log calls. + * @method _getLogDescription + * @private + * @return {String} + */ + _getLogDescription : function() { + return this.get('titlestring').get('innerHTML')+' ('+this.get('blockinstanceid')+')'; + } +}; +Y.extend(DOCKEDITEM, Y.Base, DOCKEDITEM.prototype, { + NAME : 'moodle-core-dock-dockeditem', + ATTRS : { + /** + * The block this docked item is associated with. + * @attribute block + * @type BLOCK + * @writeOnce + * @required + */ + block : { + writeOnce : 'initOnly' + }, + /** + * The dock itself. + * @attribute dock + * @type DOCK + * @writeOnce + * @required + */ + dock : { + writeOnce : 'initOnly' + }, + /** + * The docked item ID. This will be given by the dock. + * @attribute id + * @type Number + */ + id : {}, + /** + * Block instance id.Taken from the associated block. + * @attribute blockinstanceid + * @type Number + * @writeOnce + */ + blockinstanceid : { + writeOnce : 'initOnly', + setter : function(value) { + return parseInt(value, 10); + } + }, + /** + * The title nodeof the docked item. + * @attribute title + * @type Node + * @default null + */ + title : { + value : null + }, + /** + * The title string. + * @attribute titlestring + * @type String + */ + titlestring : { + value : null + }, + /** + * The contents of the docked item + * @attribute contents + * @type Node + * @writeOnce + * @required + */ + contents : { + writeOnce : 'initOnly' + }, + /** + * Commands associated with the block. + * @attribute commands + * @type Node + * @writeOnce + * @required + */ + commands : { + writeOnce : 'initOnly' + }, + /** + * The block class. + * @attribute blockclass + * @type String + * @writeOnce + * @required + */ + blockclass : { + writeOnce : 'initOnly' + }, + /** + * The title node for the docked block. + * @attribute dockTitleNode + * @type Node + */ + dockTitleNode : { + value : null + }, + /** + * The item node for the docked block. + * @attribute dockItemNode + * @type Node + */ + dockItemNode : { + value : null + }, + /** + * The container for the docked item (will contain the block contents when visible) + * @attribute dockcontainerNode + * @type Node + */ + dockcontainerNode : { + value : null + } + } +}); +Y.augment(DOCKEDITEM, Y.EventTarget); + +}, '@VERSION@', { + "requires": [ + "base", + "node", + "event-custom", + "event-mouseenter", + "event-resize", + "escape", + "moodle-core-dockloader" + ] +}); diff --git a/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js b/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js new file mode 100644 index 00000000000..2bfff9fcfb6 --- /dev/null +++ b/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js @@ -0,0 +1,4 @@ +YUI.add("moodle-core-dock",function(e,t){var n="moodle-core-dock",r=e.one(document.body),i={dock:"dock",dockspacer:"dockspacer",controls:"controls",body:"has_dock",buttonscontainer:"buttons_container",dockeditem:"dockeditem",dockeditemcontainer:"dockeditem_container",dockedtitle:"dockedtitle",activeitem:"activeitem",contentonly:"content-only",dockonload:"dock_on_load"},s={dockableblock:".block[data-instanceid][data-dockable]",blockmoveto:".block[data-instanceid][data-dockable] .moveto",panelmoveto:"#dockeditempanel .commands a.moveto",dockonload:".block."+i.dockonload,blockregion:"[data-blockregion]"};M.core=M.core||{},M.core.dock=M.core.dock||{},M.core.dock._dock=null,M.core.dock._dockableblocks={},M.core.dock.init=function(){e.all(s.dockableblock).each(M.core.dock.registerDockableBlock),r.delegate("click",M.core.dock.dockBlock,s.blockmoveto),r.delegate("key",M.core.dock.dockBlock,s.blockmoveto,"enter")},M.core.dock.get=function(){return this._dock===null&&(this._dock=new o),this._dock},M.core.dock.registerDockableBlock=function(e){typeof e=="object"&&typeof e.getData=="function"&&(e=e.getData("instanceid")),M.core.dock._dockableblocks[e]=new f({id:e})},M.core.dock.dockBlock=function(e){typeof e=="object"&&e.target!=="undefined"&&(e=e.target);if(typeof e=="object"){e.test(s.dockableblock)||(e=e.ancestor(s.dockableblock));if(typeof e!="object"||typeof e.getData!="function"||!!e.ancestor("."+i.dock))return;e=e.getData("instanceid")}var t=M.core.dock._dockableblocks[e];t&&t.moveToDock()},M.core.dock.fixTitleOrientation=function(t,n){var i=M.core.dock.get();t=e.one(t);if(i.get("orientation")!=="vertical")return t.setContent(n),t;e.UA.ie>0&&e.UA.ie<8&&(M.str.langconfig.thisdirectionvertical="ver");var s=!1;switch(M.str.langconfig.thisdirectionvertical){case"ver":return t.setContent(n.split("").join("
    "));case"ttb":s=!0;break;case"btt":s=!1}if(e.UA.ie===8)return t.setContent(n),t.setAttribute("style","writing-mode: tb-rl; filter: flipV flipH;display:inline;"),t.addClass("filterrotate"),t;var o="11px",u=s?"rotate(90deg)":"rotate(270deg)",a=e.Node.create('

    '+n+"

    ");r.insert(a,0);var f=a.one("span").get("offsetWidth")*1.2,l=a.one("span").get("offsetHeight");a.remove(),t.setContent(n),t.addClass("css3transform"),t.setStyles({margin:"0",padding:"0",position:"relative",fontSize:o,width:f,top:f/2}),right_to_left()?t.setStyle("left",f/2-l):t.setStyle("right",f/2-l),t.setStyles({transform:u,"-ms-transform":u,"-moz-transform":u,"-webkit-transform":u,"-o-transform":u});var c=e.Node.create("
    ");return c.append(t),c.setStyle("height",f+f/4),c.setStyle("position","relative"),c},M.core.dock.notifyBlockChange=function(e){if(this._dock!==null){var t=M.core.dock.get(),n=t.getActiveItem();n&&n.get("blockinstanceid")===parseInt(e,10)&&t.resizePanelIfRequired()}};var o=function(){o.superclass.constructor.apply(this,arguments)};o.prototype={tabheightmanager:null,preventevent:null,delayedevent:null,dockeditems:[],dockdrawn:!1,count:0,totalcount:0,holdingareanode:null,initializer:function(){this.publish("dock:starting",{prefix:"dock",broadcast:2,emitFacade:!0,fireOnce:!0}),this.publish("dock:initialised",{prefix:"dock",broadcast:2,emitFacade:!0,fireOnce:!0}),this.publish("dock:beforedraw",{prefix:"dock",fireOnce:!0}),this.publish("dock:beforeshow",{prefix:"dock"}),this.publish("dock:shown",{prefix:"dock"}),this.publish("dock:hidden",{prefix:"dock"}),this.publish("dock:itemadded",{prefix:"dock"}),this.publish("dock:itemremoved",{prefix:"dock"}),this.publish("dock:itemschanged",{prefix:"dock"}),this.publish("dock:panelgenerated",{prefix:"dock",fireOnce:!0}),this.publish("dock:panelresizestart",{prefix:"dock"}),this.publish("dock:resizepanelcomplete",{prefix:"dock"}),this._applyThemeCustomisation(),this.fire("dock:starting"),this._ensureDockDrawn(),this.fire("dock:initialised")},_ensureDockDrawn:function(){if(this.dockdrawn===!0)return!0;var t=this._initialiseDockNode();e.UA.ie>0&&e.UA.ie<7&&t.setStyle("height",t.get("winHeight")+"px"),this.fire("dock:beforedraw"),this._initialiseDockControls(),this.tabheightmanager=new a({dock:this});var n={cssselector:"."+i.dockedtitle,delay:0},o={cssselector:"."+i.dockedtitle,delay:.5,iscontained:!0,preventevent:"click",preventdelay:3};return e.delegate("click",this.handleEvent,this.get("dockNode"),"."+i.dockedtitle,this,n),e.delegate("mouseenter",this.handleEvent,this.get("dockNode"),"."+i.dockedtitle,this,o),this.get("dockNode").on("mouseleave",this.handleEvent,this,{cssselector:"#dock",delay:.5,iscontained:!1}),e.delegate("click",this.handleReturnToBlock,this.get("dockNode"),s.panelmoveto,this),e.delegate("click",this.handleReturnToBlock,this.get("dockNode"),s.panelmoveto,this),e.delegate("dock:actionkey",this.handleDockedItemEvent,this.get("dockNode"),"."+i.dockeditem,this),r.on("click",this.handleEvent,this,{cssselector:"body",delay:0}),this.on("dock:itemschanged",this.resizeBlockSpace,this),this.on("dock:itemschanged",this.checkDockVisibility,this),this.on("dock:itemschanged",this.resetFirstItem,this),this.dockdrawn=!0,!0},handleDockedItemEvent:function(e){if(e.type!=="dock:actionkey")return!1;var t=e.target,n="."+i.dockeditem;t.test(n)||(t=t.ancestor(n));if(!t)return!1;e.halt();var r=t.getAttribute("rel");this.dockeditems[r].toggle(e.action)},_applyThemeCustomisation:function(){if(typeof customise_dock_for_theme=="function"){M.core_dock=this,M.core_dock.cfg={buffer:null,orientation:null,position:null,spacebeforefirstitem:null,removeallicon:null},M.core_dock.css={dock:null,dockspacer:null,controls:null,body:null,buttonscontainer:null,dockeditem:null,dockeditemcontainer:null,dockedtitle:null,activeitem:null};try{customise_dock_for_theme(this)}catch(e){}var t,n,r=!1,s={buffer:"bufferPanel",orientation:"orientation",position:"position",spacebeforefirstitem:"bufferBeforeFirstItem",removeallicon:"undockAllIconUrl"};for(t in M.core_dock.cfg){n=M.core_dock.cfg[t];if(n===null)continue;r||(r=!0),this +.set(s[t],n)}for(t in M.core_dock.css){n=M.core_dock.css[t];if(n===null)continue;r||(r=!0),i[t]=n}}},_initialiseDockNode:function(){var t=this.get("dockNode"),n=i.dock+"_"+this.get("position")+"_"+this.get("orientation"),o=e.Node.create("
    ").setStyles({display:"none"}),u=this.get("buttonsNode"),a=this.get("itemContainerNode");return t||(t=e.one("#"+i.dock)),t||(t=e.Node.create('
    '),r.append(t)),t.setAttribute("role","menubar").addClass(n),e.all(s.dockonload).size()===0?t.addClass("nothingdocked"):(n=i.body+"_"+this.get("position")+"_"+this.get("orientation"),r.addClass(i.body).addClass()),u||(u=t.one("."+i.buttonscontainer)),u||(u=e.Node.create('
    '),t.append(u)),a||(a=t.one("."+i.dockeditemcontainer)),a||(a=e.Node.create('
    '),u.append(a)),r.append(o),this.holdingareanode=o,this.set("dockNode",t),this.set("buttonsNode",u),this.set("itemContainerNode",a),t},_initialiseDockControls:function(){var t=e.Node.create(''+M.util.get_string(');t.setAttribute("src",this.get("undockAllIconUrl")),t.on("removeall|click",this.removeAll,this),t.on("dock:actionkey",this.removeAll,this,{actions:{enter:!0}}),this.get("buttonsNode").append(e.Node.create('
    ').append(t))},getPanel:function(){var t=this.get("panel");return t||(t=new u({dock:this}),t.on("panel:visiblechange",this.resize,this),e.on("windowresize",this.resize,this),this.set("panel",t),this.fire("dock:panelgenerated")),t},resizePanelIfRequired:function(){this.resize();var e=this.get("panel");e&&e.correctWidth()},handleEvent:function(e,t){var n=this.getActiveItem();if(t.cssselector==="body")this.get("dockNode").contains(e.target)||n&&n.hide();else{var r;e.target.test(t.cssselector)?r=e.target:r=e.target.ancestor(t.cssselector);if(!r)return!0;if(this.preventevent!==null&&e.type===this.preventevent)return!0;if(t.preventevent){this.preventevent=t.preventevent;if(t.preventdelay){var i=this;setTimeout(function(){i.preventevent=null},t.preventdelay*1e3)}}this.delayedevent&&this.delayedevent.timeout&&(clearTimeout(this.delayedevent.timeout),this.delayedevent.event.detach(),this.delayedevent=null);if(t.delay>0)return this.delayEvent(e,t,r);var s=r.get("id"),o=/^dock_item_(\d+)_title$/;s.match(o)?(n=this.dockeditems[s.replace(o,"$1")],n.active?n.hide():n.show()):n&&n.hide()}return!0},delayEvent:function(e,t,n){var i=this;return i.delayedevent=function(){return{target:n,event:r.on("mousemove",function(e){i.delayedevent.target=e.target}),timeout:null}}(i),i.delayedevent.timeout=setTimeout(function(){i.delayedevent.timeout=null,i.delayedevent.event.detach(),t.iscontained===i.get("dockNode").contains(i.delayedevent.target)&&i.handleEvent(e,{cssselector:t.cssselector,delay:0,iscontained:t.iscontained})},t.delay*1e3),!0},resizeBlockSpace:function(){if(e.all(s.dockonload).size()>0)return;var t=[],n=0,o=!0,u=!1;e.all(s.blockregion).each(function(){var e=region.getData("blockregion");region.all(".block").size()>0?(n++,r.addClass("used-region-"+e),r.removeClass("empty-region-"+e)):(r.addClass("empty-region-"+e),r.removeClass("used-region-"+e))}),e.all(".block-region").each(function(e){if(e.test(s.blockregion))return;var r=e.all(".block").size()>0;r&&n++,o=!1,t[e.get("id")]={hasblocks:r,bodyclass:e.get("id").replace(/^region\-/,"side-")+"-only"}}),r.hasClass("blocks-moving")&&(u=!0),n===0&&u===!1?r.addClass(i.contentonly):r.removeClass(i.contentonly);if(!o){var a=null;if(n===0&&u===!1)for(a in t)r.removeClass(t[a].bodyclass);else if(n===1&&u===!1)for(a in t)t[a].hasblocks?r.addClass(t[a].bodyclass):r.removeClass(t[a].bodyclass);else for(a in t)r.removeClass(t[a].bodyclass)}},add:function(e){e.set("id",this.totalcount),this.count++,this.totalcount++,this.dockeditems[e.get("id")]=e,this.dockeditems[e.get("id")].draw(),this.fire("dock:itemadded",e),this.fire("dock:itemschanged",e)},append:function(e){this.get("itemContainerNode").append(e)},handleReturnToBlock:function(e){e.halt(),this.remove(this.getActiveItem().get("id"))},remove:function(e){return this.dockeditems[e]?(this.dockeditems[e].remove(),delete this.dockeditems[e],this.count--,this.fire("dock:itemremoved",e),this.fire("dock:itemschanged",e),!0):!1},resetFirstItem:function(){this.get("dockNode").all("."+i.dockeditem+".firstdockitem").removeClass("firstdockitem"),this.get("dockNode").one("."+i.dockeditem)&&this.get("dockNode").one("."+i.dockeditem).addClass("firstdockitem")},removeAll:function(){for(var e in this.dockeditems)this.remove(e);return!0},hideActive:function(){var e=this.getActiveItem();e&&e.hide()},checkDockVisibility:function(){var e=i.body+"_"+this.get("position")+"_"+this.get("orientation");this.count?(this.fire("dock:beforeshow"),this.get("dockNode").removeClass("nothingdocked"),r.addClass(i.body).addClass(e),this.fire("dock:shown")):(this.get("dockNode").addClass("nothingdocked"),r.removeClass(i.body).removeClass(),this.fire("dock:hidden"))},resize:function(){var t=this.getPanel(),n=this.getActiveItem();if(!t.get("visible")||!n)return!0;this.fire("dock:panelresizestart");if(this.get("orientation")==="vertical"){var i=this.get("bufferPanel"),s=parseInt(r.get("winHeight"),10)-i*2,o=this.get("dockNode").getY(),u=n.get("dockTitleNode").getY()-o-i,a=this.get("itemContainerNode").getY(),f=a-o+this.get("buttonsNode").get("offsetHeight"),l=t.get("bodyNode").get("scrollTop");t.get("bodyNode").setStyle("height","auto"),t.get("node").removeClass("oversized_content");var c=t.get("node").get("offsetHeight");if(e.UA.ie>0&&e.UA.ie<7)t.setTop(n.get("dockTitleNode").getY());else if(c>s)t.setTop(i-f),t.get("bodyNode").setStyle("height",s-t.get("headerNode").get("offsetHeight")+"px"),t.get("node").addClass("oversized_content");else if(c>s-(u-i)){var h=c-(s-u);t.setTop(u-f-h+i)}else t.setTop(u-f+i);l&&t.get("bodyNode").set("scrollTop",l)}if(this.get("position")==="right")t.get("node").setStyle("left",-t.get("offsetWidth" +)+"px");else if(this.get("position")==="top"){var p=this.get("dockNode").getX(),d=n.get("dockTitleNode").getX()-p;t.get("node").setStyle("left",d+"px")}return this.fire("dock:resizepanelcomplete"),!0},getActiveItem:function(){for(var e in this.dockeditems)if(this.dockeditems[e].active)return this.dockeditems[e];return!1},addToHoldingArea:function(e){this.holdingareanode.append(e)}},e.extend(o,e.Base,o.prototype,{NAME:"moodle-core-dock",ATTRS:{dockNode:{writeOnce:!0},panel:{writeOnce:!0},buttonsNode:{writeOnce:!0},itemContainerNode:{writeOnce:!0},bufferPanel:{value:10,validator:e.Lang.isNumber},position:{value:"left",validator:e.Lang.isString},orientation:{value:"vertical",validator:e.Lang.isString,setter:function(e){return e.match(/^vertical$/i)?"vertical":"horizontal"}},bufferBeforeFirstItem:{value:10,validator:e.Lang.isNumber},undockAllIconUrl:{value:M.util.image_url("t/dock_to_block","moodle"),validator:e.Lang.isString}}}),e.augment(o,e.EventTarget);var u=function(){u.superclass.constructor.apply(this,arguments)};u.prototype={created:!1,initializer:function(){this.publish("dockpanel:beforeshow",{prefix:"dockpanel"}),this.publish("dockpanel:shown",{prefix:"dockpanel"}),this.publish("dockpanel:beforehide",{prefix:"dockpanel"}),this.publish("dockpanel:hidden",{prefix:"dockpanel"}),this.publish("dockpanel:visiblechange",{prefix:"dockpanel"})},create:function(){if(this.created)return!0;this.created=!0;var t=this.get("dock"),n=t.get("dockNode");this.set("node",e.Node.create('
    ')),this.set("contentNode",e.Node.create('
    ')),this.set("headerNode",e.Node.create('
    ')),this.set("bodyNode",e.Node.create('
    ')),n.append(this.get("node").append(this.get("contentNode").append(this.get("headerNode")).append(this.get("bodyNode"))))},show:function(){this.create(),this.fire("dockpanel:beforeshow"),this.set("visible",!0),this.get("node").removeClass("dockitempanel_hidden"),this.fire("dockpanel:shown"),this.fire("dockpanel:visiblechange")},hide:function(){this.fire("dockpanel:beforehide"),this.set("visible",!1),this.get("node").addClass("dockitempanel_hidden"),this.fire("dockpanel:hidden"),this.fire("dockpanel:visiblechange")},setHeader:function(e){this.create();var t=this.get("headerNode");t.setContent(e);if(arguments.length>1)for(var n=1;n0&&e.UA.ie<7?this.get("node").setY(t):this.get("node").setStyle("top",t.toString()+"px")},correctWidth:function(){var e=this.get("bodyNode"),t=e.get("clientWidth"),n=e.get("scrollWidth"),r=e.get("offsetWidth"),i=t,s=Math.round(e.get("winWidth")*.8);n>t&&(i=t+(n-t)+(r-t)*2+10),i>s&&(i=s),i>r&&this.get("node").setStyle("width",i+"px")}},e.extend(u,e.Base,u.prototype,{NAME:"moodle-core-dock-panel",ATTRS:{dock:{writeOnce:"initOnly"},node:{value:null},contentNode:{value:null},headerNode:{value:null},bodyNode:{value:null},visible:{value:!1}}}),e.augment(u,e.EventTarget);var a=function(){a.superclass.constructor.apply(this,arguments)};a.prototype={initializer:function(){var t=this.get("dock");t.on("dock:itemschanged",this.checkSizing,this),e.on("windowresize",this.checkSizing,this)},checkSizing:function(){var t=this.get("dock"),n=t.get("dockNode"),r=t.dockeditems,s=n.get("offsetHeight")-n.one(".controls").get("offsetHeight")-t.get("bufferPanel")*3-r.length*2,o=0,u,a;if(r.length>0){for(u in r)a=e.one(r[u].get("title")).ancestor("."+i.dockedtitle),a&&(this.get("enabled")&&a.setStyle("height","auto"),o+=a.get("offsetHeight")||0);o>s&&this.enable(s)}},enable:function(t){var n=this.get("dock"),r=n.dockeditems,s=n.count,o=0,u=0,a,f,l,c;this.set("enabled",!0);for(a in r){f=e.one(r[a].get("title")).ancestor("."+i.dockedtitle);if(!f)continue;l=Math.floor((t-u)/(s-o)),c=f.get("offsetHeight"),f.setStyle("overflow","hidden"),c>l?(f.setStyle("height",l+"px"),u+=l):u+=c,o++}}},e.extend(a,e.Base,a.prototype,{NAME:"moodle-core-tabheightmanager",ATTRS:{dock:{writeOnce:"initOnly"},enabled:{value:!1}}}),e.Event.define("dock:actionkey",{_event:e.UA.webkit||e.UA.ie?"keydown":"keypress",_keys:{37:"collapse",39:"expand",32:"toggle",13:"enter"},_keyHandler:function(e,t,n){var r;n.actions?r=n.actions:r={collapse:!0,expand:!0,toggle:!0,enter:!0},this._keys[e.keyCode]&&r[this._keys[e.keyCode]]&&(e.action=this._keys[e.keyCode],t.fire(e))},on:function(e,t,n){t.args===null?t._detacher=e.on(this._event,this._keyHandler,this,n,{actions:!1}):t._detacher=e.on(this._event,this._keyHandler,this,n,t.args[0])},detach:function(e,t,n){t._detacher.detach()},delegate:function(e,t,n,r){t.args===null?t._delegateDetacher=e.delegate(this._event,this._keyHandler,r,this,n,{actions:!1}):t._delegateDetacher=e.delegate(this._event,this._keyHandler,r,this,n,t.args[0])},detachDelegate:function(e,t,n,r){t._delegateDetacher.detach()}});var f=function(){f.superclass.constructor.apply(this,arguments)};f.prototype={contentplaceholder:null,contentskipanchor:null,cachedcontentnode:null,skipsetposition:!0,dockitem:null,initializer:function(){var t=e.one("#inst"+this.get("id"));if(!t)return!1;M.core.dock.ensureMoveToIconExists(t);if(t.hasClass(i.dockonload)){t.removeClass(i.dockonload);var n=t.one(".header .title .commands");n||(n=e.Node.create('
    '),t.one(".header .title")&&t.one(".header .title").append(n)),this.moveToDock(null,n)}return this.skipsetposition=!1,!0},_getBlockClass:function(e){var t=e.getAttribute("className").toString(),n=/(^|\s)(block_[a-zA-Z0-9_]+)(\s|$)/,r=n.exec(t);return r?r[2]:r},moveToDock:function(t){t&&t.halt(!0);var n=M.core.dock.get(),r=this.get("id"),i=e.one("#inst"+r).one(".content");if(!i)return;var s=right_to_left()?"t/dock_to_block_rtl":"t/dock_to_block",o=location.href.match(/\?/)?"&":"?",u,a,f,c;this.recordBlockState(),u=this.cachedcontentnode.one(".title h2").cloneNode +(!0),a=this.cachedcontentnode.one(".title .commands").cloneNode(!0),f=e.Node.create(''+e.Escape.html(M.str.block.undockitem)+''),f.setAttribute("src",M.util.image_url(s,"moodle")),c=e.Node.create('').append(f),c.set("href",location.href+o+"dock="+r),a.append(c),this.dockitem=new l({block:this,dock:n,blockinstanceid:r,title:u,contents:i,commands:a,blockclass:this._getBlockClass(e.one("#inst"+r))}),n.add(this.dockitem),this.skipsetposition||M.util.set_user_preference("docked_block_instance_"+r,1),this.set("idDocked",!0)},recordBlockState:function(){var t=this.get("id"),n=M.core.dock.get(),r=e.one("#inst"+t),i=r.previous();i.hasClass("skip-block")&&(this.contentskipanchor=i,this.contentskipanchor.hide()),this.cachedcontentnode=r,this.contentplaceholder=e.Node.create('
    '),r.replace(this.contentplaceholder),n.addToHoldingArea(r),r=null,this.cachedcontentnode.one(".title .commands")||this.cachedcontentnode.one(".title").append(e.Node.create('
    '))},returnToBlock:function(){var t=this.get("id");this.contentskipanchor&&this.contentskipanchor.show(),this.cachedcontentnode.one(".header")?this.cachedcontentnode.one(".header").insert(this.dockitem.get("contents"),"after"):this.cachedcontentnode.insert(this.dockitem.get("contents")),this.contentplaceholder.replace(this.cachedcontentnode),this.cachedcontentnode=e.one("#"+this.cachedcontentnode.get("id"));var n=this.dockitem.get("commands");return n&&(n.all(".hidepanelicon").remove(),n.all(".moveto").remove(),n.remove()),this.cachedcontentnode.one(".title").append(n),this.cachedcontentnode=null,M.util.set_user_preference("docked_block_instance_"+t,0),this.set("idDocked",!1),!0}},e.extend(f,e.Base,f.prototype,{NAME:"moodle-core-dock-block",ATTRS:{id:{writeOnce:"initOnly",setter:function(e){return parseInt(e,10)}},isDocked:{value:!1}}});var l=function(){l.superclass.constructor.apply(this,arguments)};l.prototype={active:!1,initializer:function(){var t=this.get("title"),n;this.publish("dockeditem:drawstart",{prefix:"dockeditem"}),this.publish("dockeditem:drawcomplete",{prefix:"dockeditem"}),this.publish("dockeditem:showstart",{prefix:"dockeditem"}),this.publish("dockeditem:showcomplete",{prefix:"dockeditem"}),this.publish("dockeditem:hidestart",{prefix:"dockeditem"}),this.publish("dockeditem:hidecomplete",{prefix:"dockeditem"}),this.publish("dockeditem:itemremoved",{prefix:"dockeditem"});if(t){var r=t.get("nodeName");n=t.cloneNode(!0),t=e.Node.create("<"+r+">"),t=M.core.dock.fixTitleOrientation(t,n.get("text")),this.set("title",t),this.set("titlestring",n)}},draw:function(){var t=e.Node.create,n=this.get("dock"),r=n.count,s,o,u,a,f=this.get("id");return this.fire("dockeditem:drawstart"),s=t(''),s.append(this.get("title")),o=t('
    '),r===1&&o.addClass("firstdockitem"),o.append(s),n.append(o),a=t(''+M.str.block.hidepanel+''),a.setAttribute("src",M.util.image_url("t/dockclose","moodle")),u=t('').append(a),u.on("forceclose|click",this.hide,this),u.on("dock:actionkey",this.hide,this,{actions:{enter:!0,toggle:!0}}),this.get("commands").append(u),this.set("dockTitleNode",s),this.set("dockItemNode",o),this.fire("dockeditem:drawcomplete"),!0},show:function(){var t=this.get("dock"),n=t.getPanel(),r=this.get("dockTitleNode");return t.hideActive(),this.fire("dockeditem:showstart"),n.setHeader(this.get("titlestring"),this.get("commands")),n.setBody(e.Node.create('
    ').append(this.get("contents"))),n.show(),n.correctWidth(),this.active=!0,r.addClass(i.activeitem),r.set("aria-expanded","true"),this.fire("dockeditem:showcomplete"),t.resize(),!0},hide:function(){this.fire("dockeditem:hidestart"),this.active&&(this.active=!1,this.get("dock").getPanel().hide()),this.get("dockTitleNode").removeClass(i.activeitem).set("aria-expanded","false"),this.fire("dockeditem:hidecomplete")},toggle:function(e){var t=this.get("dockTitleNode");t.hasClass(i.activeitem)&&e!=="expand"?this.hide():!t.hasClass(i.activeitem)&&e!=="collapse"&&this.show()},remove:function(){this.hide(),this.get("block").returnToBlock(),this.get("dockItemNode").remove(),this.fire("dockeditem:itemremoved")},_getLogDescription:function(){return this.get("titlestring").get("innerHTML")+" ("+this.get("blockinstanceid")+")"}},e.extend(l,e.Base,l.prototype,{NAME:"moodle-core-dock-dockeditem",ATTRS:{block:{writeOnce:"initOnly"},dock:{writeOnce:"initOnly"},id:{},blockinstanceid:{writeOnce:"initOnly",setter:function(e){return parseInt(e,10)}},title:{value:null},titlestring:{value:null},contents:{writeOnce:"initOnly"},commands:{writeOnce:"initOnly"},blockclass:{writeOnce:"initOnly"},dockTitleNode:{value:null},dockItemNode:{value:null},dockcontainerNode:{value:null}}}),e.augment(l,e.EventTarget)},"@VERSION@",{requires:["base","node","event-custom","event-mouseenter","event-resize","escape","moodle-core-dockloader"]}); diff --git a/lib/yui/build/moodle-core-dock/moodle-core-dock.js b/lib/yui/build/moodle-core-dock/moodle-core-dock.js new file mode 100644 index 00000000000..66efa1ae6dc --- /dev/null +++ b/lib/yui/build/moodle-core-dock/moodle-core-dock.js @@ -0,0 +1,2098 @@ +YUI.add('moodle-core-dock', function (Y, NAME) { + +/** + * Dock JS. + * + * This file contains the DOCK object and all dock related global namespace methods and properties. + * + * @module moodle-core-dock + */ + + +var LOGNS = 'moodle-core-dock'; +var BODY = Y.one(document.body); +var CSS = { + dock: 'dock', // CSS Class applied to the dock box + dockspacer: 'dockspacer', // CSS class applied to the dockspacer + controls: 'controls', // CSS class applied to the controls box + body: 'has_dock', // CSS class added to the body when there is a dock + buttonscontainer: 'buttons_container', + dockeditem: 'dockeditem', // CSS class added to each item in the dock + dockeditemcontainer: 'dockeditem_container', + dockedtitle: 'dockedtitle', // CSS class added to the item's title in each dock + activeitem: 'activeitem', // CSS class added to the active item + contentonly: 'content-only', + dockonload: 'dock_on_load' +}; +var SELECTOR = { + dockableblock: '.block[data-instanceid][data-dockable]', + blockmoveto: '.block[data-instanceid][data-dockable] .moveto', + panelmoveto: '#dockeditempanel .commands a.moveto', + dockonload: '.block.'+CSS.dockonload, + blockregion: '[data-blockregion]' +}; + +/** + * Core namespace. + * + * @static + * @namespace M + * @class core + */ +M.core = M.core || {}; + +/** + * Dock namespace. + * + * @static + * @namespace M.core + * @class dock + */ +M.core.dock = M.core.dock || {}; + +/** + * The dock - once initialised. + * + * @private + * @property _dock + * @type DOCK + */ +M.core.dock._dock = null; + +/** + * An associative array of dockable blocks. + * @property _dockableblocks + * @type {Array} An array of BLOCK objects organised by instanceid. + * @private + */ +M.core.dock._dockableblocks = {}; + +/** + * Initialises the dock. + * This method registers dockable blocks, and creates delegations to dock them. + * @static + * @method init + */ +M.core.dock.init = function() { + Y.all(SELECTOR.dockableblock).each(M.core.dock.registerDockableBlock); + BODY.delegate('click', M.core.dock.dockBlock, SELECTOR.blockmoveto); + BODY.delegate('key', M.core.dock.dockBlock, SELECTOR.blockmoveto, 'enter'); +}; + +/** + * Returns an instance of the dock. + * Initialises one if one hasn't already being initialised. + * + * @static + * @method get + * @return DOCK + */ +M.core.dock.get = function() { + if (this._dock === null) { + this._dock = new DOCK(); + } + return this._dock; +}; + +/** + * Registers a dockable block with the dock. + * + * @static + * @method registerDockableBlock + * @param {int} id The block instance ID. + * @return void + */ +M.core.dock.registerDockableBlock = function(id) { + if (typeof id === 'object' && typeof id.getData === 'function') { + id = id.getData('instanceid'); + } + M.core.dock._dockableblocks[id] = new BLOCK({id : id}); +}; + +/** + * Docks a block given either its instanceid, its node, or an event fired from within the block. + * @static + * @method dockBlockByInstanceID + * @param id + * @return void + */ +M.core.dock.dockBlock = function(id) { + if (typeof id === 'object' && id.target !== 'undefined') { + id = id.target; + } + if (typeof id === "object") { + if (!id.test(SELECTOR.dockableblock)) { + id = id.ancestor(SELECTOR.dockableblock); + } + if (typeof id === 'object' && typeof id.getData === 'function' && !id.ancestor('.'+CSS.dock)) { + id = id.getData('instanceid'); + } else { + return; + } + } + var block = M.core.dock._dockableblocks[id]; + if (block) { + block.moveToDock(); + } +}; + +/** + * Fixes the title orientation. Rotating it if required. + * + * @static + * @method fixTitleOrientation + * @param {Node} title The title node we are looking at. + * @param {String} text The string to use as the title. + * @return {Node} The title node to use. + */ +M.core.dock.fixTitleOrientation = function(title, text) { + var dock = M.core.dock.get(); + title = Y.one(title); + + if (dock.get('orientation') !== 'vertical') { + // If the dock isn't vertical don't adjust it! + title.setContent(text); + return title; + } + + if (Y.UA.ie > 0 && Y.UA.ie < 8) { + // IE 6/7 can't rotate text so force ver + M.str.langconfig.thisdirectionvertical = 'ver'; + } + + var clockwise = false; + switch (M.str.langconfig.thisdirectionvertical) { + case 'ver': + // Stacked is easy + return title.setContent(text.split('').join('
    ')); + case 'ttb': + clockwise = true; + break; + case 'btt': + clockwise = false; + break; + } + + if (Y.UA.ie === 8) { + // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute. + title.setContent(text); + title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;'); + title.addClass('filterrotate'); + return title; + } + + // We need to fix a font-size - sorry theme designers. + var fontsize = '11px'; + var transform = (clockwise) ? 'rotate(90deg)' : 'rotate(270deg)'; + var test = Y.Node.create('

    '+text+'

    '); + BODY.insert(test, 0); + var width = test.one('span').get('offsetWidth') * 1.2; + var height = test.one('span').get('offsetHeight'); + test.remove(); + + title.setContent(text); + title.addClass('css3transform'); + + // Move the title into position + title.setStyles({ + 'margin' : '0', + 'padding' : '0', + 'position' : 'relative', + 'fontSize' : fontsize, + 'width' : width, + 'top' : width/2 + }); + + // Positioning is different when in RTL mode. + if (right_to_left()) { + title.setStyle('left', width/2 - height); + } else { + title.setStyle('right', width/2 - height); + } + + // Rotate the text + title.setStyles({ + 'transform' : transform, + '-ms-transform' : transform, + '-moz-transform' : transform, + '-webkit-transform' : transform, + '-o-transform' : transform + }); + + var container = Y.Node.create('
    '); + container.append(title); + container.setStyle('height', width + (width / 4)); + container.setStyle('position', 'relative'); + return container; +}; + +/** + * Informs the dock that the content of the block has changed. + * This should be called by the blocks JS code if its content has been updated dynamically. + * This method ensure the dock resizes if need be. + * + * @static + * @method notifyBlockChange + * @param {Number} instanceid + * @return void + */ +M.core.dock.notifyBlockChange = function(instanceid) { + if (this._dock !== null) { + var dock = M.core.dock.get(), + activeitem = dock.getActiveItem(); + if (activeitem && activeitem.get('blockinstanceid') === parseInt(instanceid, 10)) { + dock.resizePanelIfRequired(); + } + } +}; + +/** + * The Dock. + * + * @namespace M.core.dock + * @class Dock + * @constructor + * @extends Y.Base + * @uses Y.EventTarget + */ +var DOCK = function() { + DOCK.superclass.constructor.apply(this, arguments); +}; +DOCK.prototype = { + /** + * Tab height manager used to ensure tabs are always visible. + * @protected + * @property tabheightmanager + * @type TABHEIGHTMANAGER + */ + tabheightmanager : null, + /** + * Will be an eventtype if there is an eventype to prevent. + * @protected + * @property preventevent + * @type String + */ + preventevent : null, + /** + * Will be an object if there is a delayed event in effect. + * @protected + * @property delayedevent + * @type {Object} + */ + delayedevent : null, + /** + * An array of currently docked items. + * @protected + * @property dockeditems + * @type Array + */ + dockeditems : [], + /** + * Set to true once the dock has been drawn. + * @protected + * @property dockdrawn + * @type Boolean + */ + dockdrawn : false, + /** + * The number of blocks that are currently docked. + * @protected + * @property count + * @type Number + */ + count : 0, + /** + * The total number of blocks that have been docked. + * @protected + * @property totalcount + * @type Number + */ + totalcount : 0, + /** + * A hidden node used as a holding area for DOM objects used by blocks that have been docked. + * @protected + * @property holdingareanode + * @type Node + */ + holdingareanode : null, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + + // Publish the events the dock has + /** + * Fired when the dock first starts initialising. + * @event dock:starting + */ + this.publish('dock:starting', {prefix: 'dock',broadcast: 2,emitFacade: true, fireOnce:true}); + /** + * Fired after the dock is initialised for the first time. + * @event dock:initialised + */ + this.publish('dock:initialised', {prefix: 'dock',broadcast: 2,emitFacade: true, fireOnce:true}); + /** + * Fired before the dock structure and content is first created. + * @event dock:beforedraw + */ + this.publish('dock:beforedraw', {prefix:'dock', fireOnce:true}); + /** + * Fired before the dock is changed from hidden to visible. + * @event dock:beforeshow + */ + this.publish('dock:beforeshow', {prefix:'dock'}); + /** + * Fires after the dock has been changed from hidden to visible. + * @event dock:shown + */ + this.publish('dock:shown', {prefix:'dock'}); + /** + * Fired after the dock has been changed from visible to hidden. + * @event dock:hidden + */ + this.publish('dock:hidden', {prefix:'dock'}); + /** + * Fires when an item is added to the dock. + * @event dock:itemadded + */ + this.publish('dock:itemadded', {prefix:'dock'}); + /** + * Fires when an item is removed from the dock. + * @event dock:itemremoved + */ + this.publish('dock:itemremoved', {prefix:'dock'}); + /** + * Fires when a block is added or removed from the dock. + * This happens after the itemadded and itemremoved events have been called. + * @event dock:itemschanged + */ + this.publish('dock:itemschanged', {prefix:'dock'}); + /** + * Fires once when the docks panel is first initialised. + * @event dock:panelgenerated + */ + this.publish('dock:panelgenerated', {prefix:'dock', fireOnce:true}); + /** + * Fires when the dock panel is about to be resized. + * @event dock:panelresizestart + */ + this.publish('dock:panelresizestart', {prefix:'dock'}); + /** + * Fires after the dock panel has been resized. + * @event dock:resizepanelcomplete + */ + this.publish('dock:resizepanelcomplete', {prefix:'dock'}); + + // Apply theme customisations here before we do any real work. + this._applyThemeCustomisation(); + // Inform everyone we are now about to initialise. + this.fire('dock:starting'); + this._ensureDockDrawn(); + // Inform everyone the dock has been initialised + this.fire('dock:initialised'); + }, + /** + * Ensures that the dock has been drawn. + * @private + * @method _ensureDockDrawn + * @return {Boolean} + */ + _ensureDockDrawn : function() { + if (this.dockdrawn === true) { + return true; + } + var dock = this._initialiseDockNode(); + if (Y.UA.ie > 0 && Y.UA.ie < 7) { + // Adjust for IE 6 (can't handle fixed pos) + dock.setStyle('height', dock.get('winHeight')+'px'); + } + + this.fire('dock:beforedraw'); + + this._initialiseDockControls(); + + this.tabheightmanager = new TABHEIGHTMANAGER({dock:this}); + + var clickargs = {cssselector:'.'+CSS.dockedtitle, delay:0}; + var mouseenterargs = {cssselector:'.'+CSS.dockedtitle, delay:0.5, iscontained:true, preventevent:'click', preventdelay:3}; + + // Attach the required event listeners + // We use delegate here as that way a handful of events are created for the dock + // and all items rather than the same number for the dock AND every item individually + Y.delegate('click', this.handleEvent, this.get('dockNode'), '.'+CSS.dockedtitle, this, clickargs); + Y.delegate('mouseenter', this.handleEvent, this.get('dockNode'), '.'+CSS.dockedtitle, this, mouseenterargs); + this.get('dockNode').on('mouseleave', this.handleEvent, this, {cssselector:'#dock', delay:0.5, iscontained:false}); + + Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this); + Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this); + Y.delegate('dock:actionkey', this.handleDockedItemEvent, this.get('dockNode'), '.'+CSS.dockeditem, this); + + BODY.on('click', this.handleEvent, this, {cssselector:'body', delay:0}); + this.on('dock:itemschanged', this.resizeBlockSpace, this); + this.on('dock:itemschanged', this.checkDockVisibility, this); + this.on('dock:itemschanged', this.resetFirstItem, this); + this.dockdrawn = true; + return true; + }, + /** + * Handles an actionkey event on the dock. + * @param {EventFacade} e + * @method handleDockedItemEvent + * @return {Boolean} + */ + handleDockedItemEvent : function(e) { + if (e.type !== 'dock:actionkey') { + return false; + } + var target = e.target, + dockeditem = '.'+CSS.dockeditem; + if (!target.test(dockeditem)) { + target = target.ancestor(dockeditem); + } + if (!target) { + return false; + } + e.halt(); + var id = target.getAttribute('rel'); + this.dockeditems[id].toggle(e.action); + }, + /** + * Call the theme customisation method "customise_dock_for_theme" if it exists. + * @private + * @method _applyThemeCustomisation + */ + _applyThemeCustomisation : function() { + // Check if there is a customisation function + if (typeof(customise_dock_for_theme) === 'function') { + // First up pre the legacy object. + M.core_dock = this; + M.core_dock.cfg = { + buffer : null, + orientation : null, + position : null, + spacebeforefirstitem : null, + removeallicon : null + }; + M.core_dock.css = { + dock : null, + dockspacer : null, + controls : null, + body : null, + buttonscontainer : null, + dockeditem : null, + dockeditemcontainer : null, + dockedtitle : null, + activeitem : null + }; + try { + // Run the customisation function + customise_dock_for_theme(this); + } catch (exception) { + // Do nothing at the moment. + } + // Now to work out what they did. + var key, value, + warned = false, + cfgmap = { + buffer : 'bufferPanel', + orientation : 'orientation', + position : 'position', + spacebeforefirstitem : 'bufferBeforeFirstItem', + removeallicon : 'undockAllIconUrl' + }; + // Check for and apply any legacy configuration. + for (key in M.core_dock.cfg) { + value = M.core_dock.cfg[key]; + if (value === null) { + continue; + } + if (!warned) { + warned = true; + } + // Damn, the've set something. + this.set(cfgmap[key], value); + } + // Check for and apply any legacy CSS changes.. + for (key in M.core_dock.css) { + value = M.core_dock.css[key]; + if (value === null) { + continue; + } + if (!warned) { + warned = true; + } + // Damn, they've set something. + CSS[key] = value; + } + } + }, + /** + * Initialises the dock node, creating it and its content if required. + * + * @private + * @method _initialiseDockNode + * @return {Node} The dockNode + */ + _initialiseDockNode : function() { + var dock = this.get('dockNode'), + positionorientationclass = CSS.dock+'_'+this.get('position')+'_'+this.get('orientation'), + holdingarea = Y.Node.create('
    ').setStyles({display:'none'}), + buttons = this.get('buttonsNode'), + container = this.get('itemContainerNode'); + + if (!dock) { + dock = Y.one('#'+CSS.dock); + } + if (!dock) { + dock = Y.Node.create('
    '); + BODY.append(dock); + } + dock.setAttribute('role', 'menubar').addClass(positionorientationclass); + if (Y.all(SELECTOR.dockonload).size() === 0) { + // Nothing on the dock... hide it using CSS + dock.addClass('nothingdocked'); + } else { + positionorientationclass = CSS.body+'_'+this.get('position')+'_'+this.get('orientation'); + BODY.addClass(CSS.body).addClass(); + } + + if (!buttons) { + buttons = dock.one('.'+CSS.buttonscontainer); + } + if (!buttons) { + buttons = Y.Node.create('
    '); + dock.append(buttons); + } + + if (!container) { + container = dock.one('.'+CSS.dockeditemcontainer); + } + if (!container) { + container = Y.Node.create('
    '); + buttons.append(container); + } + + BODY.append(holdingarea); + this.holdingareanode = holdingarea; + + this.set('dockNode', dock); + this.set('buttonsNode', buttons); + this.set('itemContainerNode', container); + + return dock; + }, + /** + * Initialises the dock controls. + * + * @private + * @method _initialiseDockControls + */ + _initialiseDockControls : function() { + // Add a removeall button + // Must set the image src seperatly of we get an error with XML strict headers + + var removeall = Y.Node.create(''+M.util.get_string('undockall', 'block')+''); + removeall.setAttribute('src',this.get('undockAllIconUrl')); + removeall.on('removeall|click', this.removeAll, this); + removeall.on('dock:actionkey', this.removeAll, this, {actions:{enter:true}}); + this.get('buttonsNode').append(Y.Node.create('
    ').append(removeall)); + }, + /** + * Returns the dock panel. Initialising it if it hasn't already been initialised. + * @method getPanel + * @return {DOCKPANEL} + */ + getPanel : function() { + var panel = this.get('panel'); + if (!panel) { + panel = new DOCKPANEL({dock:this}); + panel.on('panel:visiblechange', this.resize, this); + Y.on('windowresize', this.resize, this); + // Initialise the dockpanel .. should only happen once + this.set('panel', panel); + this.fire('dock:panelgenerated'); + } + return panel; + }, + /** + * Resizes the dock panel if required. + * @method resizePanelIfRequired + */ + resizePanelIfRequired : function() { + this.resize(); + var panel = this.get('panel'); + if (panel) { + panel.correctWidth(); + } + }, + /** + * Handles a dock event sending it to the right place. + * + * @method handleEvent + * @param {EventFacade} e + * @param {Object} options + * @return {Boolean} + */ + handleEvent : function(e, options) { + var item = this.getActiveItem(); + if (options.cssselector === 'body') { + if (!this.get('dockNode').contains(e.target)) { + if (item) { + item.hide(); + } + } + } else { + var target; + if (e.target.test(options.cssselector)) { + target = e.target; + } else { + target = e.target.ancestor(options.cssselector); + } + if (!target) { + return true; + } + if (this.preventevent !== null && e.type === this.preventevent) { + return true; + } + if (options.preventevent) { + this.preventevent = options.preventevent; + if (options.preventdelay) { + var self = this; + setTimeout(function(){ + self.preventevent = null; + }, options.preventdelay * 1000); + } + } + if (this.delayedevent && this.delayedevent.timeout) { + clearTimeout(this.delayedevent.timeout); + this.delayedevent.event.detach(); + this.delayedevent = null; + } + if (options.delay > 0) { + return this.delayEvent(e, options, target); + } + var targetid = target.get('id'); + var regex = /^dock_item_(\d+)_title$/; + if (targetid.match(regex)) { + item = this.dockeditems[targetid.replace(regex, '$1')]; + if (item.active) { + item.hide(); + } else { + item.show(); + } + } else if (item) { + item.hide(); + } + } + return true; + }, + /** + * Delays an event. + * + * @method delayEvent + * @param {EventFacade} event + * @param {Object} options + * @param {Node} target + * @return {Boolean} + */ + delayEvent : function(event, options, target) { + var self = this; + self.delayedevent = (function(){ + return { + target : target, + event : BODY.on('mousemove', function(e){ + self.delayedevent.target = e.target; + }), + timeout : null + }; + })(self); + self.delayedevent.timeout = setTimeout(function(){ + self.delayedevent.timeout = null; + self.delayedevent.event.detach(); + if (options.iscontained === self.get('dockNode').contains(self.delayedevent.target)) { + self.handleEvent(event, {cssselector:options.cssselector, delay:0, iscontained:options.iscontained}); + } + }, options.delay*1000); + return true; + }, + /** + * Resizes block spaces. + * @method resizeBlockSpace + */ + resizeBlockSpace : function() { + if (Y.all(SELECTOR.dockonload).size() > 0) { + // Do not resize during initial load + return; + } + var blockregions = [], + populatedblockregions = 0, + allnewregions = true, + showregions = false; + // First look for understood regions. + Y.all(SELECTOR.blockregion).each(function(){ + var regionname = region.getData('blockregion'); + if (region.all('.block').size() > 0) { + populatedblockregions++; + BODY.addClass('used-region-'+regionname); + BODY.removeClass('empty-region-'+regionname); + } else { + BODY.addClass('empty-region-'+regionname); + BODY.removeClass('used-region-'+regionname); + } + }); + // Next check for legacy regions. + Y.all('.block-region').each(function(region){ + if (region.test(SELECTOR.blockregion)) { + // This is a new region, we've already processed it. + return; + } + var hasblocks = (region.all('.block').size() > 0); + if (hasblocks) { + populatedblockregions++; + } + allnewregions = false; + blockregions[region.get('id')] = { + hasblocks : hasblocks, + bodyclass : region.get('id').replace(/^region\-/, 'side-')+'-only' + }; + }); + if (BODY.hasClass('blocks-moving')) { + // open up blocks during blocks positioning + showregions = true; + } + if (populatedblockregions === 0 && showregions === false) { + BODY.addClass(CSS.contentonly); + } else { + BODY.removeClass(CSS.contentonly); + } + + if (!allnewregions) { + var i = null; + if (populatedblockregions === 0 && showregions === false) { + for (i in blockregions) { + BODY.removeClass(blockregions[i].bodyclass); + } + } else if (populatedblockregions === 1 && showregions === false) { + for (i in blockregions) { + if (!blockregions[i].hasblocks) { + BODY.removeClass(blockregions[i].bodyclass); + } else { + BODY.addClass(blockregions[i].bodyclass); + } + } + } else { + for (i in blockregions) { + BODY.removeClass(blockregions[i].bodyclass); + } + } + } + }, + /** + * Adds an item to the dock. + * @method add + * @param {DOCKEDITEM} item + */ + add : function(item) { + // Set the dockitem id to the total count and then increment it. + item.set('id', this.totalcount); + this.count++; + this.totalcount++; + this.dockeditems[item.get('id')] = item; + this.dockeditems[item.get('id')].draw(); + this.fire('dock:itemadded', item); + this.fire('dock:itemschanged', item); + }, + /** + * Appends an item to the dock (putting it in the item container. + * @method append + * @param {Node} docknode + */ + append : function(docknode) { + this.get('itemContainerNode').append(docknode); + }, + /** + * Handles events that require a docked block to be returned to the page./ + * @method handleReturnToBlock + * @param {EventFacade} e + */ + handleReturnToBlock : function(e) { + e.halt(); + this.remove(this.getActiveItem().get('id')); + }, + /** + * Removes a docked item from the dock. + * @method remove + * @param {Number} id The docked item id. + * @return {Boolean} + */ + remove : function(id) { + if (!this.dockeditems[id]) { + return false; + } + this.dockeditems[id].remove(); + delete this.dockeditems[id]; + this.count--; + this.fire('dock:itemremoved', id); + this.fire('dock:itemschanged', id); + return true; + }, + /** + * Ensures the the first item in the dock has the correct class. + * @method resetFirstItem + */ + resetFirstItem : function() { + this.get('dockNode').all('.'+CSS.dockeditem+'.firstdockitem').removeClass('firstdockitem'); + if (this.get('dockNode').one('.'+CSS.dockeditem)) { + this.get('dockNode').one('.'+CSS.dockeditem).addClass('firstdockitem'); + } + }, + /** + * Removes all docked blocks returning them to the page. + * @method removeAll + * @return {Boolean} + */ + removeAll : function() { + for (var i in this.dockeditems) { + this.remove(i); + } + return true; + }, + /** + * Hides the active item. + * @method hideActive + */ + hideActive : function() { + var item = this.getActiveItem(); + if (item) { + item.hide(); + } + }, + /** + * Checks wether the dock should be shown or hidden + * @method checkDockVisibility + */ + checkDockVisibility : function() { + var bodyclass = CSS.body+'_'+this.get('position')+'_'+this.get('orientation'); + if (!this.count) { + this.get('dockNode').addClass('nothingdocked'); + BODY.removeClass(CSS.body).removeClass(); + this.fire('dock:hidden'); + } else { + this.fire('dock:beforeshow'); + this.get('dockNode').removeClass('nothingdocked'); + BODY.addClass(CSS.body).addClass(bodyclass); + this.fire('dock:shown'); + } + }, + /** + * This function checks the size and position of the panel and moves/resizes if + * required to keep it within the bounds of the window. + * @method resize + * @return {Boolean} + */ + resize : function() { + var panel = this.getPanel(); + var item = this.getActiveItem(); + if (!panel.get('visible') || !item) { + return true; + } + + this.fire('dock:panelresizestart'); + if (this.get('orientation') === 'vertical') { + var buffer = this.get('bufferPanel'); + var screenheight = parseInt(BODY.get('winHeight'), 10)-(buffer*2); + var docky = this.get('dockNode').getY(); + var titletop = item.get('dockTitleNode').getY()-docky-buffer; + var containery = this.get('itemContainerNode').getY(); + var containerheight = containery-docky+this.get('buttonsNode').get('offsetHeight'); + var scrolltop = panel.get('bodyNode').get('scrollTop'); + panel.get('bodyNode').setStyle('height', 'auto'); + panel.get('node').removeClass('oversized_content'); + var panelheight = panel.get('node').get('offsetHeight'); + + if (Y.UA.ie > 0 && Y.UA.ie < 7) { + panel.setTop(item.get('dockTitleNode').getY()); + } else if (panelheight > screenheight) { + panel.setTop(buffer-containerheight); + panel.get('bodyNode').setStyle('height', (screenheight-panel.get('headerNode').get('offsetHeight'))+'px'); + panel.get('node').addClass('oversized_content'); + } else if (panelheight > (screenheight-(titletop-buffer))) { + var difference = panelheight - (screenheight-titletop); + panel.setTop(titletop-containerheight-difference+buffer); + } else { + panel.setTop(titletop-containerheight+buffer); + } + + if (scrolltop) { + panel.get('bodyNode').set('scrollTop', scrolltop); + } + } + + if (this.get('position') === 'right') { + panel.get('node').setStyle('left', -panel.get('offsetWidth')+'px'); + + } else if (this.get('position') === 'top') { + var dockx = this.get('dockNode').getX(); + var titleleft = item.get('dockTitleNode').getX()-dockx; + panel.get('node').setStyle('left', titleleft+'px'); + } + + this.fire('dock:resizepanelcomplete'); + return true; + }, + /** + * Returns the currently active dock item or false + * @method getActiveItem + * @return {DOCKEDITEM} + */ + getActiveItem : function() { + for (var i in this.dockeditems) { + if (this.dockeditems[i].active) { + return this.dockeditems[i]; + } + } + return false; + }, + /** + * Adds an item to the holding area. + * @method addToHoldingArea + * @param {Node} node + */ + addToHoldingArea : function(node) { + this.holdingareanode.append(node); + } +}; + +Y.extend(DOCK, Y.Base, DOCK.prototype, { + NAME : 'moodle-core-dock', + ATTRS : { + /** + * The dock itself. #dock. + * @attribute dockNode + * @type Node + * @writeOnce + */ + dockNode : { + writeOnce : true + }, + /** + * The docks panel. + * @attribute panel + * @type DOCKPANEL + * @writeOnce + */ + panel : { + writeOnce : true + }, + /** + * A container within the dock used for buttons. + * @attribute buttonsNode + * @type Node + * @writeOnce + */ + buttonsNode : { + writeOnce : true + }, + /** + * A container within the dock used for docked blocks. + * @attribute itemContainerNode + * @type Node + * @writeOnce + */ + itemContainerNode : { + writeOnce : true + }, + + /** + * Buffer used when containing a panel. + * @attribute bufferPanel + * @type Number + * @default 10 + */ + bufferPanel : { + value : 10, + validator : Y.Lang.isNumber + }, + + /** + * Position of the dock. + * @attribute position + * @type String + * @default left + */ + position : { + value : 'left', + validator : Y.Lang.isString + }, + + /** + * vertical || horizontal determines if we change the title + * @attribute orientation + * @type String + * @default vertical + */ + orientation : { + value : 'vertical', + validator : Y.Lang.isString, + setter : function(value) { + if (value.match(/^vertical$/i)) { + return 'vertical'; + } + return 'horizontal'; + } + }, + + /** + * Space between the top of the dock and the first item. + * @attribute bufferBeforeFirstItem + * @type Number + * @default 10 + */ + bufferBeforeFirstItem : { + value : 10, + validator : Y.Lang.isNumber + }, + + /** + * Icon URL for the icon to undock all blocks + * @attribute undockAllIconUrl + * @type String + * @default t/dock_to_block + */ + undockAllIconUrl : { + value : M.util.image_url('t/dock_to_block', 'moodle'), + validator : Y.Lang.isString + } + } +}); +Y.augment(DOCK, Y.EventTarget); +/** + * Dock JS. + * + * This file contains the panel class used by the dock to display the content of docked blocks. + * + * @module moodle-core-dock + */ + +/** + * Panel. + * + * @namespace M.core.dock + * @class Panel + * @constructor + * @extends Y.Base + * @uses Y.EventTarget + */ +var DOCKPANEL = function() { + DOCKPANEL.superclass.constructor.apply(this, arguments); +}; +DOCKPANEL.prototype = { + /** + * True once the panel has been created. + * @property created + * @protected + * @type {Boolean} + */ + created : false, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + /** + * Fired before the panel is shown. + * @event dockpane::beforeshow + */ + this.publish('dockpanel:beforeshow', {prefix:'dockpanel'}); + /** + * Fired after the panel is shown. + * @event dockpanel:shown + */ + this.publish('dockpanel:shown', {prefix:'dockpanel'}); + /** + * Fired before the panel is hidden. + * @event dockpane::beforehide + */ + this.publish('dockpanel:beforehide', {prefix:'dockpanel'}); + /** + * Fired after the panel is hidden. + * @event dockpanel:hidden + */ + this.publish('dockpanel:hidden', {prefix:'dockpanel'}); + /** + * Fired when ever the dock panel is either hidden or shown. + * Always fired after the shown or hidden events. + * @event dockpanel:visiblechange + */ + this.publish('dockpanel:visiblechange', {prefix:'dockpanel'}); + }, + /** + * Creates the Panel if it has not already been created. + * @method create + * @return {Boolean} + */ + create : function() { + if (this.created) { + return true; + } + this.created = true; + var dock = this.get('dock'), + node = dock.get('dockNode'); + this.set('node', Y.Node.create('
    ')); + this.set('contentNode', Y.Node.create('
    ')); + this.set('headerNode', Y.Node.create('
    ')); + this.set('bodyNode', Y.Node.create('
    ')); + node.append( + this.get('node').append(this.get('contentNode').append(this.get('headerNode')).append(this.get('bodyNode'))) + ); + }, + /** + * Displays the panel. + * @method show + */ + show : function() { + this.create(); + this.fire('dockpanel:beforeshow'); + this.set('visible', true); + this.get('node').removeClass('dockitempanel_hidden'); + this.fire('dockpanel:shown'); + this.fire('dockpanel:visiblechange'); + }, + /** + * Hides the panel + * @method hide + */ + hide : function() { + this.fire('dockpanel:beforehide'); + this.set('visible', false); + this.get('node').addClass('dockitempanel_hidden'); + this.fire('dockpanel:hidden'); + this.fire('dockpanel:visiblechange'); + }, + /** + * Sets the panel header. + * @method setHeader + * @param {Node|String} content + */ + setHeader : function(content) { + this.create(); + var header = this.get('headerNode'); + header.setContent(content); + if (arguments.length > 1) { + for (var i=1;i < arguments.length;i++) { + header.append(arguments[i]); + } + } + }, + /** + * Sets the panel body. + * @method setBody + * @param {Node|String} content + */ + setBody : function(content) { + this.create(); + this.get('bodyNode').setContent(content); + }, + /** + * Sets the new top mark of the panel. + * + * @method setTop + * @param {Number} newtop + */ + setTop : function(newtop) { + if (Y.UA.ie > 0 && Y.UA.ie < 7) { + this.get('node').setY(newtop); + } else { + this.get('node').setStyle('top', newtop.toString()+'px'); + } + }, + /** + * Corrects the width of the panel. + * @method correctWidth + */ + correctWidth : function() { + var bodyNode = this.get('bodyNode'), + // Width of content. + width = bodyNode.get('clientWidth'), + // Scrollable width of content. + scroll = bodyNode.get('scrollWidth'), + // Width of content container with overflow. + offsetWidth = bodyNode.get('offsetWidth'), + // The new width - defaults to the current width. + newWidth = width, + // The max width (80% of screen). + maxWidth = Math.round(bodyNode.get('winWidth') * 0.8); + + // If the scrollable width is more than the visible width + if (scroll > width) { + // Content width + // + the difference + // + any rendering difference (borders, padding) + // + 10px to make it look nice. + newWidth = width + (scroll - width) + ((offsetWidth - width)*2) + 10; + } + + // Make sure its not more then the maxwidth + if (newWidth > maxWidth) { + newWidth = maxWidth; + } + + // Set the new width if its more than the old width. + if (newWidth > offsetWidth) { + this.get('node').setStyle('width', newWidth+'px'); + } + } +}; +Y.extend(DOCKPANEL, Y.Base, DOCKPANEL.prototype, { + NAME : 'moodle-core-dock-panel', + ATTRS : { + /** + * The dock itself. + * @attribute dock + * @type DOCK + * @writeonce + */ + dock : { + writeOnce : 'initOnly' + }, + /** + * The node that contains the whole panel. + * @attribute node + * @type Node + */ + node : { + value : null + }, + /** + * The node that contains the header, body and footer. + * @attribute contentNode + * @type Node + */ + contentNode : { + value : null + }, + /** + * The node that contains the header + * @attribute headerNode + * @type Node + */ + headerNode : { + value : null + }, + /** + * The node that contains the body + * @attribute bodyNode + * @type Node + */ + bodyNode : { + value : null + }, + /** + * True if the panel is currently visible. + * @attribute visible + * @type Boolean + */ + visible : { + value : false + } + } +}); +Y.augment(DOCKPANEL, Y.EventTarget); +/** + * Dock JS. + * + * This file contains the tab height manager. + * The tab height manager is responsible for ensure all tabs are visible all the time. + * + * @module moodle-core-dock + */ + +/** + * Tab height manager. + * + * @namespace M.core.dock + * @class TabHeightManager + * @constructor + * @extends Y.Base + */ +var TABHEIGHTMANAGER = function() { + TABHEIGHTMANAGER.superclass.constructor.apply(this, arguments); +}; +TABHEIGHTMANAGER.prototype = { + /** + * Initialises the dock sizer which then attaches itself to the required + * events in order to monitor the dock + * @method initializer + */ + initializer : function() { + var dock = this.get('dock'); + dock.on('dock:itemschanged', this.checkSizing, this); + Y.on('windowresize', this.checkSizing, this); + }, + /** + * Check if the size dock items needs to be adjusted + * @method checkSizing + */ + checkSizing : function() { + var dock = this.get('dock'), + node = dock.get('dockNode'), + items = dock.dockeditems, + possibleheight = node.get('offsetHeight') - node.one('.controls').get('offsetHeight') - (dock.get('bufferPanel')*3) - (items.length*2), + totalheight = 0, + id, dockedtitle; + if (items.length > 0) { + for (id in items) { + dockedtitle = Y.one(items[id].get('title')).ancestor('.'+CSS.dockedtitle); + if (dockedtitle) { + if (this.get('enabled')) { + dockedtitle.setStyle('height', 'auto'); + } + totalheight += dockedtitle.get('offsetHeight') || 0; + } + } + if (totalheight > possibleheight) { + this.enable(possibleheight); + } + } + }, + /** + * Enables the dock sizer and resizes where required. + * @method enable + * @param {Number} possibleheight + */ + enable : function(possibleheight) { + var dock = this.get('dock'), + items = dock.dockeditems, + count = dock.count, + runningcount = 0, + usedheight = 0, + id, itemtitle, itemheight, offsetheight; + this.set('enabled', true); + for (id in items) { + itemtitle = Y.one(items[id].get('title')).ancestor('.'+CSS.dockedtitle); + if (!itemtitle) { + continue; + } + itemheight = Math.floor((possibleheight-usedheight) / (count - runningcount)); + offsetheight = itemtitle.get('offsetHeight'); + itemtitle.setStyle('overflow', 'hidden'); + if (offsetheight > itemheight) { + itemtitle.setStyle('height', itemheight+'px'); + usedheight += itemheight; + } else { + usedheight += offsetheight; + } + runningcount++; + } + } +}; +Y.extend(TABHEIGHTMANAGER, Y.Base, TABHEIGHTMANAGER.prototype, { + NAME : 'moodle-core-tabheightmanager', + ATTRS : { + /** + * The dock. + * @attribute dock + * @type DOCK + * @writeOnce + */ + dock : { + writeOnce : 'initOnly' + }, + /** + * True if the item_sizer is being used, false otherwise. + * @attribute enabled + * @type Bool + */ + enabled : { + value : false + } + } +}); +/** + * Dock JS. + * + * This file contains the action key event definition that is used for accessibility handling within the Dock. + * + * @module moodle-core-dock + */ + +/** + * A 'dock:actionkey' Event. + * The event consists of the left arrow, right arrow, enter and space keys. + * More keys can be mapped to action meanings. + * actions: collapse , expand, toggle, enter. + * + * This event is subscribed to by dockitems. + * The on() method to subscribe allows specifying the desired trigger actions as JSON. + * + * This event can also be delegated if needed. + * + * @namespace M.core.dock + * @class ActionKey + */ +Y.Event.define("dock:actionkey", { + // Webkit and IE repeat keydown when you hold down arrow keys. + // Opera links keypress to page scroll; others keydown. + // Firefox prevents page scroll via preventDefault() on either + // keydown or keypress. + _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress', + + /** + * The keys to trigger on. + * @method _keys + */ + _keys: { + //arrows + '37': 'collapse', + '39': 'expand', + //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings) + '32': 'toggle', + '13': 'enter' + }, + + /** + * Handles key events + * @method _keyHandler + * @param {EventFacade} e + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {Object} args + */ + _keyHandler: function (e, notifier, args) { + var actObj; + if (!args.actions) { + actObj = {collapse:true, expand:true, toggle:true, enter:true}; + } else { + actObj = args.actions; + } + if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) { + e.action = this._keys[e.keyCode]; + notifier.fire(e); + } + }, + + /** + * Subscribes to events. + * @method on + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + */ + on: function (node, sub, notifier) { + // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). + if (sub.args === null) { + //no actions given + sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false}); + } else { + sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]); + } + }, + + /** + * Detaches an event listener + * @method detach + */ + detach: function (node, sub, notifier) { + //detach our _detacher handle of the subscription made in on() + sub._detacher.detach(); + }, + + /** + * Creates a delegated event listener. + * @method delegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ + delegate: function (node, sub, notifier, filter) { + // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). + if (sub.args === null) { + //no actions given + sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false}); + } else { + sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]); + } + }, + + /** + * Detaches a delegated event listener. + * @method detachDelegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ + detachDelegate: function (node, sub, notifier, filter) { + sub._delegateDetacher.detach(); + } +}); +/** + * Dock JS. + * + * This file contains the block class used to manage blocks (both docked and not) for the dock. + * + * @module moodle-core-dock + */ + +/** + * Block. + * + * @namespace M.core.dock + * @class Block + * @constructor + * @extends Y.Base + */ +var BLOCK = function() { + BLOCK.superclass.constructor.apply(this, arguments); +}; +BLOCK.prototype = { + /** + * A content place holder used when the block has been docked. + * @property contentplaceholder + * @protected + * @type Node + */ + contentplaceholder : null, + /** + * The skip link associated with this block. + * @property contentskipanchor + * @protected + * @type Node + */ + contentskipanchor : null, + /** + * The cached content node for the actual block + * @property cachedcontentnode + * @protected + * @type Node + */ + cachedcontentnode : null, + /** + * If true the user preference isn't updated + * @property skipsetposition + * @protected + * @type Boolean + */ + skipsetposition : true, + /** + * The dock item associated with this block + * @property dockitem + * @protected + * @type DOCKITEM + */ + dockitem : null, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + var node = Y.one('#inst'+this.get('id')); + if (!node) { + return false; + } + + + M.core.dock.ensureMoveToIconExists(node); + + // Move the block straight to the dock if required + if (node.hasClass(CSS.dockonload)) { + node.removeClass(CSS.dockonload); + var commands = node.one('.header .title .commands'); + if (!commands) { + commands = Y.Node.create('
    '); + if (node.one('.header .title')) { + node.one('.header .title').append(commands); + } + } + this.moveToDock(null, commands); + } + this.skipsetposition = false; + return true; + }, + /** + * Returns the class associated with this block. + * @method _getBlockClass + * @private + * @param {Node} node + * @return String + */ + _getBlockClass : function(node) { + var classes = node.getAttribute('className').toString(), + regex = /(^|\s)(block_[a-zA-Z0-9_]+)(\s|$)/, + matches = regex.exec(classes); + if (matches) { + return matches[2]; + } + return matches; + }, + + /** + * This function is reponsible for moving a block from the page structure onto the dock. + * @method moveToDock + * @param {EventFacade} e + */ + moveToDock : function(e) { + if (e) { + e.halt(true); + } + + var dock = M.core.dock.get(), + id = this.get('id'), + blockcontent = Y.one('#inst'+id).one('.content'); + + if (!blockcontent) { + return; + } + + + var icon = (right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block', + char = (location.href.match(/\?/)) ? '&' : '?', + blocktitle, + blockcommands, + movetoimg, + moveto; + + this.recordBlockState(); + + blocktitle = this.cachedcontentnode.one('.title h2').cloneNode(true); + blockcommands = this.cachedcontentnode.one('.title .commands').cloneNode(true); + + // Must set the image src seperatly of we get an error with XML strict headers + movetoimg = Y.Node.create(''+Y.Escape.html(M.str.block.undockitem)+''); + movetoimg.setAttribute('src', M.util.image_url(icon, 'moodle')); + moveto = Y.Node.create('').append(movetoimg); + moveto.set('href', location.href + char + 'dock='+id); + blockcommands.append(moveto); + + // Create a new dock item for the block + this.dockitem = new DOCKEDITEM({ + block : this, + dock : dock, + blockinstanceid : id, + title : blocktitle, + contents : blockcontent, + commands : blockcommands, + blockclass : this._getBlockClass(Y.one('#inst'+id)) + }); + // Register an event so that when it is removed we can put it back as a block + dock.add(this.dockitem); + + if (!this.skipsetposition) { + // save the users preference + M.util.set_user_preference('docked_block_instance_'+id, 1); + } + + this.set('idDocked', true); + }, + /** + * Records the block state and adds it to the docks holding area. + * @method recordBlockState + */ + recordBlockState : function() { + var id = this.get('id'), + dock = M.core.dock.get(), + node = Y.one('#inst'+id), + skipanchor = node.previous(); + // Disable the skip anchor when docking + if (skipanchor.hasClass('skip-block')) { + this.contentskipanchor = skipanchor; + this.contentskipanchor.hide(); + } + this.cachedcontentnode = node; + this.contentplaceholder = Y.Node.create('
    '); + node.replace(this.contentplaceholder); + dock.addToHoldingArea(node); + node = null; + if (!this.cachedcontentnode.one('.title .commands')) { + this.cachedcontentnode.one('.title').append(Y.Node.create('
    ')); + } + }, + + /** + * This function removes a block from the dock and puts it back into the page structure. + * @method returnToBlock + * @return {Boolean} + */ + returnToBlock : function() { + var id = this.get('id'); + + + // Enable the skip anchor when going back to block mode + if (this.contentskipanchor) { + this.contentskipanchor.show(); + } + + if (this.cachedcontentnode.one('.header')) { + this.cachedcontentnode.one('.header').insert(this.dockitem.get('contents'), 'after'); + } else { + this.cachedcontentnode.insert(this.dockitem.get('contents')); + } + + this.contentplaceholder.replace(this.cachedcontentnode); + this.cachedcontentnode = Y.one('#'+this.cachedcontentnode.get('id')); + + var commands = this.dockitem.get('commands'); + if (commands) { + commands.all('.hidepanelicon').remove(); + commands.all('.moveto').remove(); + commands.remove(); + } + this.cachedcontentnode.one('.title').append(commands); + this.cachedcontentnode = null; + M.util.set_user_preference('docked_block_instance_'+id, 0); + this.set('idDocked', false); + return true; + } +}; +Y.extend(BLOCK, Y.Base, BLOCK.prototype, { + NAME : 'moodle-core-dock-block', + ATTRS : { + /** + * The block instance ID + * @attribute id + * @writeOnce + * @type Number + */ + id : { + writeOnce : 'initOnly', + setter : function(value) { + return parseInt(value, 10); + } + }, + /** + * True if the block has been docked. + * @attribute isDocked + * @default false + * @type Boolean + */ + isDocked : { + value : false + } + } +}); +/** + * Dock JS. + * + * This file contains the docked item class. + * + * @module moodle-core-dock + */ + +/** + * Docked item. + * + * @namespace M.core.dock + * @class DockedItem + * @constructor + * @extends Y.Base + * @uses Y.EventTarget + */ +var DOCKEDITEM = function() { + DOCKEDITEM.superclass.constructor.apply(this, arguments); +}; +DOCKEDITEM.prototype = { + /** + * Set to true if this item is currently being displayed. + * @property active + * @protected + * @type Boolean + */ + active : false, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + var title = this.get('title'), + titlestring; + /** + * Fired before the docked item has been drawn. + * @event dockeditem:drawstart + */ + this.publish('dockeditem:drawstart', {prefix:'dockeditem'}); + /** + * Fired after the docked item has been drawn. + * @event dockeditem:drawcomplete + */ + this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'}); + /** + * Fired before the docked item is to be shown. + * @event dockeditem:showstart + */ + this.publish('dockeditem:showstart', {prefix:'dockeditem'}); + /** + * Fired after the docked item has been shown. + * @event dockeditem:showcomplete + */ + this.publish('dockeditem:showcomplete', {prefix:'dockeditem'}); + /** + * Fired before the docked item has been hidden. + * @event dockeditem:hidestart + */ + this.publish('dockeditem:hidestart', {prefix:'dockeditem'}); + /** + * Fired after the docked item has been hidden. + * @event dockeditem:hidecomplete + */ + this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'}); + /** + * Fired when the docked item is removed from the dock. + * @event dockeditem:itemremoved + */ + this.publish('dockeditem:itemremoved', {prefix:'dockeditem'}); + if (title) { + var type = title.get('nodeName'); + titlestring = title.cloneNode(true); + title = Y.Node.create('<'+type+'>'); + title = M.core.dock.fixTitleOrientation(title, titlestring.get('text')); + this.set('title', title); + this.set('titlestring', titlestring); + } + }, + /** + * This function draws the item on the dock. + * @method draw + * @return Boolean + */ + draw : function() { + var create = Y.Node.create, + dock = this.get('dock'), + count = dock.count, + docktitle, + dockitem, + closeicon, + closeiconimg, + id = this.get('id'); + + this.fire('dockeditem:drawstart'); + + docktitle = create(''); + docktitle.append(this.get('title')); + dockitem = create('
    '); + if (count === 1) { + dockitem.addClass('firstdockitem'); + } + dockitem.append(docktitle); + dock.append(dockitem); + + closeiconimg = create(''+M.str.block.hidepanel+''); + closeiconimg.setAttribute('src', M.util.image_url('t/dockclose', 'moodle')); + closeicon = create('').append(closeiconimg); + closeicon.on('forceclose|click', this.hide, this); + closeicon.on('dock:actionkey',this.hide, this, {actions:{enter:true,toggle:true}}); + this.get('commands').append(closeicon); + + this.set('dockTitleNode', docktitle); + this.set('dockItemNode', dockitem); + + this.fire('dockeditem:drawcomplete'); + return true; + }, + /** + * This function toggles makes the item active and shows it. + * @method show + * @return Boolean + */ + show : function() { + var dock = this.get('dock'), + panel = dock.getPanel(), + docktitle = this.get('dockTitleNode'); + + dock.hideActive(); + this.fire('dockeditem:showstart'); + panel.setHeader(this.get('titlestring'), this.get('commands')); + panel.setBody(Y.Node.create('
    ').append(this.get('contents'))); + panel.show(); + panel.correctWidth(); + + this.active = true; + // Add active item class first up + docktitle.addClass(CSS.activeitem); + // Set aria-exapanded property to true. + docktitle.set('aria-expanded', "true"); + this.fire('dockeditem:showcomplete'); + dock.resize(); + return true; + }, + /** + * This function hides the item and makes it inactive. + * @method hide + */ + hide : function() { + this.fire('dockeditem:hidestart'); + if (this.active) { + // No longer active + this.active = false; + // Hide the panel + this.get('dock').getPanel().hide(); + } + // Remove the active class + // Set aria-exapanded property to false + this.get('dockTitleNode').removeClass(CSS.activeitem).set('aria-expanded', "false"); + this.fire('dockeditem:hidecomplete'); + }, + /** + * A toggle between calling show and hide functions based on css.activeitem + * Applies rules to key press events (dock:actionkey) + * @method toggle + * @param {String} action + */ + toggle : function(action) { + var docktitle = this.get('dockTitleNode'); + if (docktitle.hasClass(CSS.activeitem) && action !== 'expand') { + this.hide(); + } else if (!docktitle.hasClass(CSS.activeitem) && action !== 'collapse') { + this.show(); + } + }, + /** + * This function removes the node and destroys it's bits. + * @method remove. + */ + remove : function () { + this.hide(); + // Return the block to its original position. + this.get('block').returnToBlock(); + // Remove the dock item node. + this.get('dockItemNode').remove(); + this.fire('dockeditem:itemremoved'); + }, + /** + * Returns the description of this item to use for log calls. + * @method _getLogDescription + * @private + * @return {String} + */ + _getLogDescription : function() { + return this.get('titlestring').get('innerHTML')+' ('+this.get('blockinstanceid')+')'; + } +}; +Y.extend(DOCKEDITEM, Y.Base, DOCKEDITEM.prototype, { + NAME : 'moodle-core-dock-dockeditem', + ATTRS : { + /** + * The block this docked item is associated with. + * @attribute block + * @type BLOCK + * @writeOnce + * @required + */ + block : { + writeOnce : 'initOnly' + }, + /** + * The dock itself. + * @attribute dock + * @type DOCK + * @writeOnce + * @required + */ + dock : { + writeOnce : 'initOnly' + }, + /** + * The docked item ID. This will be given by the dock. + * @attribute id + * @type Number + */ + id : {}, + /** + * Block instance id.Taken from the associated block. + * @attribute blockinstanceid + * @type Number + * @writeOnce + */ + blockinstanceid : { + writeOnce : 'initOnly', + setter : function(value) { + return parseInt(value, 10); + } + }, + /** + * The title nodeof the docked item. + * @attribute title + * @type Node + * @default null + */ + title : { + value : null + }, + /** + * The title string. + * @attribute titlestring + * @type String + */ + titlestring : { + value : null + }, + /** + * The contents of the docked item + * @attribute contents + * @type Node + * @writeOnce + * @required + */ + contents : { + writeOnce : 'initOnly' + }, + /** + * Commands associated with the block. + * @attribute commands + * @type Node + * @writeOnce + * @required + */ + commands : { + writeOnce : 'initOnly' + }, + /** + * The block class. + * @attribute blockclass + * @type String + * @writeOnce + * @required + */ + blockclass : { + writeOnce : 'initOnly' + }, + /** + * The title node for the docked block. + * @attribute dockTitleNode + * @type Node + */ + dockTitleNode : { + value : null + }, + /** + * The item node for the docked block. + * @attribute dockItemNode + * @type Node + */ + dockItemNode : { + value : null + }, + /** + * The container for the docked item (will contain the block contents when visible) + * @attribute dockcontainerNode + * @type Node + */ + dockcontainerNode : { + value : null + } + } +}); +Y.augment(DOCKEDITEM, Y.EventTarget); + +}, '@VERSION@', { + "requires": [ + "base", + "node", + "event-custom", + "event-mouseenter", + "event-resize", + "escape", + "moodle-core-dockloader" + ] +}); diff --git a/lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-debug.js b/lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-debug.js new file mode 100644 index 00000000000..959ee46e58c --- /dev/null +++ b/lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-debug.js @@ -0,0 +1,126 @@ +YUI.add('moodle-core-dockloader', function (Y, NAME) { + +var LOADERNAME = 'moodle-core-dock-loader'; + +/** + * Core namespace. + * + * @static + * @namespace M + * @class core + */ +M.core = M.core || {}; + +/** + * Dock namespace. + * + * @static + * @namespace M.core + * @class dock + */ +M.core.dock = M.core.dock || {}; + +/** + * Creates the move to dock icon for dockable blocks if it doesn't already exist. + * + * @static + * @method ensureMoveToIconExists + * @param {Node} blocknode The Blocks node (.block[data-instanceid]) + */ +M.core.dock.ensureMoveToIconExists = function(blocknode) { + if (blocknode.one('.moveto')) { + return true; + } + + var commands, + moveto = Y.Node.create(''), + blockaction = blocknode.one('.block_action'), + icon = 't/block_to_dock'; + + // Must set the image src seperatly of we get an error with XML strict headers + if (right_to_left()) { + icon = icon + '_rtl'; + } + moveto.setAttribute('alt', M.util.get_string('addtodock', 'block')); + moveto.setAttribute('title', Y.Escape.html(M.util.get_string('dockblock', 'block', blocknode.one('.header .title h2').getHTML()))); + moveto.setAttribute('src', M.util.image_url(icon, 'moodle')); + + if (blockaction) { + blockaction.prepend(moveto); + } else { + commands = blocknode.one('.header .title .commands'); + if (!commands && blocknode.one('.header .title')) { + commands = Y.Node.create('
    '); + blocknode.one('.header .title').append(commands); + } + commands.append(moveto); + } + return true; +}; + +/** + * Dock loader. + * + * The dock loader is repsponsible for loading and initialising the dock only when required. + * By doing this we avoid the need to load unnecessary JavaScript into the page for the dock just incase + * it is being used. + * + * @static + * @namespace M.core.dock + * @class Loader + */ +M.core.dock.loader = M.core.dock.loader || {}; + +/** + * Delegation events + * @property delegationEvents + * @protected + * @type {Array} + */ +M.core.dock.loader.delegationEvents = []; + +/** + * Initialises the dock loader. + * + * The dock loader works by either firing the dock immediately if there are already docked blocks. + * Or if there are not any docked blocks delegating two events and then loading and firing the dock when one of + * those delegated events is triggered. + * + * @method initLoader + */ +M.core.dock.loader.initLoader = function() { + Y.log('Dock loader initialising', 'note', LOADERNAME); + var dockedblocks = Y.all('.block[data-instanceid][data-dockable]'), + body = Y.one(document.body); + dockedblocks.each(function() { + var id = parseInt(this.getData('instanceid'), 10); + Y.log('Dock loader watching block with instance id: '+id, 'note', LOADERNAME); + M.core.dock.ensureMoveToIconExists(this); + }); + if (dockedblocks.some(function(node){return node.hasClass('dock_on_load');})) { + Y.log('Loading dock module', 'note', LOADERNAME); + Y.use('moodle-core-dock', function() { + M.core.dock.init(); + }); + } else { + var callback = function(e) { + var i, + block = this.ancestor('.block[data-instanceid]'), + instanceid = block.getData('instanceid'); + e.halt(); + for (i in M.core.dock.loader.delegationEvents) { + M.core.dock.loader.delegationEvents[i].detach(); + } + block.addClass('dock_on_load'); + Y.log('Loading dock module', 'note', LOADERNAME); + Y.use('moodle-core-dock', function(){ + M.util.set_user_preference('docked_block_instance_'+instanceid, 1); + M.core.dock.init(); + }); + }; + M.core.dock.loader.delegationEvents.push(body.delegate('click', callback, '.moveto')); + M.core.dock.loader.delegationEvents.push(body.delegate('key', callback, '.moveto', 'enter')); + } +}; + +}, '@VERSION@', {"requires": ["escape"]}); diff --git a/lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-min.js b/lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-min.js new file mode 100644 index 00000000000..b0dd7c4bc12 --- /dev/null +++ b/lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-min.js @@ -0,0 +1 @@ +YUI.add("moodle-core-dockloader",function(e,t){var n="moodle-core-dock-loader";M.core=M.core||{},M.core.dock=M.core.dock||{},M.core.dock.ensureMoveToIconExists=function(t){if(t.one(".moveto"))return!0;var n,r=e.Node.create(''),i=t.one(".block_action"),s="t/block_to_dock";return right_to_left()&&(s+="_rtl"),r.setAttribute("alt",M.util.get_string("addtodock","block")),r.setAttribute("title",e.Escape.html(M.util.get_string("dockblock","block",t.one(".header .title h2").getHTML()))),r.setAttribute("src",M.util.image_url(s,"moodle")),i?i.prepend(r):(n=t.one(".header .title .commands"),!n&&t.one(".header .title")&&(n=e.Node.create('
    '),t.one(".header .title").append(n)),n.append(r)),!0},M.core.dock.loader=M.core.dock.loader||{},M.core.dock.loader.delegationEvents=[],M.core.dock.loader.initLoader=function(){var t=e.all(".block[data-instanceid][data-dockable]"),n=e.one(document.body);t.each(function(){var e=parseInt(this.getData("instanceid"),10);M.core.dock.ensureMoveToIconExists(this)});if(t.some(function(e){return e.hasClass("dock_on_load")}))e.use("moodle-core-dock",function(){M.core.dock.init()});else{var r=function(t){var n,r=this.ancestor(".block[data-instanceid]"),i=r.getData("instanceid");t.halt();for(n in M.core.dock.loader.delegationEvents)M.core.dock.loader.delegationEvents[n].detach();r.addClass("dock_on_load"),e.use("moodle-core-dock",function(){M.util.set_user_preference("docked_block_instance_"+i,1),M.core.dock.init()})};M.core.dock.loader.delegationEvents.push(n.delegate("click",r,".moveto")),M.core.dock.loader.delegationEvents.push(n.delegate("key",r,".moveto","enter"))}}},"@VERSION@",{requires:["escape"]}); diff --git a/lib/yui/build/moodle-core-dockloader/moodle-core-dockloader.js b/lib/yui/build/moodle-core-dockloader/moodle-core-dockloader.js new file mode 100644 index 00000000000..184bcf4066e --- /dev/null +++ b/lib/yui/build/moodle-core-dockloader/moodle-core-dockloader.js @@ -0,0 +1,122 @@ +YUI.add('moodle-core-dockloader', function (Y, NAME) { + +var LOADERNAME = 'moodle-core-dock-loader'; + +/** + * Core namespace. + * + * @static + * @namespace M + * @class core + */ +M.core = M.core || {}; + +/** + * Dock namespace. + * + * @static + * @namespace M.core + * @class dock + */ +M.core.dock = M.core.dock || {}; + +/** + * Creates the move to dock icon for dockable blocks if it doesn't already exist. + * + * @static + * @method ensureMoveToIconExists + * @param {Node} blocknode The Blocks node (.block[data-instanceid]) + */ +M.core.dock.ensureMoveToIconExists = function(blocknode) { + if (blocknode.one('.moveto')) { + return true; + } + + var commands, + moveto = Y.Node.create(''), + blockaction = blocknode.one('.block_action'), + icon = 't/block_to_dock'; + + // Must set the image src seperatly of we get an error with XML strict headers + if (right_to_left()) { + icon = icon + '_rtl'; + } + moveto.setAttribute('alt', M.util.get_string('addtodock', 'block')); + moveto.setAttribute('title', Y.Escape.html(M.util.get_string('dockblock', 'block', blocknode.one('.header .title h2').getHTML()))); + moveto.setAttribute('src', M.util.image_url(icon, 'moodle')); + + if (blockaction) { + blockaction.prepend(moveto); + } else { + commands = blocknode.one('.header .title .commands'); + if (!commands && blocknode.one('.header .title')) { + commands = Y.Node.create('
    '); + blocknode.one('.header .title').append(commands); + } + commands.append(moveto); + } + return true; +}; + +/** + * Dock loader. + * + * The dock loader is repsponsible for loading and initialising the dock only when required. + * By doing this we avoid the need to load unnecessary JavaScript into the page for the dock just incase + * it is being used. + * + * @static + * @namespace M.core.dock + * @class Loader + */ +M.core.dock.loader = M.core.dock.loader || {}; + +/** + * Delegation events + * @property delegationEvents + * @protected + * @type {Array} + */ +M.core.dock.loader.delegationEvents = []; + +/** + * Initialises the dock loader. + * + * The dock loader works by either firing the dock immediately if there are already docked blocks. + * Or if there are not any docked blocks delegating two events and then loading and firing the dock when one of + * those delegated events is triggered. + * + * @method initLoader + */ +M.core.dock.loader.initLoader = function() { + var dockedblocks = Y.all('.block[data-instanceid][data-dockable]'), + body = Y.one(document.body); + dockedblocks.each(function() { + var id = parseInt(this.getData('instanceid'), 10); + M.core.dock.ensureMoveToIconExists(this); + }); + if (dockedblocks.some(function(node){return node.hasClass('dock_on_load');})) { + Y.use('moodle-core-dock', function() { + M.core.dock.init(); + }); + } else { + var callback = function(e) { + var i, + block = this.ancestor('.block[data-instanceid]'), + instanceid = block.getData('instanceid'); + e.halt(); + for (i in M.core.dock.loader.delegationEvents) { + M.core.dock.loader.delegationEvents[i].detach(); + } + block.addClass('dock_on_load'); + Y.use('moodle-core-dock', function(){ + M.util.set_user_preference('docked_block_instance_'+instanceid, 1); + M.core.dock.init(); + }); + }; + M.core.dock.loader.delegationEvents.push(body.delegate('click', callback, '.moveto')); + M.core.dock.loader.delegationEvents.push(body.delegate('key', callback, '.moveto', 'enter')); + } +}; + +}, '@VERSION@', {"requires": ["escape"]}); diff --git a/lib/yui/src/dock/build.json b/lib/yui/src/dock/build.json new file mode 100644 index 00000000000..e76c5b1c8e8 --- /dev/null +++ b/lib/yui/src/dock/build.json @@ -0,0 +1,20 @@ +{ + "name": "moodle-core-dock", + "builds": { + "moodle-core-dock": { + "jsfiles": [ + "dock.js", + "panel.js", + "tabheightmanager.js", + "actionkey.js", + "block.js", + "dockeditem.js" + ] + }, + "moodle-core-dockloader": { + "jsfiles": [ + "dockloader.js" + ] + } + } +} diff --git a/lib/yui/src/dock/js/actionkey.js b/lib/yui/src/dock/js/actionkey.js new file mode 100644 index 00000000000..76dcd48654c --- /dev/null +++ b/lib/yui/src/dock/js/actionkey.js @@ -0,0 +1,118 @@ +/** + * Dock JS. + * + * This file contains the action key event definition that is used for accessibility handling within the Dock. + * + * @module moodle-core-dock + */ + +/** + * A 'dock:actionkey' Event. + * The event consists of the left arrow, right arrow, enter and space keys. + * More keys can be mapped to action meanings. + * actions: collapse , expand, toggle, enter. + * + * This event is subscribed to by dockitems. + * The on() method to subscribe allows specifying the desired trigger actions as JSON. + * + * This event can also be delegated if needed. + * + * @namespace M.core.dock + * @class ActionKey + */ +Y.Event.define("dock:actionkey", { + // Webkit and IE repeat keydown when you hold down arrow keys. + // Opera links keypress to page scroll; others keydown. + // Firefox prevents page scroll via preventDefault() on either + // keydown or keypress. + _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress', + + /** + * The keys to trigger on. + * @method _keys + */ + _keys: { + //arrows + '37': 'collapse', + '39': 'expand', + //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings) + '32': 'toggle', + '13': 'enter' + }, + + /** + * Handles key events + * @method _keyHandler + * @param {EventFacade} e + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {Object} args + */ + _keyHandler: function (e, notifier, args) { + var actObj; + if (!args.actions) { + actObj = {collapse:true, expand:true, toggle:true, enter:true}; + } else { + actObj = args.actions; + } + if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) { + e.action = this._keys[e.keyCode]; + notifier.fire(e); + } + }, + + /** + * Subscribes to events. + * @method on + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + */ + on: function (node, sub, notifier) { + // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). + if (sub.args === null) { + //no actions given + sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false}); + } else { + sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]); + } + }, + + /** + * Detaches an event listener + * @method detach + */ + detach: function (node, sub, notifier) { + //detach our _detacher handle of the subscription made in on() + sub._detacher.detach(); + }, + + /** + * Creates a delegated event listener. + * @method delegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ + delegate: function (node, sub, notifier, filter) { + // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). + if (sub.args === null) { + //no actions given + sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false}); + } else { + sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]); + } + }, + + /** + * Detaches a delegated event listener. + * @method detachDelegate + * @param {Node} node The node this subscription was applied to. + * @param {Subscription} sub The object tracking this subscription. + * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers + * @param {String|function} filter Selector string or function that accpets an event object and returns null. + */ + detachDelegate: function (node, sub, notifier, filter) { + sub._delegateDetacher.detach(); + } +}); diff --git a/lib/yui/src/dock/js/block.js b/lib/yui/src/dock/js/block.js new file mode 100644 index 00000000000..42a51e9f03f --- /dev/null +++ b/lib/yui/src/dock/js/block.js @@ -0,0 +1,248 @@ +/** + * Dock JS. + * + * This file contains the block class used to manage blocks (both docked and not) for the dock. + * + * @module moodle-core-dock + */ + +/** + * Block. + * + * @namespace M.core.dock + * @class Block + * @constructor + * @extends Y.Base + */ +var BLOCK = function() { + BLOCK.superclass.constructor.apply(this, arguments); +}; +BLOCK.prototype = { + /** + * A content place holder used when the block has been docked. + * @property contentplaceholder + * @protected + * @type Node + */ + contentplaceholder : null, + /** + * The skip link associated with this block. + * @property contentskipanchor + * @protected + * @type Node + */ + contentskipanchor : null, + /** + * The cached content node for the actual block + * @property cachedcontentnode + * @protected + * @type Node + */ + cachedcontentnode : null, + /** + * If true the user preference isn't updated + * @property skipsetposition + * @protected + * @type Boolean + */ + skipsetposition : true, + /** + * The dock item associated with this block + * @property dockitem + * @protected + * @type DOCKITEM + */ + dockitem : null, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + var node = Y.one('#inst'+this.get('id')); + if (!node) { + return false; + } + + Y.log('Initialised block with instance id:'+this.get('id'), 'note', LOGNS); + + M.core.dock.ensureMoveToIconExists(node); + + // Move the block straight to the dock if required + if (node.hasClass(CSS.dockonload)) { + node.removeClass(CSS.dockonload); + var commands = node.one('.header .title .commands'); + if (!commands) { + commands = Y.Node.create('
    '); + if (node.one('.header .title')) { + node.one('.header .title').append(commands); + } + } + this.moveToDock(null, commands); + } + this.skipsetposition = false; + return true; + }, + /** + * Returns the class associated with this block. + * @method _getBlockClass + * @private + * @param {Node} node + * @return String + */ + _getBlockClass : function(node) { + var classes = node.getAttribute('className').toString(), + regex = /(^|\s)(block_[a-zA-Z0-9_]+)(\s|$)/, + matches = regex.exec(classes); + if (matches) { + return matches[2]; + } + return matches; + }, + + /** + * This function is reponsible for moving a block from the page structure onto the dock. + * @method moveToDock + * @param {EventFacade} e + */ + moveToDock : function(e) { + if (e) { + e.halt(true); + } + + var dock = M.core.dock.get(), + id = this.get('id'), + blockcontent = Y.one('#inst'+id).one('.content'); + + if (!blockcontent) { + return; + } + + Y.log('Moving block to the dock:'+this.get('id'), 'note', LOGNS); + + var icon = (right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block', + char = (location.href.match(/\?/)) ? '&' : '?', + blocktitle, + blockcommands, + movetoimg, + moveto; + + this.recordBlockState(); + + blocktitle = this.cachedcontentnode.one('.title h2').cloneNode(true); + blockcommands = this.cachedcontentnode.one('.title .commands').cloneNode(true); + + // Must set the image src seperatly of we get an error with XML strict headers + movetoimg = Y.Node.create(''+Y.Escape.html(M.str.block.undockitem)+''); + movetoimg.setAttribute('src', M.util.image_url(icon, 'moodle')); + moveto = Y.Node.create('').append(movetoimg); + moveto.set('href', location.href + char + 'dock='+id); + blockcommands.append(moveto); + + // Create a new dock item for the block + this.dockitem = new DOCKEDITEM({ + block : this, + dock : dock, + blockinstanceid : id, + title : blocktitle, + contents : blockcontent, + commands : blockcommands, + blockclass : this._getBlockClass(Y.one('#inst'+id)) + }); + // Register an event so that when it is removed we can put it back as a block + dock.add(this.dockitem); + + if (!this.skipsetposition) { + // save the users preference + M.util.set_user_preference('docked_block_instance_'+id, 1); + } + + this.set('idDocked', true); + }, + /** + * Records the block state and adds it to the docks holding area. + * @method recordBlockState + */ + recordBlockState : function() { + var id = this.get('id'), + dock = M.core.dock.get(), + node = Y.one('#inst'+id), + skipanchor = node.previous(); + // Disable the skip anchor when docking + if (skipanchor.hasClass('skip-block')) { + this.contentskipanchor = skipanchor; + this.contentskipanchor.hide(); + } + this.cachedcontentnode = node; + this.contentplaceholder = Y.Node.create('
    '); + node.replace(this.contentplaceholder); + dock.addToHoldingArea(node); + node = null; + if (!this.cachedcontentnode.one('.title .commands')) { + this.cachedcontentnode.one('.title').append(Y.Node.create('
    ')); + } + }, + + /** + * This function removes a block from the dock and puts it back into the page structure. + * @method returnToBlock + * @return {Boolean} + */ + returnToBlock : function() { + var id = this.get('id'); + + Y.log('Moving block out of the dock:'+this.get('id'), 'note', LOGNS); + + // Enable the skip anchor when going back to block mode + if (this.contentskipanchor) { + this.contentskipanchor.show(); + } + + if (this.cachedcontentnode.one('.header')) { + this.cachedcontentnode.one('.header').insert(this.dockitem.get('contents'), 'after'); + } else { + this.cachedcontentnode.insert(this.dockitem.get('contents')); + } + + this.contentplaceholder.replace(this.cachedcontentnode); + this.cachedcontentnode = Y.one('#'+this.cachedcontentnode.get('id')); + + var commands = this.dockitem.get('commands'); + if (commands) { + commands.all('.hidepanelicon').remove(); + commands.all('.moveto').remove(); + commands.remove(); + } + this.cachedcontentnode.one('.title').append(commands); + this.cachedcontentnode = null; + M.util.set_user_preference('docked_block_instance_'+id, 0); + this.set('idDocked', false); + return true; + } +}; +Y.extend(BLOCK, Y.Base, BLOCK.prototype, { + NAME : 'moodle-core-dock-block', + ATTRS : { + /** + * The block instance ID + * @attribute id + * @writeOnce + * @type Number + */ + id : { + writeOnce : 'initOnly', + setter : function(value) { + return parseInt(value, 10); + } + }, + /** + * True if the block has been docked. + * @attribute isDocked + * @default false + * @type Boolean + */ + isDocked : { + value : false + } + } +}); diff --git a/lib/yui/src/dock/js/dock.js b/lib/yui/src/dock/js/dock.js new file mode 100644 index 00000000000..26419d4baf1 --- /dev/null +++ b/lib/yui/src/dock/js/dock.js @@ -0,0 +1,1077 @@ +/** + * Dock JS. + * + * This file contains the DOCK object and all dock related global namespace methods and properties. + * + * @module moodle-core-dock + */ + + +var LOGNS = 'moodle-core-dock'; +var BODY = Y.one(document.body); +var CSS = { + dock: 'dock', // CSS Class applied to the dock box + dockspacer: 'dockspacer', // CSS class applied to the dockspacer + controls: 'controls', // CSS class applied to the controls box + body: 'has_dock', // CSS class added to the body when there is a dock + buttonscontainer: 'buttons_container', + dockeditem: 'dockeditem', // CSS class added to each item in the dock + dockeditemcontainer: 'dockeditem_container', + dockedtitle: 'dockedtitle', // CSS class added to the item's title in each dock + activeitem: 'activeitem', // CSS class added to the active item + contentonly: 'content-only', + dockonload: 'dock_on_load' +}; +var SELECTOR = { + dockableblock: '.block[data-instanceid][data-dockable]', + blockmoveto: '.block[data-instanceid][data-dockable] .moveto', + panelmoveto: '#dockeditempanel .commands a.moveto', + dockonload: '.block.'+CSS.dockonload, + blockregion: '[data-blockregion]' +}; + +/** + * Core namespace. + * + * @static + * @namespace M + * @class core + */ +M.core = M.core || {}; + +/** + * Dock namespace. + * + * @static + * @namespace M.core + * @class dock + */ +M.core.dock = M.core.dock || {}; + +/** + * The dock - once initialised. + * + * @private + * @property _dock + * @type DOCK + */ +M.core.dock._dock = null; + +/** + * An associative array of dockable blocks. + * @property _dockableblocks + * @type {Array} An array of BLOCK objects organised by instanceid. + * @private + */ +M.core.dock._dockableblocks = {}; + +/** + * Initialises the dock. + * This method registers dockable blocks, and creates delegations to dock them. + * @static + * @method init + */ +M.core.dock.init = function() { + Y.all(SELECTOR.dockableblock).each(M.core.dock.registerDockableBlock); + BODY.delegate('click', M.core.dock.dockBlock, SELECTOR.blockmoveto); + BODY.delegate('key', M.core.dock.dockBlock, SELECTOR.blockmoveto, 'enter'); +}; + +/** + * Returns an instance of the dock. + * Initialises one if one hasn't already being initialised. + * + * @static + * @method get + * @return DOCK + */ +M.core.dock.get = function() { + if (this._dock === null) { + this._dock = new DOCK(); + } + return this._dock; +}; + +/** + * Registers a dockable block with the dock. + * + * @static + * @method registerDockableBlock + * @param {int} id The block instance ID. + * @return void + */ +M.core.dock.registerDockableBlock = function(id) { + if (typeof id === 'object' && typeof id.getData === 'function') { + id = id.getData('instanceid'); + } + M.core.dock._dockableblocks[id] = new BLOCK({id : id}); +}; + +/** + * Docks a block given either its instanceid, its node, or an event fired from within the block. + * @static + * @method dockBlockByInstanceID + * @param id + * @return void + */ +M.core.dock.dockBlock = function(id) { + if (typeof id === 'object' && id.target !== 'undefined') { + id = id.target; + } + if (typeof id === "object") { + if (!id.test(SELECTOR.dockableblock)) { + id = id.ancestor(SELECTOR.dockableblock); + } + if (typeof id === 'object' && typeof id.getData === 'function' && !id.ancestor('.'+CSS.dock)) { + id = id.getData('instanceid'); + } else { + Y.log('Invalid instanceid given to dockBlockByInstanceID', 'warn', LOADERNAME); + return; + } + } + var block = M.core.dock._dockableblocks[id]; + if (block) { + block.moveToDock(); + } +}; + +/** + * Fixes the title orientation. Rotating it if required. + * + * @static + * @method fixTitleOrientation + * @param {Node} title The title node we are looking at. + * @param {String} text The string to use as the title. + * @return {Node} The title node to use. + */ +M.core.dock.fixTitleOrientation = function(title, text) { + var dock = M.core.dock.get(); + title = Y.one(title); + + if (dock.get('orientation') !== 'vertical') { + // If the dock isn't vertical don't adjust it! + title.setContent(text); + return title; + } + + if (Y.UA.ie > 0 && Y.UA.ie < 8) { + // IE 6/7 can't rotate text so force ver + M.str.langconfig.thisdirectionvertical = 'ver'; + } + + var clockwise = false; + switch (M.str.langconfig.thisdirectionvertical) { + case 'ver': + // Stacked is easy + return title.setContent(text.split('').join('
    ')); + case 'ttb': + clockwise = true; + break; + case 'btt': + clockwise = false; + break; + } + + if (Y.UA.ie === 8) { + // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute. + title.setContent(text); + title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;'); + title.addClass('filterrotate'); + return title; + } + + // We need to fix a font-size - sorry theme designers. + var fontsize = '11px'; + var transform = (clockwise) ? 'rotate(90deg)' : 'rotate(270deg)'; + var test = Y.Node.create('

    '+text+'

    '); + BODY.insert(test, 0); + var width = test.one('span').get('offsetWidth') * 1.2; + var height = test.one('span').get('offsetHeight'); + test.remove(); + + title.setContent(text); + title.addClass('css3transform'); + + // Move the title into position + title.setStyles({ + 'margin' : '0', + 'padding' : '0', + 'position' : 'relative', + 'fontSize' : fontsize, + 'width' : width, + 'top' : width/2 + }); + + // Positioning is different when in RTL mode. + if (right_to_left()) { + title.setStyle('left', width/2 - height); + } else { + title.setStyle('right', width/2 - height); + } + + // Rotate the text + title.setStyles({ + 'transform' : transform, + '-ms-transform' : transform, + '-moz-transform' : transform, + '-webkit-transform' : transform, + '-o-transform' : transform + }); + + var container = Y.Node.create('
    '); + container.append(title); + container.setStyle('height', width + (width / 4)); + container.setStyle('position', 'relative'); + return container; +}; + +/** + * Informs the dock that the content of the block has changed. + * This should be called by the blocks JS code if its content has been updated dynamically. + * This method ensure the dock resizes if need be. + * + * @static + * @method notifyBlockChange + * @param {Number} instanceid + * @return void + */ +M.core.dock.notifyBlockChange = function(instanceid) { + if (this._dock !== null) { + var dock = M.core.dock.get(), + activeitem = dock.getActiveItem(); + if (activeitem && activeitem.get('blockinstanceid') === parseInt(instanceid, 10)) { + dock.resizePanelIfRequired(); + } + } +}; + +/** + * The Dock. + * + * @namespace M.core.dock + * @class Dock + * @constructor + * @extends Y.Base + * @uses Y.EventTarget + */ +var DOCK = function() { + DOCK.superclass.constructor.apply(this, arguments); +}; +DOCK.prototype = { + /** + * Tab height manager used to ensure tabs are always visible. + * @protected + * @property tabheightmanager + * @type TABHEIGHTMANAGER + */ + tabheightmanager : null, + /** + * Will be an eventtype if there is an eventype to prevent. + * @protected + * @property preventevent + * @type String + */ + preventevent : null, + /** + * Will be an object if there is a delayed event in effect. + * @protected + * @property delayedevent + * @type {Object} + */ + delayedevent : null, + /** + * An array of currently docked items. + * @protected + * @property dockeditems + * @type Array + */ + dockeditems : [], + /** + * Set to true once the dock has been drawn. + * @protected + * @property dockdrawn + * @type Boolean + */ + dockdrawn : false, + /** + * The number of blocks that are currently docked. + * @protected + * @property count + * @type Number + */ + count : 0, + /** + * The total number of blocks that have been docked. + * @protected + * @property totalcount + * @type Number + */ + totalcount : 0, + /** + * A hidden node used as a holding area for DOM objects used by blocks that have been docked. + * @protected + * @property holdingareanode + * @type Node + */ + holdingareanode : null, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + Y.log('Dock initialising', 'note', LOGNS); + + // Publish the events the dock has + /** + * Fired when the dock first starts initialising. + * @event dock:starting + */ + this.publish('dock:starting', {prefix: 'dock',broadcast: 2,emitFacade: true, fireOnce:true}); + /** + * Fired after the dock is initialised for the first time. + * @event dock:initialised + */ + this.publish('dock:initialised', {prefix: 'dock',broadcast: 2,emitFacade: true, fireOnce:true}); + /** + * Fired before the dock structure and content is first created. + * @event dock:beforedraw + */ + this.publish('dock:beforedraw', {prefix:'dock', fireOnce:true}); + /** + * Fired before the dock is changed from hidden to visible. + * @event dock:beforeshow + */ + this.publish('dock:beforeshow', {prefix:'dock'}); + /** + * Fires after the dock has been changed from hidden to visible. + * @event dock:shown + */ + this.publish('dock:shown', {prefix:'dock'}); + /** + * Fired after the dock has been changed from visible to hidden. + * @event dock:hidden + */ + this.publish('dock:hidden', {prefix:'dock'}); + /** + * Fires when an item is added to the dock. + * @event dock:itemadded + */ + this.publish('dock:itemadded', {prefix:'dock'}); + /** + * Fires when an item is removed from the dock. + * @event dock:itemremoved + */ + this.publish('dock:itemremoved', {prefix:'dock'}); + /** + * Fires when a block is added or removed from the dock. + * This happens after the itemadded and itemremoved events have been called. + * @event dock:itemschanged + */ + this.publish('dock:itemschanged', {prefix:'dock'}); + /** + * Fires once when the docks panel is first initialised. + * @event dock:panelgenerated + */ + this.publish('dock:panelgenerated', {prefix:'dock', fireOnce:true}); + /** + * Fires when the dock panel is about to be resized. + * @event dock:panelresizestart + */ + this.publish('dock:panelresizestart', {prefix:'dock'}); + /** + * Fires after the dock panel has been resized. + * @event dock:resizepanelcomplete + */ + this.publish('dock:resizepanelcomplete', {prefix:'dock'}); + + // Apply theme customisations here before we do any real work. + this._applyThemeCustomisation(); + // Inform everyone we are now about to initialise. + this.fire('dock:starting'); + this._ensureDockDrawn(); + // Inform everyone the dock has been initialised + this.fire('dock:initialised'); + }, + /** + * Ensures that the dock has been drawn. + * @private + * @method _ensureDockDrawn + * @return {Boolean} + */ + _ensureDockDrawn : function() { + if (this.dockdrawn === true) { + return true; + } + var dock = this._initialiseDockNode(); + if (Y.UA.ie > 0 && Y.UA.ie < 7) { + // Adjust for IE 6 (can't handle fixed pos) + dock.setStyle('height', dock.get('winHeight')+'px'); + } + + this.fire('dock:beforedraw'); + + this._initialiseDockControls(); + + this.tabheightmanager = new TABHEIGHTMANAGER({dock:this}); + + var clickargs = {cssselector:'.'+CSS.dockedtitle, delay:0}; + var mouseenterargs = {cssselector:'.'+CSS.dockedtitle, delay:0.5, iscontained:true, preventevent:'click', preventdelay:3}; + + // Attach the required event listeners + // We use delegate here as that way a handful of events are created for the dock + // and all items rather than the same number for the dock AND every item individually + Y.delegate('click', this.handleEvent, this.get('dockNode'), '.'+CSS.dockedtitle, this, clickargs); + Y.delegate('mouseenter', this.handleEvent, this.get('dockNode'), '.'+CSS.dockedtitle, this, mouseenterargs); + this.get('dockNode').on('mouseleave', this.handleEvent, this, {cssselector:'#dock', delay:0.5, iscontained:false}); + + Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this); + Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this); + Y.delegate('dock:actionkey', this.handleDockedItemEvent, this.get('dockNode'), '.'+CSS.dockeditem, this); + + BODY.on('click', this.handleEvent, this, {cssselector:'body', delay:0}); + this.on('dock:itemschanged', this.resizeBlockSpace, this); + this.on('dock:itemschanged', this.checkDockVisibility, this); + this.on('dock:itemschanged', this.resetFirstItem, this); + this.dockdrawn = true; + return true; + }, + /** + * Handles an actionkey event on the dock. + * @param {EventFacade} e + * @method handleDockedItemEvent + * @return {Boolean} + */ + handleDockedItemEvent : function(e) { + if (e.type !== 'dock:actionkey') { + return false; + } + var target = e.target, + dockeditem = '.'+CSS.dockeditem; + if (!target.test(dockeditem)) { + target = target.ancestor(dockeditem); + } + if (!target) { + return false; + } + e.halt(); + var id = target.getAttribute('rel'); + this.dockeditems[id].toggle(e.action); + }, + /** + * Call the theme customisation method "customise_dock_for_theme" if it exists. + * @private + * @method _applyThemeCustomisation + */ + _applyThemeCustomisation : function() { + // Check if there is a customisation function + if (typeof(customise_dock_for_theme) === 'function') { + // First up pre the legacy object. + M.core_dock = this; + M.core_dock.cfg = { + buffer : null, + orientation : null, + position : null, + spacebeforefirstitem : null, + removeallicon : null + }; + M.core_dock.css = { + dock : null, + dockspacer : null, + controls : null, + body : null, + buttonscontainer : null, + dockeditem : null, + dockeditemcontainer : null, + dockedtitle : null, + activeitem : null + }; + try { + // Run the customisation function + customise_dock_for_theme(this); + } catch (exception) { + // Do nothing at the moment. + Y.log('Exception while attempting to apply theme customisations.', 'error', LOGNS); + } + // Now to work out what they did. + var key, value, + warned = false, + cfgmap = { + buffer : 'bufferPanel', + orientation : 'orientation', + position : 'position', + spacebeforefirstitem : 'bufferBeforeFirstItem', + removeallicon : 'undockAllIconUrl' + }; + // Check for and apply any legacy configuration. + for (key in M.core_dock.cfg) { + value = M.core_dock.cfg[key]; + if (value === null) { + continue; + } + if (!warned) { + Y.log('Warning: customise_dock_for_theme has changed. Please update your code.', 'warn', LOGNS); + warned = true; + } + // Damn, the've set something. + Y.log('Note for customise_dock_for_theme code: M.core_dock.cfg.'+key+' is now dock.set(\''+key+'\', value)', 'info', LOGNS); + this.set(cfgmap[key], value); + } + // Check for and apply any legacy CSS changes.. + for (key in M.core_dock.css) { + value = M.core_dock.css[key]; + if (value === null) { + continue; + } + if (!warned) { + Y.log('Warning: customise_dock_for_theme has changed. Please update your code.', 'warn', LOGNS); + warned = true; + } + // Damn, they've set something. + Y.log('Note for customise_dock_for_theme code: M.core_dock.css.'+key+' is now CSS.'+key+' = value', 'info', LOGNS); + CSS[key] = value; + } + } + }, + /** + * Initialises the dock node, creating it and its content if required. + * + * @private + * @method _initialiseDockNode + * @return {Node} The dockNode + */ + _initialiseDockNode : function() { + var dock = this.get('dockNode'), + positionorientationclass = CSS.dock+'_'+this.get('position')+'_'+this.get('orientation'), + holdingarea = Y.Node.create('
    ').setStyles({display:'none'}), + buttons = this.get('buttonsNode'), + container = this.get('itemContainerNode'); + + if (!dock) { + dock = Y.one('#'+CSS.dock); + } + if (!dock) { + dock = Y.Node.create('
    '); + BODY.append(dock); + } + dock.setAttribute('role', 'menubar').addClass(positionorientationclass); + if (Y.all(SELECTOR.dockonload).size() === 0) { + // Nothing on the dock... hide it using CSS + dock.addClass('nothingdocked'); + } else { + positionorientationclass = CSS.body+'_'+this.get('position')+'_'+this.get('orientation'); + BODY.addClass(CSS.body).addClass(); + } + + if (!buttons) { + buttons = dock.one('.'+CSS.buttonscontainer); + } + if (!buttons) { + buttons = Y.Node.create('
    '); + dock.append(buttons); + } + + if (!container) { + container = dock.one('.'+CSS.dockeditemcontainer); + } + if (!container) { + container = Y.Node.create('
    '); + buttons.append(container); + } + + BODY.append(holdingarea); + this.holdingareanode = holdingarea; + + this.set('dockNode', dock); + this.set('buttonsNode', buttons); + this.set('itemContainerNode', container); + + return dock; + }, + /** + * Initialises the dock controls. + * + * @private + * @method _initialiseDockControls + */ + _initialiseDockControls : function() { + // Add a removeall button + // Must set the image src seperatly of we get an error with XML strict headers + + var removeall = Y.Node.create(''+M.util.get_string('undockall', 'block')+''); + removeall.setAttribute('src',this.get('undockAllIconUrl')); + removeall.on('removeall|click', this.removeAll, this); + removeall.on('dock:actionkey', this.removeAll, this, {actions:{enter:true}}); + this.get('buttonsNode').append(Y.Node.create('
    ').append(removeall)); + }, + /** + * Returns the dock panel. Initialising it if it hasn't already been initialised. + * @method getPanel + * @return {DOCKPANEL} + */ + getPanel : function() { + var panel = this.get('panel'); + if (!panel) { + panel = new DOCKPANEL({dock:this}); + panel.on('panel:visiblechange', this.resize, this); + Y.on('windowresize', this.resize, this); + // Initialise the dockpanel .. should only happen once + this.set('panel', panel); + this.fire('dock:panelgenerated'); + } + return panel; + }, + /** + * Resizes the dock panel if required. + * @method resizePanelIfRequired + */ + resizePanelIfRequired : function() { + this.resize(); + var panel = this.get('panel'); + if (panel) { + panel.correctWidth(); + } + }, + /** + * Handles a dock event sending it to the right place. + * + * @method handleEvent + * @param {EventFacade} e + * @param {Object} options + * @return {Boolean} + */ + handleEvent : function(e, options) { + var item = this.getActiveItem(); + if (options.cssselector === 'body') { + if (!this.get('dockNode').contains(e.target)) { + if (item) { + item.hide(); + } + } + } else { + var target; + if (e.target.test(options.cssselector)) { + target = e.target; + } else { + target = e.target.ancestor(options.cssselector); + } + if (!target) { + return true; + } + if (this.preventevent !== null && e.type === this.preventevent) { + return true; + } + if (options.preventevent) { + this.preventevent = options.preventevent; + if (options.preventdelay) { + var self = this; + setTimeout(function(){ + self.preventevent = null; + }, options.preventdelay * 1000); + } + } + if (this.delayedevent && this.delayedevent.timeout) { + clearTimeout(this.delayedevent.timeout); + this.delayedevent.event.detach(); + this.delayedevent = null; + } + if (options.delay > 0) { + return this.delayEvent(e, options, target); + } + var targetid = target.get('id'); + var regex = /^dock_item_(\d+)_title$/; + if (targetid.match(regex)) { + item = this.dockeditems[targetid.replace(regex, '$1')]; + if (item.active) { + item.hide(); + } else { + item.show(); + } + } else if (item) { + item.hide(); + } + } + return true; + }, + /** + * Delays an event. + * + * @method delayEvent + * @param {EventFacade} event + * @param {Object} options + * @param {Node} target + * @return {Boolean} + */ + delayEvent : function(event, options, target) { + var self = this; + self.delayedevent = (function(){ + return { + target : target, + event : BODY.on('mousemove', function(e){ + self.delayedevent.target = e.target; + }), + timeout : null + }; + })(self); + self.delayedevent.timeout = setTimeout(function(){ + self.delayedevent.timeout = null; + self.delayedevent.event.detach(); + if (options.iscontained === self.get('dockNode').contains(self.delayedevent.target)) { + self.handleEvent(event, {cssselector:options.cssselector, delay:0, iscontained:options.iscontained}); + } + }, options.delay*1000); + return true; + }, + /** + * Resizes block spaces. + * @method resizeBlockSpace + */ + resizeBlockSpace : function() { + if (Y.all(SELECTOR.dockonload).size() > 0) { + // Do not resize during initial load + return; + } + var blockregions = [], + populatedblockregions = 0, + allnewregions = true, + showregions = false; + // First look for understood regions. + Y.all(SELECTOR.blockregion).each(function(){ + var regionname = region.getData('blockregion'); + if (region.all('.block').size() > 0) { + populatedblockregions++; + BODY.addClass('used-region-'+regionname); + BODY.removeClass('empty-region-'+regionname); + } else { + BODY.addClass('empty-region-'+regionname); + BODY.removeClass('used-region-'+regionname); + } + }); + // Next check for legacy regions. + Y.all('.block-region').each(function(region){ + if (region.test(SELECTOR.blockregion)) { + // This is a new region, we've already processed it. + return; + } + var hasblocks = (region.all('.block').size() > 0); + if (hasblocks) { + populatedblockregions++; + } + allnewregions = false; + blockregions[region.get('id')] = { + hasblocks : hasblocks, + bodyclass : region.get('id').replace(/^region\-/, 'side-')+'-only' + }; + }); + if (BODY.hasClass('blocks-moving')) { + // open up blocks during blocks positioning + showregions = true; + } + if (populatedblockregions === 0 && showregions === false) { + BODY.addClass(CSS.contentonly); + } else { + BODY.removeClass(CSS.contentonly); + } + + if (!allnewregions) { + var i = null; + if (populatedblockregions === 0 && showregions === false) { + for (i in blockregions) { + BODY.removeClass(blockregions[i].bodyclass); + } + } else if (populatedblockregions === 1 && showregions === false) { + for (i in blockregions) { + if (!blockregions[i].hasblocks) { + BODY.removeClass(blockregions[i].bodyclass); + } else { + BODY.addClass(blockregions[i].bodyclass); + } + } + } else { + for (i in blockregions) { + BODY.removeClass(blockregions[i].bodyclass); + } + } + } + }, + /** + * Adds an item to the dock. + * @method add + * @param {DOCKEDITEM} item + */ + add : function(item) { + // Set the dockitem id to the total count and then increment it. + item.set('id', this.totalcount); + Y.log('Adding block '+item._getLogDescription()+' to the dock.', 'info', LOGNS); + this.count++; + this.totalcount++; + this.dockeditems[item.get('id')] = item; + this.dockeditems[item.get('id')].draw(); + this.fire('dock:itemadded', item); + this.fire('dock:itemschanged', item); + }, + /** + * Appends an item to the dock (putting it in the item container. + * @method append + * @param {Node} docknode + */ + append : function(docknode) { + this.get('itemContainerNode').append(docknode); + }, + /** + * Handles events that require a docked block to be returned to the page./ + * @method handleReturnToBlock + * @param {EventFacade} e + */ + handleReturnToBlock : function(e) { + e.halt(); + this.remove(this.getActiveItem().get('id')); + }, + /** + * Removes a docked item from the dock. + * @method remove + * @param {Number} id The docked item id. + * @return {Boolean} + */ + remove : function(id) { + if (!this.dockeditems[id]) { + return false; + } + Y.log('Removing block '+this.dockeditems[id]._getLogDescription()+' from the dock.', 'info', LOGNS); + this.dockeditems[id].remove(); + delete this.dockeditems[id]; + this.count--; + this.fire('dock:itemremoved', id); + this.fire('dock:itemschanged', id); + return true; + }, + /** + * Ensures the the first item in the dock has the correct class. + * @method resetFirstItem + */ + resetFirstItem : function() { + this.get('dockNode').all('.'+CSS.dockeditem+'.firstdockitem').removeClass('firstdockitem'); + if (this.get('dockNode').one('.'+CSS.dockeditem)) { + this.get('dockNode').one('.'+CSS.dockeditem).addClass('firstdockitem'); + } + }, + /** + * Removes all docked blocks returning them to the page. + * @method removeAll + * @return {Boolean} + */ + removeAll : function() { + Y.log('Undocking all '+this.dockeditems.length+' blocks', 'note', LOGNS); + for (var i in this.dockeditems) { + this.remove(i); + } + return true; + }, + /** + * Hides the active item. + * @method hideActive + */ + hideActive : function() { + var item = this.getActiveItem(); + if (item) { + item.hide(); + } + }, + /** + * Checks wether the dock should be shown or hidden + * @method checkDockVisibility + */ + checkDockVisibility : function() { + var bodyclass = CSS.body+'_'+this.get('position')+'_'+this.get('orientation'); + if (!this.count) { + this.get('dockNode').addClass('nothingdocked'); + BODY.removeClass(CSS.body).removeClass(); + this.fire('dock:hidden'); + } else { + this.fire('dock:beforeshow'); + this.get('dockNode').removeClass('nothingdocked'); + BODY.addClass(CSS.body).addClass(bodyclass); + this.fire('dock:shown'); + } + }, + /** + * This function checks the size and position of the panel and moves/resizes if + * required to keep it within the bounds of the window. + * @method resize + * @return {Boolean} + */ + resize : function() { + var panel = this.getPanel(); + var item = this.getActiveItem(); + if (!panel.get('visible') || !item) { + return true; + } + + this.fire('dock:panelresizestart'); + if (this.get('orientation') === 'vertical') { + var buffer = this.get('bufferPanel'); + var screenheight = parseInt(BODY.get('winHeight'), 10)-(buffer*2); + var docky = this.get('dockNode').getY(); + var titletop = item.get('dockTitleNode').getY()-docky-buffer; + var containery = this.get('itemContainerNode').getY(); + var containerheight = containery-docky+this.get('buttonsNode').get('offsetHeight'); + var scrolltop = panel.get('bodyNode').get('scrollTop'); + panel.get('bodyNode').setStyle('height', 'auto'); + panel.get('node').removeClass('oversized_content'); + var panelheight = panel.get('node').get('offsetHeight'); + + if (Y.UA.ie > 0 && Y.UA.ie < 7) { + panel.setTop(item.get('dockTitleNode').getY()); + } else if (panelheight > screenheight) { + panel.setTop(buffer-containerheight); + panel.get('bodyNode').setStyle('height', (screenheight-panel.get('headerNode').get('offsetHeight'))+'px'); + panel.get('node').addClass('oversized_content'); + } else if (panelheight > (screenheight-(titletop-buffer))) { + var difference = panelheight - (screenheight-titletop); + panel.setTop(titletop-containerheight-difference+buffer); + } else { + panel.setTop(titletop-containerheight+buffer); + } + + if (scrolltop) { + panel.get('bodyNode').set('scrollTop', scrolltop); + } + } + + if (this.get('position') === 'right') { + panel.get('node').setStyle('left', -panel.get('offsetWidth')+'px'); + + } else if (this.get('position') === 'top') { + var dockx = this.get('dockNode').getX(); + var titleleft = item.get('dockTitleNode').getX()-dockx; + panel.get('node').setStyle('left', titleleft+'px'); + } + + this.fire('dock:resizepanelcomplete'); + return true; + }, + /** + * Returns the currently active dock item or false + * @method getActiveItem + * @return {DOCKEDITEM} + */ + getActiveItem : function() { + for (var i in this.dockeditems) { + if (this.dockeditems[i].active) { + return this.dockeditems[i]; + } + } + return false; + }, + /** + * Adds an item to the holding area. + * @method addToHoldingArea + * @param {Node} node + */ + addToHoldingArea : function(node) { + this.holdingareanode.append(node); + } +}; + +Y.extend(DOCK, Y.Base, DOCK.prototype, { + NAME : 'moodle-core-dock', + ATTRS : { + /** + * The dock itself. #dock. + * @attribute dockNode + * @type Node + * @writeOnce + */ + dockNode : { + writeOnce : true + }, + /** + * The docks panel. + * @attribute panel + * @type DOCKPANEL + * @writeOnce + */ + panel : { + writeOnce : true + }, + /** + * A container within the dock used for buttons. + * @attribute buttonsNode + * @type Node + * @writeOnce + */ + buttonsNode : { + writeOnce : true + }, + /** + * A container within the dock used for docked blocks. + * @attribute itemContainerNode + * @type Node + * @writeOnce + */ + itemContainerNode : { + writeOnce : true + }, + + /** + * Buffer used when containing a panel. + * @attribute bufferPanel + * @type Number + * @default 10 + */ + bufferPanel : { + value : 10, + validator : Y.Lang.isNumber + }, + + /** + * Position of the dock. + * @attribute position + * @type String + * @default left + */ + position : { + value : 'left', + validator : Y.Lang.isString + }, + + /** + * vertical || horizontal determines if we change the title + * @attribute orientation + * @type String + * @default vertical + */ + orientation : { + value : 'vertical', + validator : Y.Lang.isString, + setter : function(value) { + if (value.match(/^vertical$/i)) { + return 'vertical'; + } + return 'horizontal'; + } + }, + + /** + * Space between the top of the dock and the first item. + * @attribute bufferBeforeFirstItem + * @type Number + * @default 10 + */ + bufferBeforeFirstItem : { + value : 10, + validator : Y.Lang.isNumber + }, + + /** + * Icon URL for the icon to undock all blocks + * @attribute undockAllIconUrl + * @type String + * @default t/dock_to_block + */ + undockAllIconUrl : { + value : M.util.image_url('t/dock_to_block', 'moodle'), + validator : Y.Lang.isString + } + } +}); +Y.augment(DOCK, Y.EventTarget); diff --git a/lib/yui/src/dock/js/dockeditem.js b/lib/yui/src/dock/js/dockeditem.js new file mode 100644 index 00000000000..ba929677464 --- /dev/null +++ b/lib/yui/src/dock/js/dockeditem.js @@ -0,0 +1,315 @@ +/** + * Dock JS. + * + * This file contains the docked item class. + * + * @module moodle-core-dock + */ + +/** + * Docked item. + * + * @namespace M.core.dock + * @class DockedItem + * @constructor + * @extends Y.Base + * @uses Y.EventTarget + */ +var DOCKEDITEM = function() { + DOCKEDITEM.superclass.constructor.apply(this, arguments); +}; +DOCKEDITEM.prototype = { + /** + * Set to true if this item is currently being displayed. + * @property active + * @protected + * @type Boolean + */ + active : false, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + var title = this.get('title'), + titlestring; + /** + * Fired before the docked item has been drawn. + * @event dockeditem:drawstart + */ + this.publish('dockeditem:drawstart', {prefix:'dockeditem'}); + /** + * Fired after the docked item has been drawn. + * @event dockeditem:drawcomplete + */ + this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'}); + /** + * Fired before the docked item is to be shown. + * @event dockeditem:showstart + */ + this.publish('dockeditem:showstart', {prefix:'dockeditem'}); + /** + * Fired after the docked item has been shown. + * @event dockeditem:showcomplete + */ + this.publish('dockeditem:showcomplete', {prefix:'dockeditem'}); + /** + * Fired before the docked item has been hidden. + * @event dockeditem:hidestart + */ + this.publish('dockeditem:hidestart', {prefix:'dockeditem'}); + /** + * Fired after the docked item has been hidden. + * @event dockeditem:hidecomplete + */ + this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'}); + /** + * Fired when the docked item is removed from the dock. + * @event dockeditem:itemremoved + */ + this.publish('dockeditem:itemremoved', {prefix:'dockeditem'}); + if (title) { + var type = title.get('nodeName'); + titlestring = title.cloneNode(true); + title = Y.Node.create('<'+type+'>'); + title = M.core.dock.fixTitleOrientation(title, titlestring.get('text')); + this.set('title', title); + this.set('titlestring', titlestring); + } + Y.log('Initialised dockeditem for block with title "'+this._getLogDescription(), 'note', LOGNS); + }, + /** + * This function draws the item on the dock. + * @method draw + * @return Boolean + */ + draw : function() { + var create = Y.Node.create, + dock = this.get('dock'), + count = dock.count, + docktitle, + dockitem, + closeicon, + closeiconimg, + id = this.get('id'); + + this.fire('dockeditem:drawstart'); + + docktitle = create(''); + docktitle.append(this.get('title')); + dockitem = create('
    '); + if (count === 1) { + dockitem.addClass('firstdockitem'); + } + dockitem.append(docktitle); + dock.append(dockitem); + + closeiconimg = create(''+M.str.block.hidepanel+''); + closeiconimg.setAttribute('src', M.util.image_url('t/dockclose', 'moodle')); + closeicon = create('').append(closeiconimg); + closeicon.on('forceclose|click', this.hide, this); + closeicon.on('dock:actionkey',this.hide, this, {actions:{enter:true,toggle:true}}); + this.get('commands').append(closeicon); + + this.set('dockTitleNode', docktitle); + this.set('dockItemNode', dockitem); + + this.fire('dockeditem:drawcomplete'); + return true; + }, + /** + * This function toggles makes the item active and shows it. + * @method show + * @return Boolean + */ + show : function() { + var dock = this.get('dock'), + panel = dock.getPanel(), + docktitle = this.get('dockTitleNode'); + + dock.hideActive(); + this.fire('dockeditem:showstart'); + Y.log('Showing '+this._getLogDescription(), 'info', LOGNS); + panel.setHeader(this.get('titlestring'), this.get('commands')); + panel.setBody(Y.Node.create('
    ').append(this.get('contents'))); + panel.show(); + panel.correctWidth(); + + this.active = true; + // Add active item class first up + docktitle.addClass(CSS.activeitem); + // Set aria-exapanded property to true. + docktitle.set('aria-expanded', "true"); + this.fire('dockeditem:showcomplete'); + dock.resize(); + return true; + }, + /** + * This function hides the item and makes it inactive. + * @method hide + */ + hide : function() { + this.fire('dockeditem:hidestart'); + Y.log('Hiding "'+this._getLogDescription(), 'info', LOGNS); + if (this.active) { + // No longer active + this.active = false; + // Hide the panel + this.get('dock').getPanel().hide(); + } + // Remove the active class + // Set aria-exapanded property to false + this.get('dockTitleNode').removeClass(CSS.activeitem).set('aria-expanded', "false"); + this.fire('dockeditem:hidecomplete'); + }, + /** + * A toggle between calling show and hide functions based on css.activeitem + * Applies rules to key press events (dock:actionkey) + * @method toggle + * @param {String} action + */ + toggle : function(action) { + var docktitle = this.get('dockTitleNode'); + if (docktitle.hasClass(CSS.activeitem) && action !== 'expand') { + this.hide(); + } else if (!docktitle.hasClass(CSS.activeitem) && action !== 'collapse') { + this.show(); + } + }, + /** + * This function removes the node and destroys it's bits. + * @method remove. + */ + remove : function () { + this.hide(); + // Return the block to its original position. + this.get('block').returnToBlock(); + // Remove the dock item node. + this.get('dockItemNode').remove(); + this.fire('dockeditem:itemremoved'); + }, + /** + * Returns the description of this item to use for log calls. + * @method _getLogDescription + * @private + * @return {String} + */ + _getLogDescription : function() { + return this.get('titlestring').get('innerHTML')+' ('+this.get('blockinstanceid')+')'; + } +}; +Y.extend(DOCKEDITEM, Y.Base, DOCKEDITEM.prototype, { + NAME : 'moodle-core-dock-dockeditem', + ATTRS : { + /** + * The block this docked item is associated with. + * @attribute block + * @type BLOCK + * @writeOnce + * @required + */ + block : { + writeOnce : 'initOnly' + }, + /** + * The dock itself. + * @attribute dock + * @type DOCK + * @writeOnce + * @required + */ + dock : { + writeOnce : 'initOnly' + }, + /** + * The docked item ID. This will be given by the dock. + * @attribute id + * @type Number + */ + id : {}, + /** + * Block instance id.Taken from the associated block. + * @attribute blockinstanceid + * @type Number + * @writeOnce + */ + blockinstanceid : { + writeOnce : 'initOnly', + setter : function(value) { + return parseInt(value, 10); + } + }, + /** + * The title nodeof the docked item. + * @attribute title + * @type Node + * @default null + */ + title : { + value : null + }, + /** + * The title string. + * @attribute titlestring + * @type String + */ + titlestring : { + value : null + }, + /** + * The contents of the docked item + * @attribute contents + * @type Node + * @writeOnce + * @required + */ + contents : { + writeOnce : 'initOnly' + }, + /** + * Commands associated with the block. + * @attribute commands + * @type Node + * @writeOnce + * @required + */ + commands : { + writeOnce : 'initOnly' + }, + /** + * The block class. + * @attribute blockclass + * @type String + * @writeOnce + * @required + */ + blockclass : { + writeOnce : 'initOnly' + }, + /** + * The title node for the docked block. + * @attribute dockTitleNode + * @type Node + */ + dockTitleNode : { + value : null + }, + /** + * The item node for the docked block. + * @attribute dockItemNode + * @type Node + */ + dockItemNode : { + value : null + }, + /** + * The container for the docked item (will contain the block contents when visible) + * @attribute dockcontainerNode + * @type Node + */ + dockcontainerNode : { + value : null + } + } +}); +Y.augment(DOCKEDITEM, Y.EventTarget); \ No newline at end of file diff --git a/lib/yui/src/dock/js/dockloader.js b/lib/yui/src/dock/js/dockloader.js new file mode 100644 index 00000000000..1983108d8ad --- /dev/null +++ b/lib/yui/src/dock/js/dockloader.js @@ -0,0 +1,122 @@ +var LOADERNAME = 'moodle-core-dock-loader'; + +/** + * Core namespace. + * + * @static + * @namespace M + * @class core + */ +M.core = M.core || {}; + +/** + * Dock namespace. + * + * @static + * @namespace M.core + * @class dock + */ +M.core.dock = M.core.dock || {}; + +/** + * Creates the move to dock icon for dockable blocks if it doesn't already exist. + * + * @static + * @method ensureMoveToIconExists + * @param {Node} blocknode The Blocks node (.block[data-instanceid]) + */ +M.core.dock.ensureMoveToIconExists = function(blocknode) { + if (blocknode.one('.moveto')) { + return true; + } + + var commands, + moveto = Y.Node.create(''), + blockaction = blocknode.one('.block_action'), + icon = 't/block_to_dock'; + + // Must set the image src seperatly of we get an error with XML strict headers + if (right_to_left()) { + icon = icon + '_rtl'; + } + moveto.setAttribute('alt', M.util.get_string('addtodock', 'block')); + moveto.setAttribute('title', Y.Escape.html(M.util.get_string('dockblock', 'block', blocknode.one('.header .title h2').getHTML()))); + moveto.setAttribute('src', M.util.image_url(icon, 'moodle')); + + if (blockaction) { + blockaction.prepend(moveto); + } else { + commands = blocknode.one('.header .title .commands'); + if (!commands && blocknode.one('.header .title')) { + commands = Y.Node.create('
    '); + blocknode.one('.header .title').append(commands); + } + commands.append(moveto); + } + return true; +}; + +/** + * Dock loader. + * + * The dock loader is repsponsible for loading and initialising the dock only when required. + * By doing this we avoid the need to load unnecessary JavaScript into the page for the dock just incase + * it is being used. + * + * @static + * @namespace M.core.dock + * @class Loader + */ +M.core.dock.loader = M.core.dock.loader || {}; + +/** + * Delegation events + * @property delegationEvents + * @protected + * @type {Array} + */ +M.core.dock.loader.delegationEvents = []; + +/** + * Initialises the dock loader. + * + * The dock loader works by either firing the dock immediately if there are already docked blocks. + * Or if there are not any docked blocks delegating two events and then loading and firing the dock when one of + * those delegated events is triggered. + * + * @method initLoader + */ +M.core.dock.loader.initLoader = function() { + Y.log('Dock loader initialising', 'note', LOADERNAME); + var dockedblocks = Y.all('.block[data-instanceid][data-dockable]'), + body = Y.one(document.body); + dockedblocks.each(function() { + var id = parseInt(this.getData('instanceid'), 10); + Y.log('Dock loader watching block with instance id: '+id, 'note', LOADERNAME); + M.core.dock.ensureMoveToIconExists(this); + }); + if (dockedblocks.some(function(node){return node.hasClass('dock_on_load');})) { + Y.log('Loading dock module', 'note', LOADERNAME); + Y.use('moodle-core-dock', function() { + M.core.dock.init(); + }); + } else { + var callback = function(e) { + var i, + block = this.ancestor('.block[data-instanceid]'), + instanceid = block.getData('instanceid'); + e.halt(); + for (i in M.core.dock.loader.delegationEvents) { + M.core.dock.loader.delegationEvents[i].detach(); + } + block.addClass('dock_on_load'); + Y.log('Loading dock module', 'note', LOADERNAME); + Y.use('moodle-core-dock', function(){ + M.util.set_user_preference('docked_block_instance_'+instanceid, 1); + M.core.dock.init(); + }); + }; + M.core.dock.loader.delegationEvents.push(body.delegate('click', callback, '.moveto')); + M.core.dock.loader.delegationEvents.push(body.delegate('key', callback, '.moveto', 'enter')); + } +}; \ No newline at end of file diff --git a/lib/yui/src/dock/js/panel.js b/lib/yui/src/dock/js/panel.js new file mode 100644 index 00000000000..d7edf45715d --- /dev/null +++ b/lib/yui/src/dock/js/panel.js @@ -0,0 +1,233 @@ +/** + * Dock JS. + * + * This file contains the panel class used by the dock to display the content of docked blocks. + * + * @module moodle-core-dock + */ + +/** + * Panel. + * + * @namespace M.core.dock + * @class Panel + * @constructor + * @extends Y.Base + * @uses Y.EventTarget + */ +var DOCKPANEL = function() { + DOCKPANEL.superclass.constructor.apply(this, arguments); +}; +DOCKPANEL.prototype = { + /** + * True once the panel has been created. + * @property created + * @protected + * @type {Boolean} + */ + created : false, + /** + * Called during the initialisation process of the object. + * @method initializer + */ + initializer : function() { + Y.log('Panel initialising', 'note', LOGNS); + /** + * Fired before the panel is shown. + * @event dockpane::beforeshow + */ + this.publish('dockpanel:beforeshow', {prefix:'dockpanel'}); + /** + * Fired after the panel is shown. + * @event dockpanel:shown + */ + this.publish('dockpanel:shown', {prefix:'dockpanel'}); + /** + * Fired before the panel is hidden. + * @event dockpane::beforehide + */ + this.publish('dockpanel:beforehide', {prefix:'dockpanel'}); + /** + * Fired after the panel is hidden. + * @event dockpanel:hidden + */ + this.publish('dockpanel:hidden', {prefix:'dockpanel'}); + /** + * Fired when ever the dock panel is either hidden or shown. + * Always fired after the shown or hidden events. + * @event dockpanel:visiblechange + */ + this.publish('dockpanel:visiblechange', {prefix:'dockpanel'}); + }, + /** + * Creates the Panel if it has not already been created. + * @method create + * @return {Boolean} + */ + create : function() { + if (this.created) { + return true; + } + this.created = true; + var dock = this.get('dock'), + node = dock.get('dockNode'); + this.set('node', Y.Node.create('
    ')); + this.set('contentNode', Y.Node.create('
    ')); + this.set('headerNode', Y.Node.create('
    ')); + this.set('bodyNode', Y.Node.create('
    ')); + node.append( + this.get('node').append(this.get('contentNode').append(this.get('headerNode')).append(this.get('bodyNode'))) + ); + }, + /** + * Displays the panel. + * @method show + */ + show : function() { + this.create(); + this.fire('dockpanel:beforeshow'); + this.set('visible', true); + this.get('node').removeClass('dockitempanel_hidden'); + this.fire('dockpanel:shown'); + this.fire('dockpanel:visiblechange'); + }, + /** + * Hides the panel + * @method hide + */ + hide : function() { + this.fire('dockpanel:beforehide'); + this.set('visible', false); + this.get('node').addClass('dockitempanel_hidden'); + this.fire('dockpanel:hidden'); + this.fire('dockpanel:visiblechange'); + }, + /** + * Sets the panel header. + * @method setHeader + * @param {Node|String} content + */ + setHeader : function(content) { + this.create(); + var header = this.get('headerNode'); + header.setContent(content); + if (arguments.length > 1) { + for (var i=1;i < arguments.length;i++) { + header.append(arguments[i]); + } + } + }, + /** + * Sets the panel body. + * @method setBody + * @param {Node|String} content + */ + setBody : function(content) { + this.create(); + this.get('bodyNode').setContent(content); + }, + /** + * Sets the new top mark of the panel. + * + * @method setTop + * @param {Number} newtop + */ + setTop : function(newtop) { + if (Y.UA.ie > 0 && Y.UA.ie < 7) { + this.get('node').setY(newtop); + } else { + this.get('node').setStyle('top', newtop.toString()+'px'); + } + }, + /** + * Corrects the width of the panel. + * @method correctWidth + */ + correctWidth : function() { + var bodyNode = this.get('bodyNode'), + // Width of content. + width = bodyNode.get('clientWidth'), + // Scrollable width of content. + scroll = bodyNode.get('scrollWidth'), + // Width of content container with overflow. + offsetWidth = bodyNode.get('offsetWidth'), + // The new width - defaults to the current width. + newWidth = width, + // The max width (80% of screen). + maxWidth = Math.round(bodyNode.get('winWidth') * 0.8); + + // If the scrollable width is more than the visible width + if (scroll > width) { + // Content width + // + the difference + // + any rendering difference (borders, padding) + // + 10px to make it look nice. + newWidth = width + (scroll - width) + ((offsetWidth - width)*2) + 10; + } + + // Make sure its not more then the maxwidth + if (newWidth > maxWidth) { + newWidth = maxWidth; + } + + // Set the new width if its more than the old width. + if (newWidth > offsetWidth) { + this.get('node').setStyle('width', newWidth+'px'); + } + } +}; +Y.extend(DOCKPANEL, Y.Base, DOCKPANEL.prototype, { + NAME : 'moodle-core-dock-panel', + ATTRS : { + /** + * The dock itself. + * @attribute dock + * @type DOCK + * @writeonce + */ + dock : { + writeOnce : 'initOnly' + }, + /** + * The node that contains the whole panel. + * @attribute node + * @type Node + */ + node : { + value : null + }, + /** + * The node that contains the header, body and footer. + * @attribute contentNode + * @type Node + */ + contentNode : { + value : null + }, + /** + * The node that contains the header + * @attribute headerNode + * @type Node + */ + headerNode : { + value : null + }, + /** + * The node that contains the body + * @attribute bodyNode + * @type Node + */ + bodyNode : { + value : null + }, + /** + * True if the panel is currently visible. + * @attribute visible + * @type Boolean + */ + visible : { + value : false + } + } +}); +Y.augment(DOCKPANEL, Y.EventTarget); diff --git a/lib/yui/src/dock/js/tabheightmanager.js b/lib/yui/src/dock/js/tabheightmanager.js new file mode 100644 index 00000000000..3f07c0743fc --- /dev/null +++ b/lib/yui/src/dock/js/tabheightmanager.js @@ -0,0 +1,111 @@ +/** + * Dock JS. + * + * This file contains the tab height manager. + * The tab height manager is responsible for ensure all tabs are visible all the time. + * + * @module moodle-core-dock + */ + +/** + * Tab height manager. + * + * @namespace M.core.dock + * @class TabHeightManager + * @constructor + * @extends Y.Base + */ +var TABHEIGHTMANAGER = function() { + TABHEIGHTMANAGER.superclass.constructor.apply(this, arguments); +}; +TABHEIGHTMANAGER.prototype = { + /** + * Initialises the dock sizer which then attaches itself to the required + * events in order to monitor the dock + * @method initializer + */ + initializer : function() { + var dock = this.get('dock'); + dock.on('dock:itemschanged', this.checkSizing, this); + Y.on('windowresize', this.checkSizing, this); + }, + /** + * Check if the size dock items needs to be adjusted + * @method checkSizing + */ + checkSizing : function() { + var dock = this.get('dock'), + node = dock.get('dockNode'), + items = dock.dockeditems, + possibleheight = node.get('offsetHeight') - node.one('.controls').get('offsetHeight') - (dock.get('bufferPanel')*3) - (items.length*2), + totalheight = 0, + id, dockedtitle; + if (items.length > 0) { + for (id in items) { + dockedtitle = Y.one(items[id].get('title')).ancestor('.'+CSS.dockedtitle); + if (dockedtitle) { + if (this.get('enabled')) { + dockedtitle.setStyle('height', 'auto'); + } + totalheight += dockedtitle.get('offsetHeight') || 0; + } + } + if (totalheight > possibleheight) { + this.enable(possibleheight); + } + } + }, + /** + * Enables the dock sizer and resizes where required. + * @method enable + * @param {Number} possibleheight + */ + enable : function(possibleheight) { + var dock = this.get('dock'), + items = dock.dockeditems, + count = dock.count, + runningcount = 0, + usedheight = 0, + id, itemtitle, itemheight, offsetheight; + Y.log('Enabling the dock tab sizer.', 'note', LOGNS); + this.set('enabled', true); + for (id in items) { + itemtitle = Y.one(items[id].get('title')).ancestor('.'+CSS.dockedtitle); + if (!itemtitle) { + continue; + } + itemheight = Math.floor((possibleheight-usedheight) / (count - runningcount)); + offsetheight = itemtitle.get('offsetHeight'); + itemtitle.setStyle('overflow', 'hidden'); + if (offsetheight > itemheight) { + itemtitle.setStyle('height', itemheight+'px'); + usedheight += itemheight; + } else { + usedheight += offsetheight; + } + runningcount++; + } + } +}; +Y.extend(TABHEIGHTMANAGER, Y.Base, TABHEIGHTMANAGER.prototype, { + NAME : 'moodle-core-tabheightmanager', + ATTRS : { + /** + * The dock. + * @attribute dock + * @type DOCK + * @writeOnce + */ + dock : { + writeOnce : 'initOnly' + }, + /** + * True if the item_sizer is being used, false otherwise. + * @attribute enabled + * @type Bool + */ + enabled : { + value : false + } + } +}); diff --git a/lib/yui/src/dock/meta/dock.json b/lib/yui/src/dock/meta/dock.json new file mode 100644 index 00000000000..49cc7f315cc --- /dev/null +++ b/lib/yui/src/dock/meta/dock.json @@ -0,0 +1,18 @@ +{ + "moodle-core-dock": { + "requires": [ + "base", + "node", + "event-custom", + "event-mouseenter", + "event-resize", + "escape", + "moodle-core-dockloader" + ] + }, + "moodle-core-dockloader": { + "requires": [ + "escape" + ] + } +} \ No newline at end of file