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', BODY = Y.one(Y.config.doc.body), 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 dockonload: 'dock_on_load' }, 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]' }, DOCK, DOCKPANEL, TABHEIGHTMANAGER, BLOCK, DOCKEDITEM; // eslint-disable-line no-unused-vars M.core = M.core || {}; 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); Y.Global.on(M.core.globalEvents.BLOCK_CONTENT_UPDATED, function(e) { M.core.dock.notifyBlockChange(e.instanceid); }, this); 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', LOGNS); 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(), fontsize = '11px', transform = 'rotate(270deg)', test, width, height, container, verticaldirection = M.util.get_string('thisdirectionvertical', 'langconfig'); title = Y.one(title); if (dock.get('orientation') !== 'vertical') { // If the dock isn't vertical don't adjust it! title.set('innerHTML', text); return title; } if (Y.UA.ie > 0 && Y.UA.ie < 8) { // IE 6/7 can't rotate text so force ver verticaldirection = 'ver'; } switch (verticaldirection) { case 'ver': // Stacked is easy return title.set('innerHTML', text.split('').join('
')); case 'ttb': transform = 'rotate(90deg)'; break; case 'btt': // Nothing to do here. transform default is good. 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.set('innerHTML', 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. test = Y.Node.create('

' + text + '

'); BODY.insert(test, 0); width = test.one('span').get('offsetWidth') * 1.2; height = test.one('span').get('offsetHeight'); test.remove(); title.set('innerHTML', text); title.addClass('css3transform'); // Move the title into position title.setStyles({ 'position': 'relative', 'fontSize': fontsize, 'width': width, 'top': (width - height) / 2 }); // Positioning is different when in RTL mode. if (window.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 }); container = Y.Node.create('
'); container.append(title); container.setStyles({ height: width + (width / 4), 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 Base * @uses EventTarget */ 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', 'debug', 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', broadcast: 2}); /** * Fired after the dock has been changed from visible to hidden. * @event dock:hidden */ this.publish('dock:hidden', {prefix: 'dock', broadcast: 2}); /** * 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', broadcast: 2}); /** * 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(), clickargs = { cssselector: '.' + CSS.dockedtitle, delay: 0 }, mouseenterargs = { cssselector: '.' + CSS.dockedtitle, delay: 0.5, iscontained: true, preventevent: 'click', preventdelay: 3 }; 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}); // 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('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(); this.dockeditems[target.getAttribute('rel')].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 window.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) { if (Y.Lang.isString(key) && cfgmap[key]) { 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)', 'debug', LOGNS); this.set(cfgmap[key], value); } } // Check for and apply any legacy CSS changes.. for (key in M.core_dock.css) { if (Y.Lang.isString(key)) { 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', 'debug', 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(), target, targetid, regex = /^dock_item_(\d+)_title$/, self = this; if (options.cssselector === 'body') { if (!this.get('dockNode').contains(e.target)) { if (item) { item.hide(); } } } else { 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() { 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); } targetid = target.get('id'); 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 populatedRegionCount = 0, populatedBlockRegions = [], unpopulatedBlockRegions = [], isMoving = false, populatedLegacyRegions = [], containsLegacyRegions = false, classesToAdd = [], classesToRemove = []; // First look for understood regions. Y.all(SELECTOR.blockregion).each(function(region) { var regionname = region.getData('blockregion'); if (region.all('.block').size() > 0) { populatedBlockRegions.push(regionname); populatedRegionCount++; } else if (region.all('.block_dock_placeholder').size() > 0) { unpopulatedBlockRegions.push(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; } // Sigh - there are legacy regions. containsLegacyRegions = true; var regionname = region.get('id').replace(/^region\-/, 'side-'), hasblocks = (region.all('.block').size() > 0); if (hasblocks) { populatedLegacyRegions.push(regionname); populatedRegionCount++; } else { // This legacy region has no blocks so cannot have the -only body tag. classesToRemove.push( regionname + '-only' ); } }); if (BODY.hasClass('blocks-moving')) { // When we're moving blocks, we do not want to collapse. isMoving = true; } Y.each(unpopulatedBlockRegions, function(regionname) { classesToAdd.push( // This block region is empty. 'empty-region-' + regionname, // Which has the same effect as being docked. 'docked-region-' + regionname ); classesToRemove.push( // It is no-longer used. 'used-region-' + regionname, // It cannot be the only region on screen if it is empty. regionname + '-only' ); }, this); Y.each(populatedBlockRegions, function(regionname) { classesToAdd.push( // This block region is in use. 'used-region-' + regionname ); classesToRemove.push( // It is not empty. 'empty-region-' + regionname, // Is it not docked. 'docked-region-' + regionname ); if (populatedRegionCount === 1 && isMoving === false) { // There was only one populated region, and we are not moving blocks. classesToAdd.push(regionname + '-only'); } else { // There were multiple block regions visible - remove any 'only' classes. classesToRemove.push(regionname + '-only'); } }, this); if (containsLegacyRegions) { // Handle the classing for legacy blocks. These have slightly different class names for the body. if (isMoving || populatedRegionCount !== 1) { Y.each(populatedLegacyRegions, function(regionname) { classesToRemove.push(regionname + '-only'); }); } else { Y.each(populatedLegacyRegions, function(regionname) { classesToAdd.push(regionname + '-only'); }); } } if (!BODY.hasClass('has-region-content')) { // This page does not have a content region, therefore content-only is implied when all block regions are docked. if (populatedRegionCount === 0 && isMoving === false) { // If all blocks are docked, ensure that the content-only class is added anyway. classesToAdd.push('content-only'); } else { // Otherwise remove it. classesToRemove.push('content-only'); } } // Modify the body clases. Y.each(classesToRemove, function(className) { BODY.removeClass(className); }); Y.each(classesToAdd, function(className) { BODY.addClass(className); }); }, /** * 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.', 'debug', 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.', 'debug', 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', 'debug', LOGNS); var i; for (i in this.dockeditems) { if (Y.Lang.isNumber(i) || Y.Lang.isString(i)) { 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(), item = this.getActiveItem(), buffer, screenh, docky, titletop, containery, containerheight, scrolltop, panelheight, dockx, titleleft; if (!panel.get('visible') || !item) { return true; } this.fire('dock:panelresizestart'); if (this.get('orientation') === 'vertical') { buffer = this.get('bufferPanel'); screenh = parseInt(BODY.get('winHeight'), 10) - (buffer * 2); docky = this.get('dockNode').getY(); titletop = item.get('dockTitleNode').getY() - docky - buffer; containery = this.get('itemContainerNode').getY(); containerheight = containery - docky + this.get('buttonsNode').get('offsetHeight'); scrolltop = panel.get('bodyNode').get('scrollTop'); panel.get('bodyNode').setStyle('height', 'auto'); panel.get('node').removeClass('oversized_content'); panelheight = panel.get('node').get('offsetHeight'); if (Y.UA.ie > 0 && Y.UA.ie < 7) { panel.setTop(item.get('dockTitleNode').getY()); } else if (panelheight > screenh) { panel.setTop(buffer - containerheight); panel.get('bodyNode').setStyle('height', (screenh - panel.get('headerNode').get('offsetHeight')) + 'px'); panel.get('node').addClass('oversized_content'); } else if (panelheight > (screenh - (titletop - buffer))) { panel.setTop(titletop - containerheight - (panelheight - (screenh - titletop)) + 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('node').get('offsetWidth') + 'px'); } else if (this.get('position') === 'top') { dockx = this.get('dockNode').getX(); 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() { var i; for (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((window.right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block', 'moodle'), validator: Y.Lang.isString } } }); Y.augment(DOCK, Y.EventTarget); /* global DOCKPANEL, LOGNS */ /** * 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 Base * @uses EventTarget */ 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', 'debug', 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'), i; header.setContent(content); if (arguments.length > 1) { for (i = 1; i < arguments.length; i++) { if (Y.Lang.isNumber(i) || Y.Lang.isString(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); /* global TABHEIGHTMANAGER, LOGNS */ /** * 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 Base */ 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, containermargin = parseInt(node.one('.dockeditem_container').getStyle('marginTop').replace('/[^0-9]+$/', ''), 10), dockheight = node.get('offsetHeight') - containermargin, controlheight = node.one('.controls').get('offsetHeight'), buffer = (dock.get('bufferPanel') * 3), possibleheight = dockheight - controlheight - buffer - (items.length * 2), totalheight = 0, id, dockedtitle; if (items.length > 0) { for (id in items) { if (Y.Lang.isNumber(id) || Y.Lang.isString(id)) { 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.', 'debug', LOGNS); this.set('enabled', true); for (id in items) { if (Y.Lang.isNumber(id) || Y.Lang.isString(id)) { 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. * @property _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 * @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 */ 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) { // 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) { sub._delegateDetacher.detach(); } }); /* global BLOCK, LOGNS, DOCKEDITEM */ /** * 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 Base */ 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 DOCKEDITEM */ 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'), 'debug', LOGNS); M.core.dock.ensureMoveToIconExists(node); // Move the block straight to the dock if required if (node.hasClass(CSS.dockonload)) { node.removeClass(CSS.dockonload); this.moveToDock(); } this.skipsetposition = false; return true; }, /** * Returns the class associated with this block. * @method _getBlockClass * @private * @param {Node} node * @return String */ _getBlockClass: function(node) { var block = node.getData('block'), classes, matches; if (Y.Lang.isString(block) && block !== '') { return block; } classes = node.getAttribute('className').toString(); matches = /(^| )block_([^ ]+)/.exec(classes); if (matches) { return matches[2]; } return matches; }, /** * This function is responsible 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'), icon = (window.right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block', breakchar = (location.href.match(/\?/)) ? '&' : '?', blocktitle, blockcommands, movetoimg, moveto; if (!blockcontent) { return; } Y.log('Moving block to the dock:' + this.get('id'), 'debug', LOGNS); this.recordBlockState(); blocktitle = this.cachedcontentnode.one('.title h2').cloneNode(true); // Build up the block commands. // These should not actually added to the DOM. blockcommands = this.cachedcontentnode.one('.title .commands'); if (blockcommands) { blockcommands = blockcommands.cloneNode(true); } else { blockcommands = Y.Node.create('
'); } movetoimg = Y.Node.create('').setAttrs({ alt: Y.Escape.html(M.util.get_string('undockitem', 'block')), title: Y.Escape.html(M.util.get_string('undockblock', 'block', blocktitle.get('innerHTML'))), src: M.util.image_url(icon, 'moodle') }); moveto = Y.Node.create('').setAttrs({ href: Y.config.win.location.href + breakchar + 'dock=' + id }); moveto.append(movetoimg); blockcommands.append(moveto.append(movetoimg)); // 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('isDocked', 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; }, /** * This function removes a block from the dock and puts it back into the page structure. * @method returnToPage * @return {Boolean} */ returnToPage: function() { var id = this.get('id'); Y.log('Moving block out of the dock:' + this.get('id'), 'debug', 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 = null; M.util.set_user_preference('docked_block_instance_' + id, 0); this.set('isDocked', 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 } } }); /* global LOGNS, DOCKEDITEM */ /** * Dock JS. * * This file contains the docked item class. * * @module moodle-core-dock */ /** * Docked item. * * @namespace M.core.dock * @class DockedItem * @constructor * @extends Base * @uses EventTarget */ 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, type; /** * 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) { 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(), 'debug', 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.util.get_string('hidepanel', 'block') +
                ''); 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(), 'debug', LOGNS); panel.setHeader(this.get('titlestring'), this.get('commands')); panel.setBody(Y.Node.create('
') .append(this.get('contents'))); if (M.core.actionmenu !== undefined) { M.core.actionmenu.newDOMNode(panel.get('node')); } 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(), 'debug', 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').returnToPage(); // 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-dock-loader", "moodle-core-event" ] });