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