[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 YUI.add('moodle-core-dock', function (Y, NAME) { 2 3 /** 4 * Dock JS. 5 * 6 * This file contains the DOCK object and all dock related global namespace methods and properties. 7 * 8 * @module moodle-core-dock 9 */ 10 11 var LOGNS = 'moodle-core-dock', 12 BODY = Y.one(Y.config.doc.body), 13 CSS = { 14 dock: 'dock', // CSS Class applied to the dock box 15 dockspacer: 'dockspacer', // CSS class applied to the dockspacer 16 controls: 'controls', // CSS class applied to the controls box 17 body: 'has_dock', // CSS class added to the body when there is a dock 18 buttonscontainer: 'buttons_container', 19 dockeditem: 'dockeditem', // CSS class added to each item in the dock 20 dockeditemcontainer: 'dockeditem_container', 21 dockedtitle: 'dockedtitle', // CSS class added to the item's title in each dock 22 activeitem: 'activeitem', // CSS class added to the active item 23 dockonload: 'dock_on_load' 24 }, 25 SELECTOR = { 26 dockableblock: '.block[data-instanceid][data-dockable]', 27 blockmoveto: '.block[data-instanceid][data-dockable] .moveto', 28 panelmoveto: '#dockeditempanel .commands a.moveto', 29 dockonload: '.block.' + CSS.dockonload, 30 blockregion: '[data-blockregion]' 31 }, 32 DOCK, 33 DOCKPANEL, 34 TABHEIGHTMANAGER, 35 BLOCK, 36 DOCKEDITEM; // eslint-disable-line no-unused-vars 37 38 M.core = M.core || {}; 39 M.core.dock = M.core.dock || {}; 40 41 /** 42 * The dock - once initialised. 43 * 44 * @private 45 * @property _dock 46 * @type DOCK 47 */ 48 M.core.dock._dock = null; 49 50 /** 51 * An associative array of dockable blocks. 52 * @property _dockableblocks 53 * @type {Array} An array of BLOCK objects organised by instanceid. 54 * @private 55 */ 56 M.core.dock._dockableblocks = {}; 57 58 /** 59 * Initialises the dock. 60 * This method registers dockable blocks, and creates delegations to dock them. 61 * @static 62 * @method init 63 */ 64 M.core.dock.init = function() { 65 Y.all(SELECTOR.dockableblock).each(M.core.dock.registerDockableBlock); 66 Y.Global.on(M.core.globalEvents.BLOCK_CONTENT_UPDATED, function(e) { 67 M.core.dock.notifyBlockChange(e.instanceid); 68 }, this); 69 BODY.delegate('click', M.core.dock.dockBlock, SELECTOR.blockmoveto); 70 BODY.delegate('key', M.core.dock.dockBlock, SELECTOR.blockmoveto, 'enter'); 71 }; 72 73 /** 74 * Returns an instance of the dock. 75 * Initialises one if one hasn't already being initialised. 76 * 77 * @static 78 * @method get 79 * @return DOCK 80 */ 81 M.core.dock.get = function() { 82 if (this._dock === null) { 83 this._dock = new DOCK(); 84 } 85 return this._dock; 86 }; 87 88 /** 89 * Registers a dockable block with the dock. 90 * 91 * @static 92 * @method registerDockableBlock 93 * @param {int} id The block instance ID. 94 * @return void 95 */ 96 M.core.dock.registerDockableBlock = function(id) { 97 if (typeof id === 'object' && typeof id.getData === 'function') { 98 id = id.getData('instanceid'); 99 } 100 M.core.dock._dockableblocks[id] = new BLOCK({id: id}); 101 }; 102 103 /** 104 * Docks a block given either its instanceid, its node, or an event fired from within the block. 105 * @static 106 * @method dockBlockByInstanceID 107 * @param id 108 * @return void 109 */ 110 M.core.dock.dockBlock = function(id) { 111 if (typeof id === 'object' && id.target !== 'undefined') { 112 id = id.target; 113 } 114 if (typeof id === "object") { 115 if (!id.test(SELECTOR.dockableblock)) { 116 id = id.ancestor(SELECTOR.dockableblock); 117 } 118 if (typeof id === 'object' && typeof id.getData === 'function' && !id.ancestor('.' + CSS.dock)) { 119 id = id.getData('instanceid'); 120 } else { 121 Y.log('Invalid instanceid given to dockBlockByInstanceID', 'warn', LOGNS); 122 return; 123 } 124 } 125 var block = M.core.dock._dockableblocks[id]; 126 if (block) { 127 block.moveToDock(); 128 } 129 }; 130 131 /** 132 * Fixes the title orientation. Rotating it if required. 133 * 134 * @static 135 * @method fixTitleOrientation 136 * @param {Node} title The title node we are looking at. 137 * @param {String} text The string to use as the title. 138 * @return {Node} The title node to use. 139 */ 140 M.core.dock.fixTitleOrientation = function(title, text) { 141 var dock = M.core.dock.get(), 142 fontsize = '11px', 143 transform = 'rotate(270deg)', 144 test, 145 width, 146 height, 147 container, 148 verticaldirection = M.util.get_string('thisdirectionvertical', 'langconfig'); 149 title = Y.one(title); 150 151 if (dock.get('orientation') !== 'vertical') { 152 // If the dock isn't vertical don't adjust it! 153 title.set('innerHTML', text); 154 return title; 155 } 156 157 if (Y.UA.ie > 0 && Y.UA.ie < 8) { 158 // IE 6/7 can't rotate text so force ver 159 verticaldirection = 'ver'; 160 } 161 162 switch (verticaldirection) { 163 case 'ver': 164 // Stacked is easy 165 return title.set('innerHTML', text.split('').join('<br />')); 166 case 'ttb': 167 transform = 'rotate(90deg)'; 168 break; 169 case 'btt': 170 // Nothing to do here. transform default is good. 171 break; 172 } 173 174 if (Y.UA.ie === 8) { 175 // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute. 176 title.set('innerHTML', text); 177 title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;'); 178 title.addClass('filterrotate'); 179 return title; 180 } 181 182 // We need to fix a font-size - sorry theme designers. 183 test = Y.Node.create('<h2 class="transform-test-heading"><span class="transform-test-node" style="font-size:' + 184 fontsize + ';">' + text + '</span></h2>'); 185 BODY.insert(test, 0); 186 width = test.one('span').get('offsetWidth') * 1.2; 187 height = test.one('span').get('offsetHeight'); 188 test.remove(); 189 190 title.set('innerHTML', text); 191 title.addClass('css3transform'); 192 193 // Move the title into position 194 title.setStyles({ 195 'position': 'relative', 196 'fontSize': fontsize, 197 'width': width, 198 'top': (width - height) / 2 199 }); 200 201 // Positioning is different when in RTL mode. 202 if (window.right_to_left()) { 203 title.setStyle('left', width / 2 - height); 204 } else { 205 title.setStyle('right', width / 2 - height); 206 } 207 208 // Rotate the text 209 title.setStyles({ 210 'transform': transform, 211 '-ms-transform': transform, 212 '-moz-transform': transform, 213 '-webkit-transform': transform, 214 '-o-transform': transform 215 }); 216 217 container = Y.Node.create('<div></div>'); 218 container.append(title); 219 container.setStyles({ 220 height: width + (width / 4), 221 position: 'relative' 222 }); 223 return container; 224 }; 225 226 /** 227 * Informs the dock that the content of the block has changed. 228 * This should be called by the blocks JS code if its content has been updated dynamically. 229 * This method ensure the dock resizes if need be. 230 * 231 * @static 232 * @method notifyBlockChange 233 * @param {Number} instanceid 234 * @return void 235 */ 236 M.core.dock.notifyBlockChange = function(instanceid) { 237 if (this._dock !== null) { 238 var dock = M.core.dock.get(), 239 activeitem = dock.getActiveItem(); 240 if (activeitem && activeitem.get('blockinstanceid') === parseInt(instanceid, 10)) { 241 dock.resizePanelIfRequired(); 242 } 243 } 244 }; 245 246 /** 247 * The Dock. 248 * 249 * @namespace M.core.dock 250 * @class Dock 251 * @constructor 252 * @extends Base 253 * @uses EventTarget 254 */ 255 DOCK = function() { 256 DOCK.superclass.constructor.apply(this, arguments); 257 }; 258 DOCK.prototype = { 259 /** 260 * Tab height manager used to ensure tabs are always visible. 261 * @protected 262 * @property tabheightmanager 263 * @type TABHEIGHTMANAGER 264 */ 265 tabheightmanager: null, 266 /** 267 * Will be an eventtype if there is an eventype to prevent. 268 * @protected 269 * @property preventevent 270 * @type String 271 */ 272 preventevent: null, 273 /** 274 * Will be an object if there is a delayed event in effect. 275 * @protected 276 * @property delayedevent 277 * @type {Object} 278 */ 279 delayedevent: null, 280 /** 281 * An array of currently docked items. 282 * @protected 283 * @property dockeditems 284 * @type Array 285 */ 286 dockeditems: [], 287 /** 288 * Set to true once the dock has been drawn. 289 * @protected 290 * @property dockdrawn 291 * @type Boolean 292 */ 293 dockdrawn: false, 294 /** 295 * The number of blocks that are currently docked. 296 * @protected 297 * @property count 298 * @type Number 299 */ 300 count: 0, 301 /** 302 * The total number of blocks that have been docked. 303 * @protected 304 * @property totalcount 305 * @type Number 306 */ 307 totalcount: 0, 308 /** 309 * A hidden node used as a holding area for DOM objects used by blocks that have been docked. 310 * @protected 311 * @property holdingareanode 312 * @type Node 313 */ 314 holdingareanode: null, 315 /** 316 * Called during the initialisation process of the object. 317 * @method initializer 318 */ 319 initializer: function() { 320 Y.log('Dock initialising', 'debug', LOGNS); 321 322 // Publish the events the dock has 323 /** 324 * Fired when the dock first starts initialising. 325 * @event dock:starting 326 */ 327 this.publish('dock:starting', {prefix: 'dock', broadcast: 2, emitFacade: true, fireOnce: true}); 328 /** 329 * Fired after the dock is initialised for the first time. 330 * @event dock:initialised 331 */ 332 this.publish('dock:initialised', {prefix: 'dock', broadcast: 2, emitFacade: true, fireOnce: true}); 333 /** 334 * Fired before the dock structure and content is first created. 335 * @event dock:beforedraw 336 */ 337 this.publish('dock:beforedraw', {prefix: 'dock', fireOnce: true}); 338 /** 339 * Fired before the dock is changed from hidden to visible. 340 * @event dock:beforeshow 341 */ 342 this.publish('dock:beforeshow', {prefix: 'dock'}); 343 /** 344 * Fires after the dock has been changed from hidden to visible. 345 * @event dock:shown 346 */ 347 this.publish('dock:shown', {prefix: 'dock', broadcast: 2}); 348 /** 349 * Fired after the dock has been changed from visible to hidden. 350 * @event dock:hidden 351 */ 352 this.publish('dock:hidden', {prefix: 'dock', broadcast: 2}); 353 /** 354 * Fires when an item is added to the dock. 355 * @event dock:itemadded 356 */ 357 this.publish('dock:itemadded', {prefix: 'dock'}); 358 /** 359 * Fires when an item is removed from the dock. 360 * @event dock:itemremoved 361 */ 362 this.publish('dock:itemremoved', {prefix: 'dock'}); 363 /** 364 * Fires when a block is added or removed from the dock. 365 * This happens after the itemadded and itemremoved events have been called. 366 * @event dock:itemschanged 367 */ 368 this.publish('dock:itemschanged', {prefix: 'dock', broadcast: 2}); 369 /** 370 * Fires once when the docks panel is first initialised. 371 * @event dock:panelgenerated 372 */ 373 this.publish('dock:panelgenerated', {prefix: 'dock', fireOnce: true}); 374 /** 375 * Fires when the dock panel is about to be resized. 376 * @event dock:panelresizestart 377 */ 378 this.publish('dock:panelresizestart', {prefix: 'dock'}); 379 /** 380 * Fires after the dock panel has been resized. 381 * @event dock:resizepanelcomplete 382 */ 383 this.publish('dock:resizepanelcomplete', {prefix: 'dock'}); 384 385 // Apply theme customisations here before we do any real work. 386 this._applyThemeCustomisation(); 387 // Inform everyone we are now about to initialise. 388 this.fire('dock:starting'); 389 this._ensureDockDrawn(); 390 // Inform everyone the dock has been initialised 391 this.fire('dock:initialised'); 392 }, 393 /** 394 * Ensures that the dock has been drawn. 395 * @private 396 * @method _ensureDockDrawn 397 * @return {Boolean} 398 */ 399 _ensureDockDrawn: function() { 400 if (this.dockdrawn === true) { 401 return true; 402 } 403 var dock = this._initialiseDockNode(), 404 clickargs = { 405 cssselector: '.' + CSS.dockedtitle, 406 delay: 0 407 }, 408 mouseenterargs = { 409 cssselector: '.' + CSS.dockedtitle, 410 delay: 0.5, 411 iscontained: true, 412 preventevent: 'click', 413 preventdelay: 3 414 }; 415 if (Y.UA.ie > 0 && Y.UA.ie < 7) { 416 // Adjust for IE 6 (can't handle fixed pos) 417 dock.setStyle('height', dock.get('winHeight') + 'px'); 418 } 419 420 this.fire('dock:beforedraw'); 421 422 this._initialiseDockControls(); 423 424 this.tabheightmanager = new TABHEIGHTMANAGER({dock: this}); 425 426 // Attach the required event listeners 427 // We use delegate here as that way a handful of events are created for the dock 428 // and all items rather than the same number for the dock AND every item individually 429 Y.delegate('click', this.handleEvent, this.get('dockNode'), '.' + CSS.dockedtitle, this, clickargs); 430 Y.delegate('mouseenter', this.handleEvent, this.get('dockNode'), '.' + CSS.dockedtitle, this, mouseenterargs); 431 this.get('dockNode').on('mouseleave', this.handleEvent, this, {cssselector: '#dock', delay: 0.5, iscontained: false}); 432 433 Y.delegate('click', this.handleReturnToBlock, this.get('dockNode'), SELECTOR.panelmoveto, this); 434 Y.delegate('dock:actionkey', this.handleDockedItemEvent, this.get('dockNode'), '.' + CSS.dockeditem, this); 435 436 BODY.on('click', this.handleEvent, this, {cssselector: 'body', delay: 0}); 437 this.on('dock:itemschanged', this.resizeBlockSpace, this); 438 this.on('dock:itemschanged', this.checkDockVisibility, this); 439 this.on('dock:itemschanged', this.resetFirstItem, this); 440 this.dockdrawn = true; 441 return true; 442 }, 443 /** 444 * Handles an actionkey event on the dock. 445 * @param {EventFacade} e 446 * @method handleDockedItemEvent 447 * @return {Boolean} 448 */ 449 handleDockedItemEvent: function(e) { 450 if (e.type !== 'dock:actionkey') { 451 return false; 452 } 453 var target = e.target, 454 dockeditem = '.' + CSS.dockeditem; 455 if (!target.test(dockeditem)) { 456 target = target.ancestor(dockeditem); 457 } 458 if (!target) { 459 return false; 460 } 461 e.halt(); 462 this.dockeditems[target.getAttribute('rel')].toggle(e.action); 463 }, 464 /** 465 * Call the theme customisation method "customise_dock_for_theme" if it exists. 466 * @private 467 * @method _applyThemeCustomisation 468 */ 469 _applyThemeCustomisation: function() { 470 // Check if there is a customisation function 471 if (typeof (customise_dock_for_theme) === 'function') { 472 // First up pre the legacy object. 473 M.core_dock = this; 474 M.core_dock.cfg = { 475 buffer: null, 476 orientation: null, 477 position: null, 478 spacebeforefirstitem: null, 479 removeallicon: null 480 }; 481 M.core_dock.css = { 482 dock: null, 483 dockspacer: null, 484 controls: null, 485 body: null, 486 buttonscontainer: null, 487 dockeditem: null, 488 dockeditemcontainer: null, 489 dockedtitle: null, 490 activeitem: null 491 }; 492 try { 493 // Run the customisation function 494 window.customise_dock_for_theme(this); 495 } catch (exception) { 496 // Do nothing at the moment. 497 Y.log('Exception while attempting to apply theme customisations.', 'error', LOGNS); 498 } 499 // Now to work out what they did. 500 var key, value, 501 warned = false, 502 cfgmap = { 503 buffer: 'bufferPanel', 504 orientation: 'orientation', 505 position: 'position', 506 spacebeforefirstitem: 'bufferBeforeFirstItem', 507 removeallicon: 'undockAllIconUrl' 508 }; 509 // Check for and apply any legacy configuration. 510 for (key in M.core_dock.cfg) { 511 if (Y.Lang.isString(key) && cfgmap[key]) { 512 value = M.core_dock.cfg[key]; 513 if (value === null) { 514 continue; 515 } 516 if (!warned) { 517 Y.log('Warning: customise_dock_for_theme has changed. Please update your code.', 'warn', LOGNS); 518 warned = true; 519 } 520 // Damn, the've set something. 521 Y.log('Note for customise_dock_for_theme code: M.core_dock.cfg.' + key + 522 ' is now dock.set(\'' + key + '\', value)', 523 'debug', LOGNS); 524 this.set(cfgmap[key], value); 525 } 526 } 527 // Check for and apply any legacy CSS changes.. 528 for (key in M.core_dock.css) { 529 if (Y.Lang.isString(key)) { 530 value = M.core_dock.css[key]; 531 if (value === null) { 532 continue; 533 } 534 if (!warned) { 535 Y.log('Warning: customise_dock_for_theme has changed. Please update your code.', 'warn', LOGNS); 536 warned = true; 537 } 538 // Damn, they've set something. 539 Y.log('Note for customise_dock_for_theme code: M.core_dock.css.' + key + ' is now CSS.' + key + ' = value', 540 'debug', LOGNS); 541 CSS[key] = value; 542 } 543 } 544 } 545 }, 546 /** 547 * Initialises the dock node, creating it and its content if required. 548 * 549 * @private 550 * @method _initialiseDockNode 551 * @return {Node} The dockNode 552 */ 553 _initialiseDockNode: function() { 554 var dock = this.get('dockNode'), 555 positionorientationclass = CSS.dock + '_' + this.get('position') + '_' + this.get('orientation'), 556 holdingarea = Y.Node.create('<div></div>').setStyles({display: 'none'}), 557 buttons = this.get('buttonsNode'), 558 container = this.get('itemContainerNode'); 559 560 if (!dock) { 561 dock = Y.one('#' + CSS.dock); 562 } 563 if (!dock) { 564 dock = Y.Node.create('<div id="' + CSS.dock + '"></div>'); 565 BODY.append(dock); 566 } 567 dock.setAttribute('role', 'menubar').addClass(positionorientationclass); 568 if (Y.all(SELECTOR.dockonload).size() === 0) { 569 // Nothing on the dock... hide it using CSS 570 dock.addClass('nothingdocked'); 571 } else { 572 positionorientationclass = CSS.body + '_' + this.get('position') + '_' + this.get('orientation'); 573 BODY.addClass(CSS.body).addClass(); 574 } 575 576 if (!buttons) { 577 buttons = dock.one('.' + CSS.buttonscontainer); 578 } 579 if (!buttons) { 580 buttons = Y.Node.create('<div class="' + CSS.buttonscontainer + '"></div>'); 581 dock.append(buttons); 582 } 583 584 if (!container) { 585 container = dock.one('.' + CSS.dockeditemcontainer); 586 } 587 if (!container) { 588 container = Y.Node.create('<div class="' + CSS.dockeditemcontainer + '"></div>'); 589 buttons.append(container); 590 } 591 592 BODY.append(holdingarea); 593 this.holdingareanode = holdingarea; 594 595 this.set('dockNode', dock); 596 this.set('buttonsNode', buttons); 597 this.set('itemContainerNode', container); 598 599 return dock; 600 }, 601 /** 602 * Initialises the dock controls. 603 * 604 * @private 605 * @method _initialiseDockControls 606 */ 607 _initialiseDockControls: function() { 608 // Add a removeall button 609 // Must set the image src seperatly of we get an error with XML strict headers 610 611 var removeall = Y.Node.create('<img alt="' + M.util.get_string('undockall', 'block') + '" tabindex="0" />'); 612 removeall.setAttribute('src', this.get('undockAllIconUrl')); 613 removeall.on('removeall|click', this.removeAll, this); 614 removeall.on('dock:actionkey', this.removeAll, this, {actions: {enter: true}}); 615 this.get('buttonsNode').append(Y.Node.create('<div class="' + CSS.controls + '"></div>').append(removeall)); 616 }, 617 /** 618 * Returns the dock panel. Initialising it if it hasn't already been initialised. 619 * @method getPanel 620 * @return {DOCKPANEL} 621 */ 622 getPanel: function() { 623 var panel = this.get('panel'); 624 if (!panel) { 625 panel = new DOCKPANEL({dock: this}); 626 panel.on('panel:visiblechange', this.resize, this); 627 Y.on('windowresize', this.resize, this); 628 // Initialise the dockpanel .. should only happen once 629 this.set('panel', panel); 630 this.fire('dock:panelgenerated'); 631 } 632 return panel; 633 }, 634 /** 635 * Resizes the dock panel if required. 636 * @method resizePanelIfRequired 637 */ 638 resizePanelIfRequired: function() { 639 this.resize(); 640 var panel = this.get('panel'); 641 if (panel) { 642 panel.correctWidth(); 643 } 644 }, 645 /** 646 * Handles a dock event sending it to the right place. 647 * 648 * @method handleEvent 649 * @param {EventFacade} e 650 * @param {Object} options 651 * @return {Boolean} 652 */ 653 handleEvent: function(e, options) { 654 var item = this.getActiveItem(), 655 target, 656 targetid, 657 regex = /^dock_item_(\d+)_title$/, 658 self = this; 659 if (options.cssselector === 'body') { 660 if (!this.get('dockNode').contains(e.target)) { 661 if (item) { 662 item.hide(); 663 } 664 } 665 } else { 666 if (e.target.test(options.cssselector)) { 667 target = e.target; 668 } else { 669 target = e.target.ancestor(options.cssselector); 670 } 671 if (!target) { 672 return true; 673 } 674 if (this.preventevent !== null && e.type === this.preventevent) { 675 return true; 676 } 677 if (options.preventevent) { 678 this.preventevent = options.preventevent; 679 if (options.preventdelay) { 680 setTimeout(function() { 681 self.preventevent = null; 682 }, options.preventdelay * 1000); 683 } 684 } 685 if (this.delayedevent && this.delayedevent.timeout) { 686 clearTimeout(this.delayedevent.timeout); 687 this.delayedevent.event.detach(); 688 this.delayedevent = null; 689 } 690 if (options.delay > 0) { 691 return this.delayEvent(e, options, target); 692 } 693 targetid = target.get('id'); 694 if (targetid.match(regex)) { 695 item = this.dockeditems[targetid.replace(regex, '$1')]; 696 if (item.active) { 697 item.hide(); 698 } else { 699 item.show(); 700 } 701 } else if (item) { 702 item.hide(); 703 } 704 } 705 return true; 706 }, 707 /** 708 * Delays an event. 709 * 710 * @method delayEvent 711 * @param {EventFacade} event 712 * @param {Object} options 713 * @param {Node} target 714 * @return {Boolean} 715 */ 716 delayEvent: function(event, options, target) { 717 var self = this; 718 self.delayedevent = (function() { 719 return { 720 target: target, 721 event: BODY.on('mousemove', function(e) { 722 self.delayedevent.target = e.target; 723 }), 724 timeout: null 725 }; 726 })(self); 727 self.delayedevent.timeout = setTimeout(function() { 728 self.delayedevent.timeout = null; 729 self.delayedevent.event.detach(); 730 if (options.iscontained === self.get('dockNode').contains(self.delayedevent.target)) { 731 self.handleEvent(event, {cssselector: options.cssselector, delay: 0, iscontained: options.iscontained}); 732 } 733 }, options.delay * 1000); 734 return true; 735 }, 736 /** 737 * Resizes block spaces. 738 * @method resizeBlockSpace 739 */ 740 resizeBlockSpace: function() { 741 if (Y.all(SELECTOR.dockonload).size() > 0) { 742 // Do not resize during initial load 743 return; 744 } 745 746 var populatedRegionCount = 0, 747 populatedBlockRegions = [], 748 unpopulatedBlockRegions = [], 749 isMoving = false, 750 populatedLegacyRegions = [], 751 containsLegacyRegions = false, 752 classesToAdd = [], 753 classesToRemove = []; 754 755 // First look for understood regions. 756 Y.all(SELECTOR.blockregion).each(function(region) { 757 var regionname = region.getData('blockregion'); 758 if (region.all('.block').size() > 0) { 759 populatedBlockRegions.push(regionname); 760 populatedRegionCount++; 761 } else if (region.all('.block_dock_placeholder').size() > 0) { 762 unpopulatedBlockRegions.push(regionname); 763 } 764 }); 765 766 // Next check for legacy regions. 767 Y.all('.block-region').each(function(region) { 768 if (region.test(SELECTOR.blockregion)) { 769 // This is a new region, we've already processed it. 770 return; 771 } 772 773 // Sigh - there are legacy regions. 774 containsLegacyRegions = true; 775 776 var regionname = region.get('id').replace(/^region\-/, 'side-'), 777 hasblocks = (region.all('.block').size() > 0); 778 779 if (hasblocks) { 780 populatedLegacyRegions.push(regionname); 781 populatedRegionCount++; 782 } else { 783 // This legacy region has no blocks so cannot have the -only body tag. 784 classesToRemove.push( 785 regionname + '-only' 786 ); 787 } 788 }); 789 790 if (BODY.hasClass('blocks-moving')) { 791 // When we're moving blocks, we do not want to collapse. 792 isMoving = true; 793 } 794 795 Y.each(unpopulatedBlockRegions, function(regionname) { 796 classesToAdd.push( 797 // This block region is empty. 798 'empty-region-' + regionname, 799 800 // Which has the same effect as being docked. 801 'docked-region-' + regionname 802 ); 803 classesToRemove.push( 804 // It is no-longer used. 805 'used-region-' + regionname, 806 807 // It cannot be the only region on screen if it is empty. 808 regionname + '-only' 809 ); 810 }, this); 811 812 Y.each(populatedBlockRegions, function(regionname) { 813 classesToAdd.push( 814 // This block region is in use. 815 'used-region-' + regionname 816 ); 817 classesToRemove.push( 818 // It is not empty. 819 'empty-region-' + regionname, 820 821 // Is it not docked. 822 'docked-region-' + regionname 823 ); 824 825 if (populatedRegionCount === 1 && isMoving === false) { 826 // There was only one populated region, and we are not moving blocks. 827 classesToAdd.push(regionname + '-only'); 828 } else { 829 // There were multiple block regions visible - remove any 'only' classes. 830 classesToRemove.push(regionname + '-only'); 831 } 832 }, this); 833 834 if (containsLegacyRegions) { 835 // Handle the classing for legacy blocks. These have slightly different class names for the body. 836 if (isMoving || populatedRegionCount !== 1) { 837 Y.each(populatedLegacyRegions, function(regionname) { 838 classesToRemove.push(regionname + '-only'); 839 }); 840 } else { 841 Y.each(populatedLegacyRegions, function(regionname) { 842 classesToAdd.push(regionname + '-only'); 843 }); 844 } 845 } 846 847 if (!BODY.hasClass('has-region-content')) { 848 // This page does not have a content region, therefore content-only is implied when all block regions are docked. 849 if (populatedRegionCount === 0 && isMoving === false) { 850 // If all blocks are docked, ensure that the content-only class is added anyway. 851 classesToAdd.push('content-only'); 852 } else { 853 // Otherwise remove it. 854 classesToRemove.push('content-only'); 855 } 856 } 857 858 // Modify the body clases. 859 Y.each(classesToRemove, function(className) { 860 BODY.removeClass(className); 861 }); 862 Y.each(classesToAdd, function(className) { 863 BODY.addClass(className); 864 }); 865 }, 866 /** 867 * Adds an item to the dock. 868 * @method add 869 * @param {DOCKEDITEM} item 870 */ 871 add: function(item) { 872 // Set the dockitem id to the total count and then increment it. 873 item.set('id', this.totalcount); 874 Y.log('Adding block ' + item._getLogDescription() + ' to the dock.', 'debug', LOGNS); 875 this.count++; 876 this.totalcount++; 877 this.dockeditems[item.get('id')] = item; 878 this.dockeditems[item.get('id')].draw(); 879 this.fire('dock:itemadded', item); 880 this.fire('dock:itemschanged', item); 881 }, 882 /** 883 * Appends an item to the dock (putting it in the item container. 884 * @method append 885 * @param {Node} docknode 886 */ 887 append: function(docknode) { 888 this.get('itemContainerNode').append(docknode); 889 }, 890 /** 891 * Handles events that require a docked block to be returned to the page./ 892 * @method handleReturnToBlock 893 * @param {EventFacade} e 894 */ 895 handleReturnToBlock: function(e) { 896 e.halt(); 897 this.remove(this.getActiveItem().get('id')); 898 }, 899 /** 900 * Removes a docked item from the dock. 901 * @method remove 902 * @param {Number} id The docked item id. 903 * @return {Boolean} 904 */ 905 remove: function(id) { 906 if (!this.dockeditems[id]) { 907 return false; 908 } 909 Y.log('Removing block ' + this.dockeditems[id]._getLogDescription() + ' from the dock.', 'debug', LOGNS); 910 this.dockeditems[id].remove(); 911 delete this.dockeditems[id]; 912 this.count--; 913 this.fire('dock:itemremoved', id); 914 this.fire('dock:itemschanged', id); 915 return true; 916 }, 917 /** 918 * Ensures the the first item in the dock has the correct class. 919 * @method resetFirstItem 920 */ 921 resetFirstItem: function() { 922 this.get('dockNode').all('.' + CSS.dockeditem + '.firstdockitem').removeClass('firstdockitem'); 923 if (this.get('dockNode').one('.' + CSS.dockeditem)) { 924 this.get('dockNode').one('.' + CSS.dockeditem).addClass('firstdockitem'); 925 } 926 }, 927 /** 928 * Removes all docked blocks returning them to the page. 929 * @method removeAll 930 * @return {Boolean} 931 */ 932 removeAll: function() { 933 Y.log('Undocking all ' + this.dockeditems.length + ' blocks', 'debug', LOGNS); 934 var i; 935 for (i in this.dockeditems) { 936 if (Y.Lang.isNumber(i) || Y.Lang.isString(i)) { 937 this.remove(i); 938 } 939 } 940 return true; 941 }, 942 /** 943 * Hides the active item. 944 * @method hideActive 945 */ 946 hideActive: function() { 947 var item = this.getActiveItem(); 948 if (item) { 949 item.hide(); 950 } 951 }, 952 /** 953 * Checks wether the dock should be shown or hidden 954 * @method checkDockVisibility 955 */ 956 checkDockVisibility: function() { 957 var bodyclass = CSS.body + '_' + this.get('position') + '_' + this.get('orientation'); 958 if (!this.count) { 959 this.get('dockNode').addClass('nothingdocked'); 960 BODY.removeClass(CSS.body).removeClass(); 961 this.fire('dock:hidden'); 962 } else { 963 this.fire('dock:beforeshow'); 964 this.get('dockNode').removeClass('nothingdocked'); 965 BODY.addClass(CSS.body).addClass(bodyclass); 966 this.fire('dock:shown'); 967 } 968 }, 969 /** 970 * This function checks the size and position of the panel and moves/resizes if 971 * required to keep it within the bounds of the window. 972 * @method resize 973 * @return {Boolean} 974 */ 975 resize: function() { 976 var panel = this.getPanel(), 977 item = this.getActiveItem(), 978 buffer, 979 screenh, 980 docky, 981 titletop, 982 containery, 983 containerheight, 984 scrolltop, 985 panelheight, 986 dockx, 987 titleleft; 988 if (!panel.get('visible') || !item) { 989 return true; 990 } 991 992 this.fire('dock:panelresizestart'); 993 if (this.get('orientation') === 'vertical') { 994 buffer = this.get('bufferPanel'); 995 screenh = parseInt(BODY.get('winHeight'), 10) - (buffer * 2); 996 docky = this.get('dockNode').getY(); 997 titletop = item.get('dockTitleNode').getY() - docky - buffer; 998 containery = this.get('itemContainerNode').getY(); 999 containerheight = containery - docky + this.get('buttonsNode').get('offsetHeight'); 1000 scrolltop = panel.get('bodyNode').get('scrollTop'); 1001 panel.get('bodyNode').setStyle('height', 'auto'); 1002 panel.get('node').removeClass('oversized_content'); 1003 panelheight = panel.get('node').get('offsetHeight'); 1004 1005 if (Y.UA.ie > 0 && Y.UA.ie < 7) { 1006 panel.setTop(item.get('dockTitleNode').getY()); 1007 } else if (panelheight > screenh) { 1008 panel.setTop(buffer - containerheight); 1009 panel.get('bodyNode').setStyle('height', (screenh - panel.get('headerNode').get('offsetHeight')) + 'px'); 1010 panel.get('node').addClass('oversized_content'); 1011 } else if (panelheight > (screenh - (titletop - buffer))) { 1012 panel.setTop(titletop - containerheight - (panelheight - (screenh - titletop)) + buffer); 1013 } else { 1014 panel.setTop(titletop - containerheight + buffer); 1015 } 1016 1017 if (scrolltop) { 1018 panel.get('bodyNode').set('scrollTop', scrolltop); 1019 } 1020 } 1021 1022 if (this.get('position') === 'right') { 1023 panel.get('node').setStyle('left', '-' + panel.get('node').get('offsetWidth') + 'px'); 1024 1025 } else if (this.get('position') === 'top') { 1026 dockx = this.get('dockNode').getX(); 1027 titleleft = item.get('dockTitleNode').getX() - dockx; 1028 panel.get('node').setStyle('left', titleleft + 'px'); 1029 } 1030 1031 this.fire('dock:resizepanelcomplete'); 1032 return true; 1033 }, 1034 /** 1035 * Returns the currently active dock item or false 1036 * @method getActiveItem 1037 * @return {DOCKEDITEM} 1038 */ 1039 getActiveItem: function() { 1040 var i; 1041 for (i in this.dockeditems) { 1042 if (this.dockeditems[i].active) { 1043 return this.dockeditems[i]; 1044 } 1045 } 1046 return false; 1047 }, 1048 /** 1049 * Adds an item to the holding area. 1050 * @method addToHoldingArea 1051 * @param {Node} node 1052 */ 1053 addToHoldingArea: function(node) { 1054 this.holdingareanode.append(node); 1055 } 1056 }; 1057 1058 Y.extend(DOCK, Y.Base, DOCK.prototype, { 1059 NAME: 'moodle-core-dock', 1060 ATTRS: { 1061 /** 1062 * The dock itself. #dock. 1063 * @attribute dockNode 1064 * @type Node 1065 * @writeOnce 1066 */ 1067 dockNode: { 1068 writeOnce: true 1069 }, 1070 /** 1071 * The docks panel. 1072 * @attribute panel 1073 * @type DOCKPANEL 1074 * @writeOnce 1075 */ 1076 panel: { 1077 writeOnce: true 1078 }, 1079 /** 1080 * A container within the dock used for buttons. 1081 * @attribute buttonsNode 1082 * @type Node 1083 * @writeOnce 1084 */ 1085 buttonsNode: { 1086 writeOnce: true 1087 }, 1088 /** 1089 * A container within the dock used for docked blocks. 1090 * @attribute itemContainerNode 1091 * @type Node 1092 * @writeOnce 1093 */ 1094 itemContainerNode: { 1095 writeOnce: true 1096 }, 1097 1098 /** 1099 * Buffer used when containing a panel. 1100 * @attribute bufferPanel 1101 * @type Number 1102 * @default 10 1103 */ 1104 bufferPanel: { 1105 value: 10, 1106 validator: Y.Lang.isNumber 1107 }, 1108 1109 /** 1110 * Position of the dock. 1111 * @attribute position 1112 * @type String 1113 * @default left 1114 */ 1115 position: { 1116 value: 'left', 1117 validator: Y.Lang.isString 1118 }, 1119 1120 /** 1121 * vertical || horizontal determines if we change the title 1122 * @attribute orientation 1123 * @type String 1124 * @default vertical 1125 */ 1126 orientation: { 1127 value: 'vertical', 1128 validator: Y.Lang.isString, 1129 setter: function(value) { 1130 if (value.match(/^vertical$/i)) { 1131 return 'vertical'; 1132 } 1133 return 'horizontal'; 1134 } 1135 }, 1136 1137 /** 1138 * Space between the top of the dock and the first item. 1139 * @attribute bufferBeforeFirstItem 1140 * @type Number 1141 * @default 10 1142 */ 1143 bufferBeforeFirstItem: { 1144 value: 10, 1145 validator: Y.Lang.isNumber 1146 }, 1147 1148 /** 1149 * Icon URL for the icon to undock all blocks 1150 * @attribute undockAllIconUrl 1151 * @type String 1152 * @default t/dock_to_block 1153 */ 1154 undockAllIconUrl: { 1155 value: M.util.image_url((window.right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block', 'moodle'), 1156 validator: Y.Lang.isString 1157 } 1158 } 1159 }); 1160 Y.augment(DOCK, Y.EventTarget); 1161 /* global DOCKPANEL, LOGNS */ 1162 1163 /** 1164 * Dock JS. 1165 * 1166 * This file contains the panel class used by the dock to display the content of docked blocks. 1167 * 1168 * @module moodle-core-dock 1169 */ 1170 1171 /** 1172 * Panel. 1173 * 1174 * @namespace M.core.dock 1175 * @class Panel 1176 * @constructor 1177 * @extends Base 1178 * @uses EventTarget 1179 */ 1180 DOCKPANEL = function() { 1181 DOCKPANEL.superclass.constructor.apply(this, arguments); 1182 }; 1183 DOCKPANEL.prototype = { 1184 /** 1185 * True once the panel has been created. 1186 * @property created 1187 * @protected 1188 * @type {Boolean} 1189 */ 1190 created: false, 1191 /** 1192 * Called during the initialisation process of the object. 1193 * @method initializer 1194 */ 1195 initializer: function() { 1196 Y.log('Panel initialising', 'debug', LOGNS); 1197 /** 1198 * Fired before the panel is shown. 1199 * @event dockpane::beforeshow 1200 */ 1201 this.publish('dockpanel:beforeshow', {prefix: 'dockpanel'}); 1202 /** 1203 * Fired after the panel is shown. 1204 * @event dockpanel:shown 1205 */ 1206 this.publish('dockpanel:shown', {prefix: 'dockpanel'}); 1207 /** 1208 * Fired before the panel is hidden. 1209 * @event dockpane::beforehide 1210 */ 1211 this.publish('dockpanel:beforehide', {prefix: 'dockpanel'}); 1212 /** 1213 * Fired after the panel is hidden. 1214 * @event dockpanel:hidden 1215 */ 1216 this.publish('dockpanel:hidden', {prefix: 'dockpanel'}); 1217 /** 1218 * Fired when ever the dock panel is either hidden or shown. 1219 * Always fired after the shown or hidden events. 1220 * @event dockpanel:visiblechange 1221 */ 1222 this.publish('dockpanel:visiblechange', {prefix: 'dockpanel'}); 1223 }, 1224 /** 1225 * Creates the Panel if it has not already been created. 1226 * @method create 1227 * @return {Boolean} 1228 */ 1229 create: function() { 1230 if (this.created) { 1231 return true; 1232 } 1233 this.created = true; 1234 var dock = this.get('dock'), 1235 node = dock.get('dockNode'); 1236 this.set('node', Y.Node.create('<div id="dockeditempanel" class="dockitempanel_hidden"></div>')); 1237 this.set('contentNode', Y.Node.create('<div class="dockeditempanel_content"></div>')); 1238 this.set('headerNode', Y.Node.create('<div class="dockeditempanel_hd"></div>')); 1239 this.set('bodyNode', Y.Node.create('<div class="dockeditempanel_bd"></div>')); 1240 node.append( 1241 this.get('node').append(this.get('contentNode').append(this.get('headerNode')).append(this.get('bodyNode'))) 1242 ); 1243 }, 1244 /** 1245 * Displays the panel. 1246 * @method show 1247 */ 1248 show: function() { 1249 this.create(); 1250 this.fire('dockpanel:beforeshow'); 1251 this.set('visible', true); 1252 this.get('node').removeClass('dockitempanel_hidden'); 1253 this.fire('dockpanel:shown'); 1254 this.fire('dockpanel:visiblechange'); 1255 }, 1256 /** 1257 * Hides the panel 1258 * @method hide 1259 */ 1260 hide: function() { 1261 this.fire('dockpanel:beforehide'); 1262 this.set('visible', false); 1263 this.get('node').addClass('dockitempanel_hidden'); 1264 this.fire('dockpanel:hidden'); 1265 this.fire('dockpanel:visiblechange'); 1266 }, 1267 /** 1268 * Sets the panel header. 1269 * @method setHeader 1270 * @param {Node|String} content 1271 */ 1272 setHeader: function(content) { 1273 this.create(); 1274 var header = this.get('headerNode'), 1275 i; 1276 header.setContent(content); 1277 if (arguments.length > 1) { 1278 for (i = 1; i < arguments.length; i++) { 1279 if (Y.Lang.isNumber(i) || Y.Lang.isString(i)) { 1280 header.append(arguments[i]); 1281 } 1282 } 1283 } 1284 }, 1285 /** 1286 * Sets the panel body. 1287 * @method setBody 1288 * @param {Node|String} content 1289 */ 1290 setBody: function(content) { 1291 this.create(); 1292 this.get('bodyNode').setContent(content); 1293 }, 1294 /** 1295 * Sets the new top mark of the panel. 1296 * 1297 * @method setTop 1298 * @param {Number} newtop 1299 */ 1300 setTop: function(newtop) { 1301 if (Y.UA.ie > 0 && Y.UA.ie < 7) { 1302 this.get('node').setY(newtop); 1303 } else { 1304 this.get('node').setStyle('top', newtop.toString() + 'px'); 1305 } 1306 }, 1307 /** 1308 * Corrects the width of the panel. 1309 * @method correctWidth 1310 */ 1311 correctWidth: function() { 1312 var bodyNode = this.get('bodyNode'), 1313 // Width of content. 1314 width = bodyNode.get('clientWidth'), 1315 // Scrollable width of content. 1316 scroll = bodyNode.get('scrollWidth'), 1317 // Width of content container with overflow. 1318 offsetWidth = bodyNode.get('offsetWidth'), 1319 // The new width - defaults to the current width. 1320 newWidth = width, 1321 // The max width (80% of screen). 1322 maxWidth = Math.round(bodyNode.get('winWidth') * 0.8); 1323 1324 // If the scrollable width is more than the visible width 1325 if (scroll > width) { 1326 // Content width 1327 // + the difference 1328 // + any rendering difference (borders, padding) 1329 // + 10px to make it look nice. 1330 newWidth = width + (scroll - width) + ((offsetWidth - width) * 2) + 10; 1331 } 1332 1333 // Make sure its not more then the maxwidth 1334 if (newWidth > maxWidth) { 1335 newWidth = maxWidth; 1336 } 1337 1338 // Set the new width if its more than the old width. 1339 if (newWidth > offsetWidth) { 1340 this.get('node').setStyle('width', newWidth + 'px'); 1341 } 1342 } 1343 }; 1344 Y.extend(DOCKPANEL, Y.Base, DOCKPANEL.prototype, { 1345 NAME: 'moodle-core-dock-panel', 1346 ATTRS: { 1347 /** 1348 * The dock itself. 1349 * @attribute dock 1350 * @type DOCK 1351 * @writeonce 1352 */ 1353 dock: { 1354 writeOnce: 'initOnly' 1355 }, 1356 /** 1357 * The node that contains the whole panel. 1358 * @attribute node 1359 * @type Node 1360 */ 1361 node: { 1362 value: null 1363 }, 1364 /** 1365 * The node that contains the header, body and footer. 1366 * @attribute contentNode 1367 * @type Node 1368 */ 1369 contentNode: { 1370 value: null 1371 }, 1372 /** 1373 * The node that contains the header 1374 * @attribute headerNode 1375 * @type Node 1376 */ 1377 headerNode: { 1378 value: null 1379 }, 1380 /** 1381 * The node that contains the body 1382 * @attribute bodyNode 1383 * @type Node 1384 */ 1385 bodyNode: { 1386 value: null 1387 }, 1388 /** 1389 * True if the panel is currently visible. 1390 * @attribute visible 1391 * @type Boolean 1392 */ 1393 visible: { 1394 value: false 1395 } 1396 } 1397 }); 1398 Y.augment(DOCKPANEL, Y.EventTarget); 1399 /* global TABHEIGHTMANAGER, LOGNS */ 1400 1401 /** 1402 * Dock JS. 1403 * 1404 * This file contains the tab height manager. 1405 * The tab height manager is responsible for ensure all tabs are visible all the time. 1406 * 1407 * @module moodle-core-dock 1408 */ 1409 1410 /** 1411 * Tab height manager. 1412 * 1413 * @namespace M.core.dock 1414 * @class TabHeightManager 1415 * @constructor 1416 * @extends Base 1417 */ 1418 TABHEIGHTMANAGER = function() { 1419 TABHEIGHTMANAGER.superclass.constructor.apply(this, arguments); 1420 }; 1421 TABHEIGHTMANAGER.prototype = { 1422 /** 1423 * Initialises the dock sizer which then attaches itself to the required 1424 * events in order to monitor the dock 1425 * @method initializer 1426 */ 1427 initializer: function() { 1428 var dock = this.get('dock'); 1429 dock.on('dock:itemschanged', this.checkSizing, this); 1430 Y.on('windowresize', this.checkSizing, this); 1431 }, 1432 /** 1433 * Check if the size dock items needs to be adjusted 1434 * @method checkSizing 1435 */ 1436 checkSizing: function() { 1437 var dock = this.get('dock'), 1438 node = dock.get('dockNode'), 1439 items = dock.dockeditems, 1440 containermargin = parseInt(node.one('.dockeditem_container').getStyle('marginTop').replace('/[^0-9]+$/', ''), 10), 1441 dockheight = node.get('offsetHeight') - containermargin, 1442 controlheight = node.one('.controls').get('offsetHeight'), 1443 buffer = (dock.get('bufferPanel') * 3), 1444 possibleheight = dockheight - controlheight - buffer - (items.length * 2), 1445 totalheight = 0, 1446 id, dockedtitle; 1447 if (items.length > 0) { 1448 for (id in items) { 1449 if (Y.Lang.isNumber(id) || Y.Lang.isString(id)) { 1450 dockedtitle = Y.one(items[id].get('title')).ancestor('.' + CSS.dockedtitle); 1451 if (dockedtitle) { 1452 if (this.get('enabled')) { 1453 dockedtitle.setStyle('height', 'auto'); 1454 } 1455 totalheight += dockedtitle.get('offsetHeight') || 0; 1456 } 1457 } 1458 } 1459 if (totalheight > possibleheight) { 1460 this.enable(possibleheight); 1461 } 1462 } 1463 }, 1464 /** 1465 * Enables the dock sizer and resizes where required. 1466 * @method enable 1467 * @param {Number} possibleheight 1468 */ 1469 enable: function(possibleheight) { 1470 var dock = this.get('dock'), 1471 items = dock.dockeditems, 1472 count = dock.count, 1473 runningcount = 0, 1474 usedheight = 0, 1475 id, itemtitle, itemheight, offsetheight; 1476 Y.log('Enabling the dock tab sizer.', 'debug', LOGNS); 1477 this.set('enabled', true); 1478 for (id in items) { 1479 if (Y.Lang.isNumber(id) || Y.Lang.isString(id)) { 1480 itemtitle = Y.one(items[id].get('title')).ancestor('.' + CSS.dockedtitle); 1481 if (!itemtitle) { 1482 continue; 1483 } 1484 itemheight = Math.floor((possibleheight - usedheight) / (count - runningcount)); 1485 offsetheight = itemtitle.get('offsetHeight'); 1486 itemtitle.setStyle('overflow', 'hidden'); 1487 if (offsetheight > itemheight) { 1488 itemtitle.setStyle('height', itemheight + 'px'); 1489 usedheight += itemheight; 1490 } else { 1491 usedheight += offsetheight; 1492 } 1493 runningcount++; 1494 } 1495 } 1496 } 1497 }; 1498 Y.extend(TABHEIGHTMANAGER, Y.Base, TABHEIGHTMANAGER.prototype, { 1499 NAME: 'moodle-core-tabheightmanager', 1500 ATTRS: { 1501 /** 1502 * The dock. 1503 * @attribute dock 1504 * @type DOCK 1505 * @writeOnce 1506 */ 1507 dock: { 1508 writeOnce: 'initOnly' 1509 }, 1510 /** 1511 * True if the item_sizer is being used, false otherwise. 1512 * @attribute enabled 1513 * @type Bool 1514 */ 1515 enabled: { 1516 value: false 1517 } 1518 } 1519 }); 1520 /** 1521 * Dock JS. 1522 * 1523 * This file contains the action key event definition that is used for accessibility handling within the Dock. 1524 * 1525 * @module moodle-core-dock 1526 */ 1527 1528 /** 1529 * A 'dock:actionkey' Event. 1530 * The event consists of the left arrow, right arrow, enter and space keys. 1531 * More keys can be mapped to action meanings. 1532 * actions: collapse , expand, toggle, enter. 1533 * 1534 * This event is subscribed to by dockitems. 1535 * The on() method to subscribe allows specifying the desired trigger actions as JSON. 1536 * 1537 * This event can also be delegated if needed. 1538 * 1539 * @namespace M.core.dock 1540 * @class ActionKey 1541 */ 1542 Y.Event.define("dock:actionkey", { 1543 // Webkit and IE repeat keydown when you hold down arrow keys. 1544 // Opera links keypress to page scroll; others keydown. 1545 // Firefox prevents page scroll via preventDefault() on either 1546 // keydown or keypress. 1547 _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress', 1548 1549 /** 1550 * The keys to trigger on. 1551 * @property _keys 1552 */ 1553 _keys: { 1554 // arrows 1555 '37': 'collapse', 1556 '39': 'expand', 1557 // (@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings) 1558 '32': 'toggle', 1559 '13': 'enter' 1560 }, 1561 1562 /** 1563 * Handles key events 1564 * @method _keyHandler 1565 * @param {EventFacade} e 1566 * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers 1567 * @param {Object} args 1568 */ 1569 _keyHandler: function(e, notifier, args) { 1570 var actObj; 1571 if (!args.actions) { 1572 actObj = {collapse: true, expand: true, toggle: true, enter: true}; 1573 } else { 1574 actObj = args.actions; 1575 } 1576 if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) { 1577 e.action = this._keys[e.keyCode]; 1578 notifier.fire(e); 1579 } 1580 }, 1581 1582 /** 1583 * Subscribes to events. 1584 * @method on 1585 * @param {Node} node The node this subscription was applied to. 1586 * @param {Subscription} sub The object tracking this subscription. 1587 * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers 1588 */ 1589 on: function(node, sub, notifier) { 1590 // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). 1591 if (sub.args === null) { 1592 // no actions given 1593 sub._detacher = node.on(this._event, this._keyHandler, this, notifier, {actions: false}); 1594 } else { 1595 sub._detacher = node.on(this._event, this._keyHandler, this, notifier, sub.args[0]); 1596 } 1597 }, 1598 1599 /** 1600 * Detaches an event listener 1601 * @method detach 1602 * @param {Node} node The node this subscription was applied to. 1603 * @param {Subscription} sub The object tracking this subscription. 1604 * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers 1605 */ 1606 detach: function(node, sub) { 1607 // detach our _detacher handle of the subscription made in on() 1608 sub._detacher.detach(); 1609 }, 1610 1611 /** 1612 * Creates a delegated event listener. 1613 * @method delegate 1614 * @param {Node} node The node this subscription was applied to. 1615 * @param {Subscription} sub The object tracking this subscription. 1616 * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers 1617 * @param {String|function} filter Selector string or function that accpets an event object and returns null. 1618 */ 1619 delegate: function(node, sub, notifier, filter) { 1620 // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). 1621 if (sub.args === null) { 1622 // no actions given 1623 sub._delegateDetacher = node.delegate(this._event, this._keyHandler, filter, this, notifier, {actions: false}); 1624 } else { 1625 sub._delegateDetacher = node.delegate(this._event, this._keyHandler, filter, this, notifier, sub.args[0]); 1626 } 1627 }, 1628 1629 /** 1630 * Detaches a delegated event listener. 1631 * @method detachDelegate 1632 * @param {Node} node The node this subscription was applied to. 1633 * @param {Subscription} sub The object tracking this subscription. 1634 * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers 1635 * @param {String|function} filter Selector string or function that accpets an event object and returns null. 1636 */ 1637 detachDelegate: function(node, sub) { 1638 sub._delegateDetacher.detach(); 1639 } 1640 }); 1641 /* global BLOCK, LOGNS, DOCKEDITEM */ 1642 1643 /** 1644 * Dock JS. 1645 * 1646 * This file contains the block class used to manage blocks (both docked and not) for the dock. 1647 * 1648 * @module moodle-core-dock 1649 */ 1650 1651 /** 1652 * Block. 1653 * 1654 * @namespace M.core.dock 1655 * @class Block 1656 * @constructor 1657 * @extends Base 1658 */ 1659 BLOCK = function() { 1660 BLOCK.superclass.constructor.apply(this, arguments); 1661 }; 1662 BLOCK.prototype = { 1663 /** 1664 * A content place holder used when the block has been docked. 1665 * @property contentplaceholder 1666 * @protected 1667 * @type Node 1668 */ 1669 contentplaceholder: null, 1670 /** 1671 * The skip link associated with this block. 1672 * @property contentskipanchor 1673 * @protected 1674 * @type Node 1675 */ 1676 contentskipanchor: null, 1677 /** 1678 * The cached content node for the actual block 1679 * @property cachedcontentnode 1680 * @protected 1681 * @type Node 1682 */ 1683 cachedcontentnode: null, 1684 /** 1685 * If true the user preference isn't updated 1686 * @property skipsetposition 1687 * @protected 1688 * @type Boolean 1689 */ 1690 skipsetposition: true, 1691 /** 1692 * The dock item associated with this block 1693 * @property dockitem 1694 * @protected 1695 * @type DOCKEDITEM 1696 */ 1697 dockitem: null, 1698 /** 1699 * Called during the initialisation process of the object. 1700 * @method initializer 1701 */ 1702 initializer: function() { 1703 var node = Y.one('#inst' + this.get('id')); 1704 if (!node) { 1705 return false; 1706 } 1707 1708 Y.log('Initialised block with instance id:' + this.get('id'), 'debug', LOGNS); 1709 1710 M.core.dock.ensureMoveToIconExists(node); 1711 1712 // Move the block straight to the dock if required 1713 if (node.hasClass(CSS.dockonload)) { 1714 node.removeClass(CSS.dockonload); 1715 this.moveToDock(); 1716 } 1717 this.skipsetposition = false; 1718 return true; 1719 }, 1720 /** 1721 * Returns the class associated with this block. 1722 * @method _getBlockClass 1723 * @private 1724 * @param {Node} node 1725 * @return String 1726 */ 1727 _getBlockClass: function(node) { 1728 var block = node.getData('block'), 1729 classes, 1730 matches; 1731 if (Y.Lang.isString(block) && block !== '') { 1732 return block; 1733 } 1734 classes = node.getAttribute('className').toString(); 1735 matches = /(^| )block_([^ ]+)/.exec(classes); 1736 if (matches) { 1737 return matches[2]; 1738 } 1739 return matches; 1740 }, 1741 1742 /** 1743 * This function is responsible for moving a block from the page structure onto the dock. 1744 * @method moveToDock 1745 * @param {EventFacade} e 1746 */ 1747 moveToDock: function(e) { 1748 if (e) { 1749 e.halt(true); 1750 } 1751 1752 var dock = M.core.dock.get(), 1753 id = this.get('id'), 1754 blockcontent = Y.one('#inst' + id).one('.content'), 1755 icon = (window.right_to_left()) ? 't/dock_to_block_rtl' : 't/dock_to_block', 1756 breakchar = (location.href.match(/\?/)) ? '&' : '?', 1757 blocktitle, 1758 blockcommands, 1759 movetoimg, 1760 moveto; 1761 1762 if (!blockcontent) { 1763 return; 1764 } 1765 1766 Y.log('Moving block to the dock:' + this.get('id'), 'debug', LOGNS); 1767 1768 this.recordBlockState(); 1769 1770 blocktitle = this.cachedcontentnode.one('.title h2').cloneNode(true); 1771 1772 // Build up the block commands. 1773 // These should not actually added to the DOM. 1774 blockcommands = this.cachedcontentnode.one('.title .commands'); 1775 if (blockcommands) { 1776 blockcommands = blockcommands.cloneNode(true); 1777 } else { 1778 blockcommands = Y.Node.create('<div class="commands"></div>'); 1779 } 1780 movetoimg = Y.Node.create('<img />').setAttrs({ 1781 alt: Y.Escape.html(M.util.get_string('undockitem', 'block')), 1782 title: Y.Escape.html(M.util.get_string('undockblock', 'block', blocktitle.get('innerHTML'))), 1783 src: M.util.image_url(icon, 'moodle') 1784 }); 1785 moveto = Y.Node.create('<a class="moveto customcommand requiresjs"></a>').setAttrs({ 1786 href: Y.config.win.location.href + breakchar + 'dock=' + id 1787 }); 1788 moveto.append(movetoimg); 1789 blockcommands.append(moveto.append(movetoimg)); 1790 1791 // Create a new dock item for the block 1792 this.dockitem = new DOCKEDITEM({ 1793 block: this, 1794 dock: dock, 1795 blockinstanceid: id, 1796 title: blocktitle, 1797 contents: blockcontent, 1798 commands: blockcommands, 1799 blockclass: this._getBlockClass(Y.one('#inst' + id)) 1800 }); 1801 // Register an event so that when it is removed we can put it back as a block 1802 dock.add(this.dockitem); 1803 1804 if (!this.skipsetposition) { 1805 // save the users preference 1806 M.util.set_user_preference('docked_block_instance_' + id, 1); 1807 } 1808 1809 this.set('isDocked', true); 1810 }, 1811 /** 1812 * Records the block state and adds it to the docks holding area. 1813 * @method recordBlockState 1814 */ 1815 recordBlockState: function() { 1816 var id = this.get('id'), 1817 dock = M.core.dock.get(), 1818 node = Y.one('#inst' + id), 1819 skipanchor = node.previous(); 1820 // Disable the skip anchor when docking 1821 if (skipanchor.hasClass('skip-block')) { 1822 this.contentskipanchor = skipanchor; 1823 this.contentskipanchor.hide(); 1824 } 1825 this.cachedcontentnode = node; 1826 this.contentplaceholder = Y.Node.create('<div class="block_dock_placeholder"></div>'); 1827 node.replace(this.contentplaceholder); 1828 dock.addToHoldingArea(node); 1829 node = null; 1830 }, 1831 1832 /** 1833 * This function removes a block from the dock and puts it back into the page structure. 1834 * @method returnToPage 1835 * @return {Boolean} 1836 */ 1837 returnToPage: function() { 1838 var id = this.get('id'); 1839 1840 Y.log('Moving block out of the dock:' + this.get('id'), 'debug', LOGNS); 1841 1842 // Enable the skip anchor when going back to block mode 1843 if (this.contentskipanchor) { 1844 this.contentskipanchor.show(); 1845 } 1846 1847 if (this.cachedcontentnode.one('.header')) { 1848 this.cachedcontentnode.one('.header').insert(this.dockitem.get('contents'), 'after'); 1849 } else { 1850 this.cachedcontentnode.insert(this.dockitem.get('contents')); 1851 } 1852 1853 this.contentplaceholder.replace(this.cachedcontentnode); 1854 this.cachedcontentnode = null; 1855 1856 M.util.set_user_preference('docked_block_instance_' + id, 0); 1857 this.set('isDocked', false); 1858 return true; 1859 } 1860 }; 1861 Y.extend(BLOCK, Y.Base, BLOCK.prototype, { 1862 NAME: 'moodle-core-dock-block', 1863 ATTRS: { 1864 /** 1865 * The block instance ID 1866 * @attribute id 1867 * @writeOnce 1868 * @type Number 1869 */ 1870 id: { 1871 writeOnce: 'initOnly', 1872 setter: function(value) { 1873 return parseInt(value, 10); 1874 } 1875 }, 1876 /** 1877 * True if the block has been docked. 1878 * @attribute isDocked 1879 * @default false 1880 * @type Boolean 1881 */ 1882 isDocked: { 1883 value: false 1884 } 1885 } 1886 }); 1887 /* global LOGNS, DOCKEDITEM */ 1888 1889 /** 1890 * Dock JS. 1891 * 1892 * This file contains the docked item class. 1893 * 1894 * @module moodle-core-dock 1895 */ 1896 1897 /** 1898 * Docked item. 1899 * 1900 * @namespace M.core.dock 1901 * @class DockedItem 1902 * @constructor 1903 * @extends Base 1904 * @uses EventTarget 1905 */ 1906 DOCKEDITEM = function() { 1907 DOCKEDITEM.superclass.constructor.apply(this, arguments); 1908 }; 1909 DOCKEDITEM.prototype = { 1910 /** 1911 * Set to true if this item is currently being displayed. 1912 * @property active 1913 * @protected 1914 * @type Boolean 1915 */ 1916 active: false, 1917 /** 1918 * Called during the initialisation process of the object. 1919 * @method initializer 1920 */ 1921 initializer: function() { 1922 var title = this.get('title'), 1923 titlestring, 1924 type; 1925 /** 1926 * Fired before the docked item has been drawn. 1927 * @event dockeditem:drawstart 1928 */ 1929 this.publish('dockeditem:drawstart', {prefix: 'dockeditem'}); 1930 /** 1931 * Fired after the docked item has been drawn. 1932 * @event dockeditem:drawcomplete 1933 */ 1934 this.publish('dockeditem:drawcomplete', {prefix: 'dockeditem'}); 1935 /** 1936 * Fired before the docked item is to be shown. 1937 * @event dockeditem:showstart 1938 */ 1939 this.publish('dockeditem:showstart', {prefix: 'dockeditem'}); 1940 /** 1941 * Fired after the docked item has been shown. 1942 * @event dockeditem:showcomplete 1943 */ 1944 this.publish('dockeditem:showcomplete', {prefix: 'dockeditem'}); 1945 /** 1946 * Fired before the docked item has been hidden. 1947 * @event dockeditem:hidestart 1948 */ 1949 this.publish('dockeditem:hidestart', {prefix: 'dockeditem'}); 1950 /** 1951 * Fired after the docked item has been hidden. 1952 * @event dockeditem:hidecomplete 1953 */ 1954 this.publish('dockeditem:hidecomplete', {prefix: 'dockeditem'}); 1955 /** 1956 * Fired when the docked item is removed from the dock. 1957 * @event dockeditem:itemremoved 1958 */ 1959 this.publish('dockeditem:itemremoved', {prefix: 'dockeditem'}); 1960 if (title) { 1961 type = title.get('nodeName'); 1962 titlestring = title.cloneNode(true); 1963 title = Y.Node.create('<' + type + '></' + type + '>'); 1964 title = M.core.dock.fixTitleOrientation(title, titlestring.get('text')); 1965 this.set('title', title); 1966 this.set('titlestring', titlestring); 1967 } 1968 Y.log('Initialised dockeditem for block with title "' + this._getLogDescription(), 'debug', LOGNS); 1969 }, 1970 /** 1971 * This function draws the item on the dock. 1972 * @method draw 1973 * @return Boolean 1974 */ 1975 draw: function() { 1976 var create = Y.Node.create, 1977 dock = this.get('dock'), 1978 count = dock.count, 1979 docktitle, 1980 dockitem, 1981 closeicon, 1982 closeiconimg, 1983 id = this.get('id'); 1984 1985 this.fire('dockeditem:drawstart'); 1986 1987 docktitle = create('<div id="dock_item_' + id + '_title" role="menu" aria-haspopup="true" class="' 1988 + CSS.dockedtitle + '"></div>'); 1989 docktitle.append(this.get('title')); 1990 dockitem = create('<div id="dock_item_' + id + '" class="' + CSS.dockeditem + '" tabindex="0" rel="' + id + '"></div>'); 1991 if (count === 1) { 1992 dockitem.addClass('firstdockitem'); 1993 } 1994 dockitem.append(docktitle); 1995 dock.append(dockitem); 1996 1997 closeiconimg = create('<img alt="' + M.util.get_string('hidepanel', 'block') + 1998 '" title="' + M.util.get_string('hidedockpanel', 'block') + '" />'); 1999 closeiconimg.setAttribute('src', M.util.image_url('t/dockclose', 'moodle')); 2000 closeicon = create('<span class="hidepanelicon" tabindex="0"></span>').append(closeiconimg); 2001 closeicon.on('forceclose|click', this.hide, this); 2002 closeicon.on('dock:actionkey', this.hide, this, {actions: {enter: true, toggle: true}}); 2003 this.get('commands').append(closeicon); 2004 2005 this.set('dockTitleNode', docktitle); 2006 this.set('dockItemNode', dockitem); 2007 2008 this.fire('dockeditem:drawcomplete'); 2009 return true; 2010 }, 2011 /** 2012 * This function toggles makes the item active and shows it. 2013 * @method show 2014 * @return Boolean 2015 */ 2016 show: function() { 2017 var dock = this.get('dock'), 2018 panel = dock.getPanel(), 2019 docktitle = this.get('dockTitleNode'); 2020 2021 dock.hideActive(); 2022 this.fire('dockeditem:showstart'); 2023 Y.log('Showing ' + this._getLogDescription(), 'debug', LOGNS); 2024 panel.setHeader(this.get('titlestring'), this.get('commands')); 2025 panel.setBody(Y.Node.create('<div class="block_' + this.get('blockclass') + ' block_docked"></div>') 2026 .append(this.get('contents'))); 2027 if (M.core.actionmenu !== undefined) { 2028 M.core.actionmenu.newDOMNode(panel.get('node')); 2029 } 2030 panel.show(); 2031 panel.correctWidth(); 2032 2033 this.active = true; 2034 // Add active item class first up 2035 docktitle.addClass(CSS.activeitem); 2036 // Set aria-exapanded property to true. 2037 docktitle.set('aria-expanded', "true"); 2038 this.fire('dockeditem:showcomplete'); 2039 dock.resize(); 2040 return true; 2041 }, 2042 /** 2043 * This function hides the item and makes it inactive. 2044 * @method hide 2045 */ 2046 hide: function() { 2047 this.fire('dockeditem:hidestart'); 2048 Y.log('Hiding "' + this._getLogDescription(), 'debug', LOGNS); 2049 if (this.active) { 2050 // No longer active 2051 this.active = false; 2052 // Hide the panel 2053 this.get('dock').getPanel().hide(); 2054 } 2055 // Remove the active class 2056 // Set aria-exapanded property to false 2057 this.get('dockTitleNode').removeClass(CSS.activeitem).set('aria-expanded', "false"); 2058 this.fire('dockeditem:hidecomplete'); 2059 }, 2060 /** 2061 * A toggle between calling show and hide functions based on css.activeitem 2062 * Applies rules to key press events (dock:actionkey) 2063 * @method toggle 2064 * @param {String} action 2065 */ 2066 toggle: function(action) { 2067 var docktitle = this.get('dockTitleNode'); 2068 if (docktitle.hasClass(CSS.activeitem) && action !== 'expand') { 2069 this.hide(); 2070 } else if (!docktitle.hasClass(CSS.activeitem) && action !== 'collapse') { 2071 this.show(); 2072 } 2073 }, 2074 /** 2075 * This function removes the node and destroys it's bits. 2076 * @method remove. 2077 */ 2078 remove: function() { 2079 this.hide(); 2080 // Return the block to its original position. 2081 this.get('block').returnToPage(); 2082 // Remove the dock item node. 2083 this.get('dockItemNode').remove(); 2084 this.fire('dockeditem:itemremoved'); 2085 }, 2086 /** 2087 * Returns the description of this item to use for log calls. 2088 * @method _getLogDescription 2089 * @private 2090 * @return {String} 2091 */ 2092 _getLogDescription: function() { 2093 return this.get('titlestring').get('innerHTML') + ' (' + this.get('blockinstanceid') + ')'; 2094 } 2095 }; 2096 Y.extend(DOCKEDITEM, Y.Base, DOCKEDITEM.prototype, { 2097 NAME: 'moodle-core-dock-dockeditem', 2098 ATTRS: { 2099 /** 2100 * The block this docked item is associated with. 2101 * @attribute block 2102 * @type BLOCK 2103 * @writeOnce 2104 * @required 2105 */ 2106 block: { 2107 writeOnce: 'initOnly' 2108 }, 2109 /** 2110 * The dock itself. 2111 * @attribute dock 2112 * @type DOCK 2113 * @writeOnce 2114 * @required 2115 */ 2116 dock: { 2117 writeOnce: 'initOnly' 2118 }, 2119 /** 2120 * The docked item ID. This will be given by the dock. 2121 * @attribute id 2122 * @type Number 2123 */ 2124 id: {}, 2125 /** 2126 * Block instance id.Taken from the associated block. 2127 * @attribute blockinstanceid 2128 * @type Number 2129 * @writeOnce 2130 */ 2131 blockinstanceid: { 2132 writeOnce: 'initOnly', 2133 setter: function(value) { 2134 return parseInt(value, 10); 2135 } 2136 }, 2137 /** 2138 * The title nodeof the docked item. 2139 * @attribute title 2140 * @type Node 2141 * @default null 2142 */ 2143 title: { 2144 value: null 2145 }, 2146 /** 2147 * The title string. 2148 * @attribute titlestring 2149 * @type String 2150 */ 2151 titlestring: { 2152 value: null 2153 }, 2154 /** 2155 * The contents of the docked item 2156 * @attribute contents 2157 * @type Node 2158 * @writeOnce 2159 * @required 2160 */ 2161 contents: { 2162 writeOnce: 'initOnly' 2163 }, 2164 /** 2165 * Commands associated with the block. 2166 * @attribute commands 2167 * @type Node 2168 * @writeOnce 2169 * @required 2170 */ 2171 commands: { 2172 writeOnce: 'initOnly' 2173 }, 2174 /** 2175 * The block class. 2176 * @attribute blockclass 2177 * @type String 2178 * @writeOnce 2179 * @required 2180 */ 2181 blockclass: { 2182 writeOnce: 'initOnly' 2183 }, 2184 /** 2185 * The title node for the docked block. 2186 * @attribute dockTitleNode 2187 * @type Node 2188 */ 2189 dockTitleNode: { 2190 value: null 2191 }, 2192 /** 2193 * The item node for the docked block. 2194 * @attribute dockItemNode 2195 * @type Node 2196 */ 2197 dockItemNode: { 2198 value: null 2199 }, 2200 /** 2201 * The container for the docked item (will contain the block contents when visible) 2202 * @attribute dockcontainerNode 2203 * @type Node 2204 */ 2205 dockcontainerNode: { 2206 value: null 2207 } 2208 } 2209 }); 2210 Y.augment(DOCKEDITEM, Y.EventTarget); 2211 2212 2213 }, '@VERSION@', { 2214 "requires": [ 2215 "base", 2216 "node", 2217 "event-custom", 2218 "event-mouseenter", 2219 "event-resize", 2220 "escape", 2221 "moodle-core-dock-loader", 2222 "moodle-core-event" 2223 ] 2224 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |