1 /*! 2 * This file is part of Aloha Editor Project http://aloha-editor.org 3 * Copyright (c) 2010-2011 Gentics Software GmbH, aloha@gentics.com 4 * Contributors http://aloha-editor.org/contribution.php 5 * Licensed unter the terms of http://www.aloha-editor.org/license.html 6 *//* 7 * Aloha Editor is free software: you can redistribute it and/or modify 8 * it under the terms of the GNU Affero General Public License as published by 9 * the Free Software Foundation, either version 3 of the License, or 10 * (at your option) any later version.* 11 * 12 * Aloha Editor is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with this program. If not, see <http://www.gnu.org/licenses/>. 19 */ 20 define( 21 ['aloha/core', 'aloha/jquery', 'aloha/ext', 'util/class', 'aloha/console', 'vendor/jquery.store'], 22 function(Aloha, jQuery, Ext, Class, console) { 23 "use strict"; 24 var GENTICS = window.GENTICS; 25 26 /** 27 * Constructor for a floatingmenu tab 28 * @namespace Aloha.FloatingMenu 29 * @class Tab 30 * @constructor 31 * @param {String} label label of the tab 32 */ 33 var Tab = Class.extend({ 34 _constructor: function(label) { 35 this.label = label; 36 this.groups = []; 37 this.groupMap = {}; 38 this.visible = true; 39 }, 40 41 /** 42 * Get the group with given index. If it does not yet exist, create a new one 43 * @method 44 * @param {int} group group index of the group to get 45 * @return group object 46 */ 47 getGroup: function(group) { 48 var groupObject = this.groupMap[group]; 49 if (typeof groupObject === 'undefined') { 50 groupObject = new Group(); 51 this.groupMap[group] = groupObject; 52 this.groups.push(groupObject); 53 // TODO resort the groups 54 } 55 56 return groupObject; 57 }, 58 59 /** 60 * Get the EXT component representing the tab 61 * @return EXT component (EXT.Panel) 62 * @hide 63 */ 64 getExtComponent: function () { 65 var that = this; 66 67 if (typeof this.extPanel === 'undefined') { 68 // generate the panel here 69 this.extPanel = new Ext.Panel({ 70 'tbar' : [], 71 'title' : this.label, 72 'style': 'margin-top:0px', 73 'bodyStyle': 'display:none', 74 'autoScroll': true 75 }); 76 77 // add the groups 78 jQuery.each(this.groups, function(index, group) { 79 // let each group generate its ext component and add them to the panel 80 that.extPanel.getTopToolbar().add(group.getExtComponent()); 81 }); 82 } 83 84 return this.extPanel; 85 }, 86 87 /** 88 * Recalculate the visibility of all groups within the tab 89 * @hide 90 */ 91 doLayout: function() { 92 var that = this; 93 94 if (Aloha.Log.isDebugEnabled()) { 95 Aloha.Log.debug(this, 'doLayout called for tab ' + this.label); 96 } 97 this.visible = false; 98 99 // check all groups in this tab 100 jQuery.each(this.groups, function(index, group) { 101 that.visible |= group.doLayout(); 102 }); 103 104 if (Aloha.Log.isDebugEnabled()) { 105 Aloha.Log.debug(this, 'tab ' + this.label + (this.visible ? ' is ' : ' is not ') + 'visible now'); 106 } 107 108 return this.visible; 109 } 110 }); 111 112 /** 113 * Constructor for a floatingmenu group 114 * @namespace Aloha.FloatingMenu 115 * @class Group 116 * @constructor 117 */ 118 var Group = Class.extend({ 119 _constructor: function() { 120 this.buttons = []; 121 this.fields = []; 122 }, 123 124 /** 125 * Add a button to this group 126 * @param {Button} buttonInfo to add to the group 127 */ 128 addButton: function(buttonInfo) { 129 if (buttonInfo.button instanceof Aloha.ui.AttributeField) { 130 if (this.fields.length < 2) { 131 this.fields.push(buttonInfo); 132 } else { 133 throw new Error("Too much fields in this group"); 134 } 135 } else { 136 // Every plugin API entryPoint (method) should be securised enough 137 // to avoid Aloha to block at startup even 138 // if a plugin is badly designed 139 if (typeof buttonInfo.button !== "undefined"){ 140 this.buttons.push(buttonInfo); 141 } 142 } 143 }, 144 145 /** 146 * Get the EXT component representing the group (Ext.ButtonGroup) 147 * @return the Ext.ButtonGroup 148 * @hide 149 */ 150 getExtComponent: function () { 151 var that = this, l, 152 items = [], 153 buttonCount = 0, 154 columnCount = 0, 155 len, idx, half; 156 157 158 if (typeof this.extButtonGroup === 'undefined') { 159 160 if (this.fields.length > 1) { 161 columnCount = 1; 162 } 163 164 jQuery.each(this.buttons, function(index, button) { 165 // count the number of buttons (large buttons count as 2) 166 buttonCount += button.button.size == 'small' ? 1 : 2; 167 }); 168 columnCount = columnCount + Math.ceil(buttonCount / 2); 169 170 len = this.buttons.length; 171 idx = 0; 172 half = Math.ceil(this.buttons.length / 2) - this.buttons.length % 2 ; 173 174 if (this.fields.length > 0) { 175 that.buttons.push(this.fields[0]); 176 items.push(this.fields[0].button.getExtConfigProperties()); 177 } 178 179 while (--len >= half) { 180 items.push(this.buttons[idx++].button.getExtConfigProperties()); 181 } 182 ++len; 183 if (this.fields.length > 1) { 184 that.buttons.push(this.fields[1]); 185 items.push(this.fields[1].button.getExtConfigProperties()); 186 } 187 while (--len >=0) { 188 items.push(this.buttons[idx++].button.getExtConfigProperties()); 189 } 190 191 this.extButtonGroup = new Ext.ButtonGroup({ 192 'columns' : columnCount, 193 'items': items 194 }); 195 // jQuery.each(this.fields, function(id, field){ 196 // that.buttons.push(field); 197 // }); 198 // now find the Ext.Buttons and set to the GENTICS buttons 199 jQuery.each(this.buttons, function(index, buttonInfo) { 200 buttonInfo.button.extButton = that.extButtonGroup.findById(buttonInfo.button.id); 201 // the following code is a work arround because ExtJS initializes later. 202 // The ui wrapper store the information and here we use it... ugly. 203 // if there are any listeners added before initializing the extButtons 204 if ( buttonInfo.button.listenerQueue && buttonInfo.button.listenerQueue.length > 0 ) { 205 while ( true ) { 206 l = buttonInfo.button.listenerQueue.shift(); 207 if ( !l ) {break;} 208 buttonInfo.button.extButton.addListener(l.eventName, l.handler, l.scope, l.options); 209 } 210 } 211 if (buttonInfo.button.extButton.setObjectTypeFilter) { 212 if (buttonInfo.button.objectTypeFilter) { 213 buttonInfo.button.extButton.noQuery = false; 214 } 215 if ( buttonInfo.button.objectTypeFilter == 'all' ) { 216 buttonInfo.button.objectTypeFilter = null; 217 } 218 buttonInfo.button.extButton.setObjectTypeFilter(buttonInfo.button.objectTypeFilter); 219 if ( buttonInfo.button.displayField) { 220 buttonInfo.button.extButton.displayField = buttonInfo.button.displayField; 221 } 222 if ( buttonInfo.button.tpl ) { 223 buttonInfo.button.extButton.tpl = buttonInfo.button.tpl; 224 } 225 } 226 }); 227 } 228 229 return this.extButtonGroup; 230 }, 231 232 /** 233 * Recalculate the visibility of the buttons and the group 234 * @hide 235 */ 236 doLayout: function () { 237 var groupVisible = false, 238 that = this; 239 jQuery.each(this.buttons, function(index, button) { 240 if (typeof button.button !== "undefined") { 241 var extButton = that.extButtonGroup.findById(button.button.id), 242 buttonVisible = button.button.isVisible() && button.scopeVisible; 243 244 if (!extButton) { 245 return; 246 } 247 248 if (buttonVisible && extButton.hidden) { 249 extButton.show(); 250 } else if (!buttonVisible && extButton && !extButton.hidden) { 251 extButton.hide(); 252 } 253 254 groupVisible |= buttonVisible; 255 } 256 }); 257 if (groupVisible && this.extButtonGroup.hidden) { 258 this.extButtonGroup.show(); 259 } else if (!groupVisible && !this.extButtonGroup.hidden) { 260 this.extButtonGroup.hide(); 261 } 262 263 return groupVisible; 264 265 } 266 }); 267 268 //========================================================================= 269 // 270 // Floating Menu 271 // 272 //========================================================================= 273 274 var lastFloatingMenuPos = { 275 top: null, 276 left: null 277 }; 278 279 /** 280 * Handler for window scroll event. Positions the floating menu 281 * appropriately. 282 * 283 * @param {Aloha.FloatingMenu} floatingmenu 284 */ 285 function onWindowScroll( floatingmenu ) { 286 if ( !Aloha.activeEditable ) { 287 return; 288 } 289 290 var element = floatingmenu.obj; 291 var editablePos = Aloha.activeEditable.obj.offset(); 292 var isTopAligned = floatingmenu.behaviour === 'topalign'; 293 var isAppended = floatingmenu.behaviour === 'append'; 294 var isManuallyPinned = floatingmenu.pinned 295 && ( parseInt( element.css( 'left' ), 10 ) 296 != ( editablePos.left 297 + floatingmenu.horizontalOffset 298 ) ); 299 300 // no calculation when pinned manually or has behaviour 'append' 301 if ( isTopAligned && isManuallyPinned || isAppended ) { 302 return; 303 } 304 305 var floatingmenuHeight = element.height(); 306 var scrollTop = jQuery( document ).scrollTop(); 307 308 // This value is what the top position of the floating menu *would* be 309 // if we tried to position it above the active editable. 310 var floatingmenuTop = editablePos.top - floatingmenuHeight 311 + floatingmenu.marginTop 312 - floatingmenu.topalignOffset; 313 314 // The floating menu does not fit in the space between the top of the 315 // viewport and the editable, so position it at the top of the viewport 316 // and over the editable. 317 if ( scrollTop > floatingmenuTop ) { 318 editablePos.top = isTopAligned 319 ? scrollTop + floatingmenu.marginTop 320 : floatingmenu.marginTop; 321 322 // There is enough space on top of the editable to fit the entire 323 // floating menu, so we do so. 324 } else if ( scrollTop <= floatingmenuTop ) { 325 editablePos.top -= floatingmenuHeight 326 + ( isTopAligned 327 ? floatingmenu.marginTop + 328 floatingmenu.topalignOffset 329 : 0 ); 330 } 331 332 floatingmenu.floatTo( editablePos ); 333 } 334 335 /** 336 * Aloha's Floating Menu 337 * @namespace Aloha 338 * @class FloatingMenu 339 * @singleton 340 */ 341 var FloatingMenu = Class.extend({ 342 /** 343 * Define the default scopes 344 * @property 345 * @type Object 346 */ 347 scopes: { 348 'Aloha.empty' : { 349 'name' : 'Aloha.empty', 350 'extendedScopes' : [], 351 'buttons' : [] 352 }, 353 'Aloha.global' : { 354 'name' : 'Aloha.global', 355 'extendedScopes' : ['Aloha.empty'], 356 'buttons' : [] 357 }, 358 'Aloha.continuoustext' : { 359 'name' : 'Aloha.continuoustext', 360 'extendedScopes' : ['Aloha.global'], 361 'buttons' : [] 362 } 363 }, 364 365 /** 366 * Array of tabs within the floatingmenu 367 * @hide 368 */ 369 tabs: [], 370 371 /** 372 * 'Map' of tabs (for easy access) 373 * @hide 374 */ 375 tabMap: {}, 376 377 /** 378 * Flag to mark whether the floatingmenu is initialized 379 * @hide 380 */ 381 initialized: false, 382 383 /** 384 * Array containing all buttons 385 * @hide 386 */ 387 allButtons: [], 388 389 /** 390 * top part of the floatingmenu position 391 * @hide 392 */ 393 top: 100, 394 395 /** 396 * left part of the floatingmenu position 397 * @hide 398 */ 399 left: 100, 400 401 /** 402 * store pinned status - true, if the FloatingMenu is pinned 403 * @property 404 * @type boolean 405 */ 406 pinned: false, 407 408 /** 409 * just a reference to the jQuery(window) object, which is used quite often 410 */ 411 window: jQuery(window), 412 413 /** 414 * Aloha.settings.floatingmenu.behaviour 415 * 416 * Is used to define the floating menu (fm) float behaviour. 417 * 418 * available: 419 * 'float' (default) the fm will float next to the position where the caret is, 420 * 'topalign' the fm is fixed above the contentEditable which is active, 421 422 * 'append' the fm is appended to the defined 'element' element position (top/left) 423 */ 424 behaviour: 'float', 425 426 /** 427 * Aloha.settings.floatingmenu.element 428 * 429 * Is used to define the element where the floating menu is positioned when 430 * Aloha.settings.floatingmenu.behaviour is set to 'append' 431 * 432 */ 433 element: 'floatingmenu', 434 435 /** 436 * topalign offset to be used for topalign behavior 437 */ 438 topalignOffset: 0, 439 440 /** 441 * topalign offset to be used for topalign behavior 442 */ 443 horizontalOffset: 0, 444 445 /** 446 * will only be hounoured when behaviour is set to 'topalign'. Adds a margin, 447 * so the floating menu is not directly attached to the top of the page 448 */ 449 marginTop: 10, 450 451 /** 452 * Define whether the floating menu shall be draggable or not via Aloha.settings.floatingmanu.draggable 453 * Default is: true 454 */ 455 draggable: true, 456 457 /** 458 * Define whether the floating menu shall be pinned or not via Aloha.settings.floatingmanu.pin 459 * Default is: false 460 */ 461 pin: false, 462 463 /** 464 * A list of all buttons that have been added to the floatingmenu 465 * This needs to be tracked, as adding buttons twice will break the fm 466 */ 467 buttonsAdded: [], 468 469 /** 470 * Will be initialized by checking Aloha.settings.toolbar, which will contain the config for 471 * the floating menu. If there is no config, tabs and groups will be generated programmatically 472 */ 473 fromConfig: false, 474 475 /** 476 * Initialize the floatingmenu 477 * @hide 478 */ 479 init: function() { 480 481 // check for behaviour setting of the floating menu 482 if ( Aloha.settings.floatingmenu ) { 483 if ( typeof Aloha.settings.floatingmenu.draggable === 484 'boolean' ) { 485 this.draggable = Aloha.settings.floatingmenu.draggable; 486 } 487 488 if ( typeof Aloha.settings.floatingmenu.behaviour === 489 'string' ) { 490 this.behaviour = Aloha.settings.floatingmenu.behaviour; 491 } 492 493 if ( typeof Aloha.settings.floatingmenu.topalignOffset !== 494 'undefined' ) { 495 this.topalignOffset = parseInt( 496 Aloha.settings.floatingmenu.topalignOffset, 10 ); 497 } 498 499 if ( typeof Aloha.settings.floatingmenu.horizontalOffset !== 500 'undefined' ) { 501 this.horizontalOffset = parseInt( 502 Aloha.settings.floatingmenu.horizontalOffset , 10 ); 503 } 504 505 if ( typeof Aloha.settings.floatingmenu.marginTop === 506 'number' ) { 507 this.marginTop = parseInt( 508 Aloha.settings.floatingmenu.marginTop , 10 ); 509 } 510 511 if ( typeof Aloha.settings.floatingmenu.element === 512 'string' ) { 513 this.element = Aloha.settings.floatingmenu.element; 514 } 515 if ( typeof Aloha.settings.floatingmenu.pin === 516 'boolean' ) { 517 this.pin = Aloha.settings.floatingmenu.pin; 518 } 519 520 521 if ( typeof Aloha.settings.floatingmenu.width !== 522 'undefined' ) { 523 this.width = parseInt( Aloha.settings.floatingmenu.width, 524 10 ); 525 } 526 } 527 528 jQuery.storage = new jQuery.store(); 529 530 this.currentScope = 'Aloha.global'; 531 532 var that = this; 533 534 this.window.unload(function () { 535 // store fm position if the panel is pinned to be able to restore it next time 536 if (that.pinned) { 537 jQuery.storage.set('Aloha.FloatingMenu.pinned', 'true'); 538 jQuery.storage.set('Aloha.FloatingMenu.top', that.top); 539 jQuery.storage.set('Aloha.FloatingMenu.left', that.left); 540 if (Aloha.Log.isInfoEnabled()) { 541 Aloha.Log.info(this, 'stored FloatingMenu pinned position {' + that.left 542 + ', ' + that.top + '}'); 543 } 544 } else { 545 // delete old localStorages 546 jQuery.storage.del('Aloha.FloatingMenu.pinned'); 547 jQuery.storage.del('Aloha.FloatingMenu.top'); 548 jQuery.storage.del('Aloha.FloatingMenu.left'); 549 } 550 551 if (that.userActivatedTab) { 552 jQuery.storage.set('Aloha.FloatingMenu.activeTab', that.userActivatedTab); 553 } 554 }).resize(function () { 555 if (that.behaviour === 'float') { 556 if (that.pinned) { 557 that.fixPinnedPosition(); 558 that.refreshShadow(); 559 that.extTabPanel.setPosition(that.left, that.top); 560 } else { 561 var target = that.calcFloatTarget(Aloha.Selection.getRangeObject()); 562 if (target) { 563 that.floatTo(target); 564 } 565 } 566 } 567 }); 568 Aloha.bind('aloha-ready', function() { 569 that.generateComponent(); 570 that.initialized = true; 571 }); 572 573 if (typeof Aloha.settings.toolbar === 'object') { 574 this.fromConfig = true; 575 } 576 }, 577 578 /** 579 * jQuery reference to the extjs tabpanel 580 * @hide 581 */ 582 obj: null, 583 584 /** 585 * jQuery reference to the shadow obj 586 * @hide 587 */ 588 shadow: null, 589 590 /** 591 * jQuery reference to the panels body wrap div 592 * @hide 593 */ 594 panelBody: null, 595 596 /** 597 * The panels width 598 * @hide 599 */ 600 width: 400, 601 602 /** 603 * initialize tabs and groups according to the current configuration 604 */ 605 initTabsAndGroups: function () { 606 var that = this; 607 608 // if there is no toolbar config tabs and groups have been initialized before 609 if (!this.fromConfig) { 610 return; 611 } 612 613 jQuery.each(Aloha.settings.toolbar.tabs, function (tab, groups) { 614 // generate or retrieve tab 615 var tabObject = that.tabMap[tab]; 616 if (typeof tabObject === 'undefined') { 617 // the tab object does not yet exist, so create a new tab and add it to the list 618 tabObject = new Tab(tab); 619 that.tabs.push(tabObject); 620 that.tabMap[tab] = tabObject; 621 } 622 623 // generate groups for current tab 624 jQuery.each(groups, function (group, buttons) { 625 var groupObject = tabObject.getGroup(group), 626 i; 627 628 // now get all the buttons for that group 629 jQuery.each(buttons, function (j, button) { 630 if (jQuery.inArray(button, that.buttonsAdded) !== -1) { 631 // buttons must not be added twice 632 console.warn('Skipping button {' + button + '}. A button can\'t be added ' + 633 'to the floating menu twice. Config key: {Aloha.settings.toolbar.' + 634 tab + '.' + group + '}'); 635 return; 636 } 637 638 // now add the button to the group 639 for (i = 0; i < that.allButtons.length; i++) { 640 if (button === that.allButtons[i].button.name) { 641 groupObject.addButton(that.allButtons[i]); 642 // remember that we've added the button 643 that.buttonsAdded.push(that.allButtons[i].button.name); 644 break; 645 } 646 } 647 }); 648 }); 649 }); 650 }, 651 652 /** 653 * Generate the rendered component for the floatingmenu 654 * @hide 655 */ 656 generateComponent: function () { 657 var that = this, pinTab; 658 659 // initialize tabs and groups first 660 this.initTabsAndGroups(); 661 662 // Initialize and configure the tooltips 663 Ext.QuickTips.init(); 664 Ext.apply(Ext.QuickTips.getQuickTip(), { 665 minWidth : 10 666 }); 667 668 669 670 if (this.extTabPanel) { 671 // TODO dispose of the ext component 672 } else { 673 674 // Enable or disable the drag functionality 675 var dragConfiguration = false; 676 677 if ( that.draggable ) { 678 dragConfiguration = { 679 insertProxy: false, 680 onDrag : function(e) { 681 var pel = this.proxy.getEl(); 682 this.x = pel.getLeft(true); 683 this.y = pel.getTop(true); 684 this.panel.shadow.hide(); 685 }, 686 endDrag : function(e) { 687 var top = (that.pinned) ? this.y - jQuery(document).scrollTop() : this.y; 688 689 that.left = this.x; 690 that.top = top; 691 692 // store the last floating menu position when the floating menu was dragged around 693 lastFloatingMenuPos.left = that.left; 694 lastFloatingMenuPos.top = that.top; 695 696 this.panel.setPosition(this.x, top); 697 that.refreshShadow(); 698 this.panel.shadow.show(); 699 } 700 }; 701 } 702 // generate the tabpanel object 703 this.extTabPanel = new Ext.TabPanel({ 704 activeTab: 0, 705 width: that.width, // 336px this fits the multisplit button and 6 small buttons placed in 3 cols 706 plain: false, 707 draggable: dragConfiguration, 708 floating: {shadow: false}, 709 defaults: { 710 autoScroll: true 711 }, 712 layoutOnTabChange : true, 713 shadow: false, 714 cls: 'aloha-floatingmenu ext-root', 715 listeners : { 716 'tabchange' : { 717 'fn' : function(tabPanel, tab) { 718 if (tab.title != that.autoActivatedTab) { 719 if (Aloha.Log.isDebugEnabled()) { 720 Aloha.Log.debug(that, 'User selected tab ' + tab.title); 721 } 722 // remember the last user-selected tab 723 that.userActivatedTab = tab.title; 724 } else { 725 if (Aloha.Log.isDebugEnabled()) { 726 Aloha.Log.debug(that, 'Tab ' + tab.title + ' was activated automatically'); 727 } 728 } 729 that.autoActivatedTab = undefined; 730 731 // ok, this is kind of a hack: when the tab changes, we check all buttons for multisplitbuttons (which have the method setActiveDOMElement). 732 // if a DOM Element is queued to be set active, we try to do this now. 733 // the reason for this is that the active DOM element can only be set when the multisplit button is currently visible. 734 jQuery.each(that.allButtons, function(index, buttonInfo) { 735 if (typeof buttonInfo.button !== 'undefined' 736 && typeof buttonInfo.button.extButton !== 'undefined' 737 && buttonInfo.button.extButton !== null 738 && typeof buttonInfo.button.extButton.setActiveDOMElement === 'function') { 739 if (typeof buttonInfo.button.extButton.activeDOMElement !== 'undefined') { 740 buttonInfo.button.extButton.setActiveDOMElement(buttonInfo.button.extButton.activeDOMElement); 741 } 742 } 743 }); 744 745 // adapt the shadow 746 that.extTabPanel.shadow.show(); 747 that.refreshShadow(); 748 } 749 } 750 }, 751 enableTabScroll : true 752 }); 753 754 755 } 756 // add the tabs 757 jQuery.each(this.tabs, function(index, tab) { 758 // let each tab generate its ext component and add them to the panel 759 try { 760 that.extTabPanel.add(tab.getExtComponent()); 761 } catch(e) { 762 Aloha.Log.error(that,"Error while inserting tab: " + e); 763 } 764 }); 765 766 // add the dropshadow 767 this.extTabPanel.shadow = jQuery('<div id="aloha-floatingmenu-shadow" class="aloha-shadow"> </div>'); 768 jQuery('body').append(this.extTabPanel.shadow); 769 770 // add an empty pin tab item, store reference 771 pinTab = this.extTabPanel.add({ 772 title : ' ' 773 }); 774 775 // finally render the panel to the body 776 this.extTabPanel.render(document.body); 777 778 // finish the pin element after the FM has rendered (before there are noe html contents to be manipulated 779 jQuery(pinTab.tabEl) 780 .addClass('aloha-floatingmenu-pin') 781 .html(' ') 782 .mousedown(function (e) { 783 that.togglePin(); 784 // Note: this event is deliberately stopped here, although normally, 785 // we would set the flag GENTICS.Aloha.eventHandled instead. 786 // But when the event bubbles up, no tab would be selected and 787 // the floatingmenu would be rather thin. 788 e.stopPropagation(); 789 }); 790 791 // a reference to the panels body needed for shadow size & position 792 this.panelBody = jQuery('div.aloha-floatingmenu div.x-tab-panel-bwrap'); 793 794 // do the visibility 795 this.doLayout(); 796 797 // bind jQuery reference to extjs obj 798 799 // this has to be done AFTER the tab panel has been rendered 800 this.obj = jQuery(this.extTabPanel.getEl().dom); 801 802 if (jQuery.storage.get('Aloha.FloatingMenu.pinned') == 'true') { 803 //this.togglePin(); 804 805 this.top = parseInt(jQuery.storage.get('Aloha.FloatingMenu.top'),10); 806 this.left = parseInt(jQuery.storage.get('Aloha.FloatingMenu.left'),10); 807 808 // do some positioning fixes 809 this.fixPinnedPosition(); 810 811 if (Aloha.Log.isInfoEnabled()) { 812 Aloha.Log.info(this, 'restored FloatingMenu pinned position {' + this.left + ', ' + this.top + '}'); 813 } 814 815 this.refreshShadow(); 816 } 817 818 // set the user activated tab stored in a localStorage 819 if (jQuery.storage.get('Aloha.FloatingMenu.activeTab')) { 820 this.userActivatedTab = jQuery.storage.get('Aloha.FloatingMenu.activeTab'); 821 } 822 823 // for now, position the panel somewhere 824 this.extTabPanel.setPosition(this.left, this.top); 825 826 // mark the event being handled by aloha, because we don't want to recognize 827 // a click into the floatingmenu to be a click into nowhere (which would 828 // deactivate the editables) 829 this.obj.mousedown(function (e) { 830 e.originalEvent.stopSelectionUpdate = true; 831 Aloha.eventHandled = true; 832 //e.stopSelectionUpdate = true; 833 }); 834 835 this.obj.mouseup(function (e) { 836 e.originalEvent.stopSelectionUpdate = true; 837 Aloha.eventHandled = false; 838 }); 839 840 jQuery( window ).scroll(function() { 841 onWindowScroll( that ); 842 }); 843 844 // don't display the drag handle bar / pin when floating menu is not draggable 845 if ( !that.draggable ) { 846 jQuery('.aloha-floatingmenu').hover( function() { 847 jQuery(this).css({background: 'none'}); 848 jQuery('.aloha-floatingmenu-pin').hide(); 849 }); 850 } 851 852 // adjust float behaviour 853 if (this.behaviour === 'float') { 854 // listen to selectionChanged event 855 Aloha.bind('aloha-selection-changed',function(event, rangeObject) { 856 if (!that.pinned) { 857 var pos = that.calcFloatTarget(rangeObject); 858 if (pos) { 859 that.floatTo(pos); 860 } 861 } 862 }); 863 } else if (this.behaviour === 'append' ) { 864 var p = jQuery( "#" + that.element ); 865 var position = p.offset(); 866 867 if ( !position ) { 868 Aloha.Log.warn(that, 'Invalid element HTML ID for floating menu: ' + that.element); 869 return false; 870 } 871 872 // set the position so that it does not float on the first editable activation 873 this.floatTo( position ); 874 875 if ( this.pin ) { 876 this.togglePin( true ); 877 } 878 879 Aloha.bind( 'aloha-editable-activated', function( event, data ) { 880 if ( that.pinned ) { 881 return; 882 } 883 that.floatTo( position ); 884 }); 885 886 } else if ( this.behaviour === 'topalign' ) { 887 // topalign will retain the user's pinned status 888 // TODO maybe the pin should be hidden in that case? 889 this.togglePin( false ); 890 891 // Float the menu to the editable that is activated. 892 Aloha.bind( 'aloha-editable-activated', function( event, data ) { 893 if ( that.pinned ) { 894 return; 895 } 896 897 // FIXME: that.obj.height() does not return the correct 898 // height of the editable. We need to fix this, and 899 // not hard-code the height as we currently do. 900 var editable = data.editable.obj; 901 var floatingmenuHeight = 90; 902 var editablePos = editable.offset(); 903 var isFloatingmenuAboveViewport = ( ( 904 editablePos.top - floatingmenuHeight ) 905 < jQuery( document ).scrollTop() ); 906 907 if ( isFloatingmenuAboveViewport ) { 908 // Since we don't have space to place the floatingmenu 909 // above the editable, we want to place it over the 910 // editable instead. But if the editable is shorter 911 // than the floatingmenu, it would be completely 912 // covered by it, and so, in such cases, we position 913 // the editable at the bottom of the short editable. 914 editablePos.top = ( editable.height() 915 < floatingmenuHeight ) 916 ? editablePos.top + editable.height() 917 : jQuery( document ).scrollTop(); 918 919 editablePos.top += that.marginTop; 920 } else { 921 editablePos.top -= floatingmenuHeight 922 + that.topalignOffset; 923 } 924 925 editablePos.left += that.horizontalOffset; 926 927 var HORIZONTAL_PADDING = 10; 928 // Calculate by how much the floating menu is pocking 929 // outside the width of the viewport. A positive number 930 // means that it is outside the viewport, negative means 931 // it is within the viewport. 932 var overhang = ( ( editablePos.left + that.width 933 + HORIZONTAL_PADDING ) - jQuery(window).width() ); 934 935 if ( overhang > 0 ) { 936 editablePos.left -= overhang; 937 } 938 939 that.floatTo( editablePos ); 940 }); 941 } 942 }, 943 944 /** 945 * Fix the position of the pinned floatingmenu to keep it visible 946 */ 947 fixPinnedPosition: function() { 948 // if not pinned, do not fix the position 949 if (!this.pinned) { 950 return; 951 } 952 953 // fix the position of the floatingmenu, to keep it visible 954 if (this.top < 30) { 955 // from top position, we want to have 30px margin 956 this.top = 30; 957 } else if (this.top > this.window.height() - this.extTabPanel.getHeight()) { 958 this.top = this.window.height() - this.extTabPanel.getHeight(); 959 } 960 if (this.left < 0) { 961 this.left = 0; 962 } else if (this.left > this.window.width() - this.extTabPanel.getWidth()) { 963 this.left = this.window.width() - this.extTabPanel.getWidth(); 964 } 965 }, 966 967 /** 968 * reposition & resize the shadow 969 * the shadow must not be repositioned outside this method! 970 * position calculation is based on this.top and this.left coordinates 971 * @method 972 */ 973 refreshShadow: function (resize) { 974 if (this.panelBody) { 975 var props = { 976 'top': this.top + 24, // 24px top offset to reflect tab bar height 977 'left': this.left 978 }; 979 980 if(typeof resize === 'undefined' || !resize) { 981 props.width = this.panelBody.width() + 'px'; 982 props.height = this.panelBody.height() + 'px'; 983 } 984 985 this.extTabPanel.shadow.css(props); 986 } 987 }, 988 989 /** 990 * toggles the pinned status of the floating menu 991 * @method 992 * @param {boolean} pinned set to true to activate pin, or set to false to deactivate pin. 993 * leave undefined to toggle pin status automatically 994 */ 995 togglePin: function(pinned) { 996 var el = jQuery('.aloha-floatingmenu-pin'); 997 998 if (typeof pinned === 'boolean') { 999 this.pinned = !pinned; 1000 } 1001 1002 if (this.pinned) { 1003 el.removeClass('aloha-floatingmenu-pinned'); 1004 this.top = this.obj.offset().top; 1005 1006 this.obj.removeClass('fixed').css({ 1007 'top': this.top 1008 }); 1009 1010 this.extTabPanel.shadow.removeClass('fixed'); 1011 this.refreshShadow(); 1012 1013 this.pinned = false; 1014 } else { 1015 el.addClass('aloha-floatingmenu-pinned'); 1016 this.top = this.obj.offset().top - this.window.scrollTop(); 1017 1018 this.obj.addClass('fixed').css({ 1019 'top': this.top // update position for fixed position 1020 }); 1021 1022 // do the same for the shadow 1023 this.extTabPanel.shadow.addClass('fixed');//props.start 1024 this.refreshShadow(); 1025 1026 this.pinned = true; 1027 } 1028 }, 1029 1030 /** 1031 * Create a new scopes 1032 * @method 1033 * @param {String} scope name of the new scope (should be namespaced for uniqueness) 1034 * @param {String} extendedScopes Array of scopes this scope extends. Can also be a single String if 1035 * only one scope is extended, or omitted if the scope should extend 1036 * the empty scope 1037 */ 1038 createScope: function(scope, extendedScopes) { 1039 if (typeof extendedScopes === 'undefined') { 1040 extendedScopes = ['Aloha.empty']; 1041 } else if (typeof extendedScopes === 'string') { 1042 extendedScopes = [extendedScopes]; 1043 } 1044 1045 // TODO check whether the extended scopes already exist 1046 1047 if (this.scopes[scope]) { 1048 // TODO what if the scope already exists? 1049 } else { 1050 // generate the new scope 1051 this.scopes[scope] = {'name' : scope, 'extendedScopes' : extendedScopes, 'buttons' : []}; 1052 } 1053 }, 1054 1055 /** 1056 * Adds a button to the floatingmenu 1057 * @method 1058 * @param {String} scope the scope for the button, should be generated before (either by core or the plugin) 1059 * @param {Button} button instance of Aloha.ui.button to add at the floatingmenu 1060 * @param {String} tab label of the tab to which the button is added 1061 * @param {int} group index of the button group in the tab, lowest index is left 1062 */ 1063 addButton: function(scope, button, tab, group) { 1064 // check whether the scope exists 1065 var 1066 scopeObject = this.scopes[scope], 1067 buttonInfo, tabObject, groupObject; 1068 1069 if (!button.name) { 1070 console.warn('Added button with iconClass {' + button.iconClass + '} which has no property "name"'); 1071 } 1072 1073 if (typeof scopeObject === 'undefined') { 1074 Aloha.Log.error("Can't add button to given scope since the scope has not yet been initialized.", scope); 1075 return false; 1076 } 1077 1078 // generate a buttonInfo object 1079 buttonInfo = { 'button' : button, 'scopeVisible' : false }; 1080 1081 // add the button to the list of all buttons 1082 this.allButtons.push(buttonInfo); 1083 1084 // add the button to the scope 1085 scopeObject.buttons.push(buttonInfo); 1086 1087 // if there is no toolbar config tabs and groups will be generated right away 1088 if (!this.fromConfig) { 1089 // get the tab object 1090 tabObject = this.tabMap[tab]; 1091 if (typeof tabObject === 'undefined') { 1092 // the tab object does not yet exist, so create a new tab and add it to the list 1093 tabObject = new Tab(tab); 1094 this.tabs.push(tabObject); 1095 this.tabMap[tab] = tabObject; 1096 } 1097 1098 // get the group 1099 groupObject = tabObject.getGroup(group); 1100 1101 // now add the button to the group 1102 groupObject.addButton(buttonInfo); 1103 } 1104 1105 // finally, when the floatingmenu is already initialized, we need to create the ext component now 1106 if (this.initialized) { 1107 this.generateComponent(); 1108 } 1109 }, 1110 1111 /** 1112 * Recalculate the visibility of tabs, groups and buttons (depending on scope and button hiding) 1113 * @hide 1114 */ 1115 doLayout: function () { 1116 if (Aloha.Log.isDebugEnabled()) { 1117 Aloha.Log.debug(this, 'doLayout called for FloatingMenu, scope is ' + this.currentScope); 1118 } 1119 1120 var that = this, 1121 firstVisibleTab = false, 1122 activeExtTab = this.extTabPanel.getActiveTab(), 1123 activeTab = false, 1124 floatingMenuVisible = false, 1125 showUserActivatedTab = false, 1126 pos; 1127 1128 // let the tabs layout themselves 1129 jQuery.each(this.tabs, function(index, tab) { 1130 // remember the active tab 1131 if (tab.extPanel == activeExtTab) { 1132 activeTab = tab; 1133 } 1134 1135 // remember whether the tab is currently visible 1136 var tabVisible = tab.visible; 1137 1138 // let each tab generate its ext component and add them to the panel 1139 if (tab.doLayout()) { 1140 // found a visible tab, so the floatingmenu needs to be visible as well 1141 floatingMenuVisible = true; 1142 1143 // make sure the tabstrip is visible 1144 if (!tabVisible) { 1145 if (Aloha.Log.isDebugEnabled()) { 1146 Aloha.Log.debug(that, 'showing tab strip for tab ' + tab.label); 1147 } 1148 that.extTabPanel.unhideTabStripItem(tab.extPanel); 1149 } 1150 1151 // remember the first visible tab 1152 if (!firstVisibleTab) { 1153 // this is the first visible tab (in case we need to switch to it) 1154 firstVisibleTab = tab; 1155 } 1156 1157 // check whether this visible tab is the last user activated tab and currently not active 1158 if (that.userActivatedTab == tab.extPanel.title && tab.extPanel != activeExtTab) { 1159 showUserActivatedTab = tab; 1160 } 1161 } else { 1162 // make sure the tabstrip is hidden 1163 if (tabVisible) { 1164 if (Aloha.Log.isDebugEnabled()) { 1165 Aloha.Log.debug(that, 'hiding tab strip for tab ' + tab.label); 1166 } 1167 that.extTabPanel.hideTabStripItem(tab.extPanel); 1168 } 1169 } 1170 }); 1171 1172 // check whether the last tab which was selected by the user is visible and not the active tab 1173 if (showUserActivatedTab) { 1174 if (Aloha.Log.isDebugEnabled()) { 1175 Aloha.Log.debug(this, 'Setting active tab to ' + showUserActivatedTab.label); 1176 } 1177 this.extTabPanel.setActiveTab(showUserActivatedTab.extPanel); 1178 } else if (typeof activeTab === 'object' && typeof firstVisibleTab === 'object') { 1179 // now check the currently visible tab, whether it is visible and enabled 1180 if (!activeTab.visible) { 1181 if (Aloha.Log.isDebugEnabled()) { 1182 Aloha.Log.debug(this, 'Setting active tab to ' + firstVisibleTab.label); 1183 } 1184 this.autoActivatedTab = firstVisibleTab.extPanel.title; 1185 this.extTabPanel.setActiveTab(firstVisibleTab.extPanel); 1186 } 1187 } 1188 1189 // set visibility of floatingmenu 1190 if (floatingMenuVisible && this.extTabPanel.hidden) { 1191 // set the remembered position 1192 this.extTabPanel.show(); 1193 this.refreshShadow(); 1194 this.extTabPanel.shadow.show(); 1195 this.extTabPanel.setPosition(this.left, this.top); 1196 } else if (!floatingMenuVisible && !this.extTabPanel.hidden) { 1197 // remember the current position 1198 pos = this.extTabPanel.getPosition(true); 1199 // restore previous position if the fm was pinned 1200 this.left = pos[0] < 0 ? 100 : pos[0]; 1201 this.top = pos[1] < 0 ? 100 : pos[1]; 1202 this.extTabPanel.hide(); 1203 this.extTabPanel.shadow.hide(); 1204 } /*else { 1205 var target = that.calcFloatTarget(Aloha.Selection.getRangeObject()); 1206 if (target) { 1207 this.left = target.left; 1208 this.top = target.top; 1209 this.extTabPanel.show(); 1210 this.refreshShadow(); 1211 this.extTabPanel.shadow.show(); 1212 this.extTabPanel.setPosition(this.left, this.top); 1213 1214 that.floatTo(target); 1215 } 1216 }*/ 1217 1218 // let the Ext object render itself again 1219 this.extTabPanel.doLayout(); 1220 }, 1221 1222 /** 1223 * Set the current scope 1224 * @method 1225 * @param {String} scope name of the new current scope 1226 */ 1227 setScope: function(scope) { 1228 // get the scope object 1229 var scopeObject = this.scopes[scope]; 1230 1231 if (typeof scopeObject === 'undefined') { 1232 // TODO log an error 1233 } else if (this.currentScope != scope) { 1234 this.currentScope = scope; 1235 1236 // first hide all buttons 1237 jQuery.each(this.allButtons, function(index, buttonInfo) { 1238 buttonInfo.scopeVisible = false; 1239 }); 1240 1241 // now set the buttons in the given scope to be visible 1242 this.setButtonScopeVisibility(scopeObject); 1243 1244 // finally refresh the layout 1245 this.doLayout(); 1246 } 1247 }, 1248 1249 /** 1250 * Set the scope visibility of the buttons for the given scope. This method will call itself for the motherscopes of the given scope. 1251 * @param scopeObject scope object 1252 * @hide 1253 */ 1254 setButtonScopeVisibility: function(scopeObject) { 1255 var that = this; 1256 1257 // set all buttons in the given scope to be visible 1258 jQuery.each(scopeObject.buttons, function(index, buttonInfo) { 1259 buttonInfo.scopeVisible = true; 1260 }); 1261 1262 // now do the recursion for the motherscopes 1263 jQuery.each(scopeObject.extendedScopes, function(index, scopeName) { 1264 var motherScopeObject = that.scopes[scopeName]; 1265 if (typeof motherScopeObject === 'object') { 1266 that.setButtonScopeVisibility(motherScopeObject); 1267 } 1268 }); 1269 }, 1270 1271 /** 1272 * returns the next possible float target dom obj 1273 * the floating menu should only float to h1-h6, p, div, td and pre elements 1274 * if the current object is not valid, it's parentNode will be considered, until 1275 * the limit object is hit 1276 * @param obj the dom object to start from (commonly this would be the commonAncestorContainer) 1277 * @param limitObj the object that limits the range (this would be the editable) 1278 * @return dom object which qualifies as a float target 1279 * @hide 1280 */ 1281 nextFloatTargetObj: function (obj, limitObj) { 1282 // if we've hit the limit object we don't care for it's type 1283 if (!obj || obj == limitObj) { 1284 return obj; 1285 } 1286 1287 // fm will only float to h1-h6, p, div, td 1288 switch (obj.nodeName.toLowerCase()) { 1289 case 'h1': 1290 case 'h2': 1291 case 'h3': 1292 case 'h4': 1293 case 'h5': 1294 case 'h6': 1295 case 'p': 1296 case 'div': 1297 case 'td': 1298 case 'pre': 1299 case 'ul': 1300 case 'ol': 1301 return obj; 1302 default: 1303 return this.nextFloatTargetObj(obj.parentNode, limitObj); 1304 } 1305 }, 1306 1307 /** 1308 * calculates the float target coordinates for a range 1309 * @param range the fm should float to 1310 * @return object containing left and top coordinates, like { left : 20, top : 43 } 1311 * @hide 1312 */ 1313 calcFloatTarget: function(range) { 1314 var 1315 i, documentWidth, editableLength, left, target, 1316 targetObj, scrollTop, top; 1317 1318 // TODO in IE8 somteimes a broken range is handed to this function - investigate this 1319 if (!Aloha.activeEditable || typeof range.getCommonAncestorContainer === 'undefined') { 1320 return false; 1321 } 1322 1323 // check if the designated editable is disabled 1324 for ( i = 0, editableLength = Aloha.editables.length; i < editableLength; i++) { 1325 if (Aloha.editables[i].obj.get(0) == range.limitObject && 1326 Aloha.editables[i].isDisabled()) { 1327 return false; 1328 } 1329 } 1330 1331 target = this.nextFloatTargetObj(range.getCommonAncestorContainer(), range.limitObject); 1332 if ( ! target ) { 1333 return false; 1334 } 1335 1336 targetObj = jQuery(target); 1337 scrollTop = GENTICS.Utils.Position.Scroll.top; 1338 if (!targetObj || !targetObj.offset()) { 1339 return false; 1340 } 1341 top = targetObj.offset().top - this.obj.height() - 50; // 50px offset above the current obj to have some space above 1342 1343 // if the floating menu would be placed higher than the top of the screen... 1344 if ( top < scrollTop) { 1345 top += 80 + GENTICS.Utils.Position.ScrollCorrection.top; 1346 // 80px if editable element is eg h1; 50px was fine for p; 1347 // todo: maybe just use GENTICS.Utils.Position.ScrollCorrection.top with a better value? 1348 // check where this is also used ... 1349 } 1350 1351 // if the floating menu would float off the bottom of the screen 1352 // we don't want it to move, so we'll return false 1353 if (top > this.window.height() + this.window.scrollTop()) { 1354 return false; 1355 } 1356 1357 // check if the floating menu does not float off the right side 1358 left = Aloha.activeEditable.obj.offset().left; 1359 documentWidth = jQuery(document).width(); 1360 if ( documentWidth - this.width < left ) { 1361 left = documentWidth - this.width - GENTICS.Utils.Position.ScrollCorrection.left; 1362 } 1363 1364 return { 1365 left : left, 1366 top : top 1367 }; 1368 }, 1369 1370 /** 1371 * float the fm to the desired position 1372 * the floating menu won't float if it is pinned 1373 * @method 1374 * @param {Object} coordinate object which has a left and top property 1375 */ 1376 floatTo: function(position) { 1377 // no floating if the panel is pinned 1378 if (this.pinned) { 1379 return; 1380 } 1381 1382 var floatingmenu = this, 1383 fmpos = this.obj.offset(), 1384 lastLeft, 1385 lastTop; 1386 1387 if ( lastFloatingMenuPos.left === null ) { 1388 lastLeft = fmpos.left; 1389 lastTop = fmpos.top; 1390 } else { 1391 lastLeft = lastFloatingMenuPos.left; 1392 lastTop = lastFloatingMenuPos.top; 1393 } 1394 1395 // Place the floatingmenu to the last place the user had seen it, 1396 // then animate it into its new position. 1397 if ( lastLeft != position.left || lastTop != position.top ) { 1398 this.obj.offset({ 1399 left: lastLeft, 1400 top: lastTop 1401 }); 1402 1403 this.obj.animate( { 1404 top: position.top, 1405 left: position.left 1406 }, { 1407 queue : false, 1408 step : function( step, props ) { 1409 // update position reference 1410 if ( props.prop === 'top' ) { 1411 floatingmenu.top = props.now; 1412 } else if ( props.prop === 'left' ) { 1413 floatingmenu.left = props.now; 1414 } 1415 1416 floatingmenu.refreshShadow( false ); 1417 }, 1418 complete: function() { 1419 // When the animation is over, remember the floatingmenu's 1420 // final resting position. 1421 lastFloatingMenuPos.left = floatingmenu.left; 1422 lastFloatingMenuPos.top = floatingmenu.top; 1423 } 1424 }); 1425 } 1426 }, 1427 1428 /** 1429 * Hide the floatingmenu 1430 */ 1431 hide: function() { 1432 if (this.obj) { 1433 this.obj.hide(); 1434 } 1435 if (this.shadow) { 1436 this.shadow.hide(); 1437 } 1438 }, 1439 1440 /** 1441 * Activate the tab containing the button with given name. 1442 * If the button with given name is not found, nothing changes 1443 * @param name name of the button 1444 */ 1445 activateTabOfButton: function(name) { 1446 var tabOfButton = null; 1447 1448 // find the tab containing the button 1449 for (var t = 0; t < this.tabs.length && !tabOfButton; t++) { 1450 var tab = this.tabs[t]; 1451 for (var g = 0; g < tab.groups.length && !tabOfButton; g++) { 1452 var group = tab.groups[g]; 1453 for (var b = 0; b < group.buttons.length && !tabOfButton; b++) { 1454 var button = group.buttons[b]; 1455 if (button.button.name == name) { 1456 tabOfButton = tab; 1457 break; 1458 } 1459 } 1460 } 1461 } 1462 1463 if (tabOfButton) { 1464 this.userActivatedTab = tabOfButton.label; 1465 this.doLayout(); 1466 } 1467 } 1468 }); 1469 1470 var menu = new FloatingMenu(); 1471 menu.init(); 1472 1473 // set scope to empty if deactivated 1474 Aloha.bind('aloha-editable-deactivated', function() { 1475 menu.setScope('Aloha.empty'); 1476 }); 1477 1478 // set scope to empty if the user selectes a non contenteditable area 1479 Aloha.bind('aloha-selection-changed', function() { 1480 if ( !Aloha.Selection.isSelectionEditable() && !Aloha.Selection.isFloatingMenuVisible() ) { 1481 menu.setScope('Aloha.empty'); 1482 } 1483 }); 1484 1485 return menu; 1486 }); 1487 1488