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