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