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 21 /** 22 * @todo: - Make the sidebars resizable using drag handles 23 * - Make overlayPage setting settable from external config 24 */ 25 26 define( [ 27 'aloha/core', 28 'aloha/jquery', 29 'aloha/selection' 30 // 'aloha/plugin' // For when we plugify sidebar 31 ], function ( Aloha, jQuery, Selection, Plugin ) { 32 'use strict'; 33 34 var $ = jQuery; 35 36 var undefined = void 0; 37 // Pseudo-namespace prefix for Sidebar elements 38 // Rational: 39 // We use a prefix instead of an enclosing class or id because we need to 40 // be paranoid of unintended style inheritance in an environment like the 41 // one in which Aloha-Editor operates in, with its numerous custom plugins. 42 // eg: .inner or .btn can be used in several plugins, with eaching adding 43 // to the class styles properties that we don't want. 44 var ns = 'aloha-sidebar'; 45 var uid = +( new Date ); 46 // namespaced classnames 47 var nsClasses = { 48 'bar' : nsClass( 'bar' ), 49 'handle' : nsClass( 'handle' ), 50 'inner' : nsClass( 'inner' ), 51 'panels' : nsClass( 'panels' ), 52 'config-btn' : nsClass( 'config-btn' ), 53 'handle-icon' : nsClass( 'handle-icon' ), 54 'panel-content' : nsClass( 'panel-content' ), 55 'panel-content-inner' : nsClass( 'panel-content-inner' ), 56 'panel-content-inner-text' : nsClass( 'panel-content-inner-text' ), 57 'panel-title' : nsClass( 'panel-title' ), 58 'panel-title-arrow' : nsClass( 'panel-title-arrow' ), 59 'panel-title-text' : nsClass( 'panel-title-text' ) 60 }; 61 62 // Extend jQuery easing animations 63 if ( !jQuery.easing.easeOutExpo ) { 64 jQuery.extend(jQuery.easing, { 65 easeOutExpo: function (x, t, b, c, d) { 66 return (t==d)?b+c:c*(-Math.pow(2,-10*t/d)+1)+b; 67 }, 68 easeOutElastic: function (x, t, b, c, d) { 69 var m=Math,s=1.70158,p=0,a=c; 70 if(!t)return b; 71 if((t/=d)==1)return b+c; 72 if(!p)p=d*.3; 73 if(a<m.abs(c)){a=c;var s=p/4;}else var s=p/(2*m.PI)*m.asin(c/a); 74 return a*m.pow(2,-10*t)*m.sin((t*d-s)*(2*m.PI)/p)+c+b; 75 } 76 }); 77 } 78 79 // ------------------------------------------------------------------------ 80 // Local (helper) functions 81 // ------------------------------------------------------------------------ 82 83 /** 84 * Simple templating 85 * 86 * @param {String} str - The string containing placeholder keys in curly 87 * brackets 88 * @param {Object} obj - Associative array of replacing placeholder keys 89 * with corresponding values 90 */ 91 function supplant ( str, obj ) { 92 return str.replace( /\{([a-z0-9\-\_]+)\}/ig, function ( str, p1, offset, s ) { 93 var replacement = obj[ p1 ] || str; 94 return ( typeof replacement == 'function' ) ? replacement() : replacement; 95 } ); 96 }; 97 98 /** 99 * Wrapper to call the supplant method on a given string, taking the 100 * nsClasses object as the associative array containing the replacement 101 * pairs 102 * 103 * @param {String} str 104 * @return {String} 105 */ 106 function renderTemplate ( str ) { 107 return ( typeof str == 'string' ) ? supplant( str, nsClasses ) : str; 108 }; 109 110 /** 111 * Generates a selector string with this plugins's namespace prefixed the 112 * each classname 113 * 114 * Usage: 115 * nsSel('header,', 'main,', 'foooter ul') 116 * will return 117 * ".aloha-myplugin-header, .aloha-myplugin-main, .aloha-mypluzgin-footer ul" 118 * 119 * @return {String} 120 */ 121 function nsSel () { 122 var strBldr = [], prx = ns; 123 jQuery.each( arguments, function () { 124 strBldr.push( '.' + ( this == '' ? prx : prx + '-' + this ) ); 125 } ); 126 return jQuery.trim( strBldr.join( ' ' ) ); 127 }; 128 129 /** 130 * Generates a string with this plugins's namespace prefixed the each 131 * classname 132 * 133 * Usage: 134 * nsClass('header', 'innerheaderdiv') 135 * will return 136 * "aloha-myplugin-header aloha-myplugin-innerheaderdiv" 137 * 138 * @return {String} 139 */ 140 function nsClass () { 141 var strBldr = [], prx = ns; 142 jQuery.each( arguments, function () { 143 strBldr.push( this == '' ? prx : prx + '-' + this ); 144 } ); 145 return jQuery.trim( strBldr.join(' ') ); 146 }; 147 148 // ------------------------------------------------------------------------ 149 // Sidebar constructor 150 151 // Only instance properties are to be defined here 152 // ------------------------------------------------------------------------ 153 var Sidebar = function Sidebar ( opts ) { 154 var sidebar = this; 155 156 this.id = nsClass( ++uid ); 157 this.panels = {}; 158 this.container = jQuery( renderTemplate( 159 '<div class="{bar}">' + 160 '<div class="{handle}">' + 161 '<span class="{handle-icon}"></span>' + 162 '</div>' + 163 '<div class="{inner}">' + 164 '<ul class="{panels}"></ul>' + 165 166 '</div>' + 167 '</div>' 168 ) ); 169 // defaults 170 this.width = 300; 171 this.opened = false; 172 this.isOpen = false; 173 174 this.settings = { 175 // We automatically set this to true when we are in IE, where rotating 176 // elements using filters causes undesirable rendering ugliness. 177 // Our solution is to fallback to swapping icon images. 178 // We set this as a sidebar property so that it can overridden by 179 // whoever thinks they are smarter than we are. 180 rotateIcons : !jQuery.browser.msie, 181 overlayPage : true 182 }; 183 184 // Initialize after dom is ready 185 jQuery( function () { 186 if ( !( ( typeof Aloha.settings.sidebar != 'undefined' ) && 187 Aloha.settings.sidebar.disabled ) ) { 188 sidebar.init( opts ); 189 } 190 } ); 191 192 }; 193 194 // ------------------------------------------------------------------------ 195 // Sidebar prototype 196 // All properties to be shared across Sidebar instances can be placed in 197 // the prototype object 198 // ------------------------------------------------------------------------ 199 jQuery.extend(Sidebar.prototype, { 200 201 // Build as much of the sidebar as we can before appending it to DOM to 202 // minimize reflow. 203 init: function (opts) { 204 var that = this; 205 var panels; 206 207 // Pluck panels list from opts 208 if (typeof opts == 'object') { 209 panels = opts.panels; 210 delete opts.panels; 211 } 212 213 // Copy any implements, and overrides in opts to this Sidebar instance 214 jQuery.extend(this, opts); 215 216 if (typeof panels == 'object') { 217 jQuery.each(panels, function () { 218 that.addPanel(this, true); 219 }); 220 } 221 222 var bar = this.container; 223 224 if (this.position == 'right') { 225 bar.addClass(nsClass('right')); 226 } 227 228 // Place the bar into the DOM 229 bar.hide() 230 .appendTo(jQuery('body')) 231 .click(function () {that.barClicked.apply(that, arguments);}) 232 .find(nsSel('panels')).width(this.width); 233 234 // IE7 needs us to explicitly set the container width, since it is 235 // unable to determine it on its own 236 bar.width(this.width); 237 238 this.width = bar.width(); 239 240 jQuery(window).resize(function () { 241 that.updateHeight(); 242 }); 243 244 this.updateHeight(); 245 this.roundCorners(); 246 this.initToggler(); 247 248 this.container.css(this.position == 'right' ? 'marginRight' : 'marginLeft', -this.width); 249 250 if (this.opened) { 251 this.open(0); 252 } 253 254 this.toggleHandleIcon(this.isOpen); 255 256 this.subscribeToEvents(); 257 258 jQuery(window).resize(function () { 259 that.correctHeight(); 260 }); 261 262 this.correctHeight(); 263 }, 264 265 show: function () { 266 267 this.container.css( 'display', 'block' ); 268 //.animate({opacity: 1}, 1000); 269 return this; 270 }, 271 272 hide: function () { 273 this.container.css( 'display','none' ); 274 // .animate({opacity: 0}, 1000, function () { 275 // jQuery(this).css('display', 'block') 276 // }); 277 return this; 278 }, 279 280 /** 281 * Determines the effective elements at the current selection. 282 * Iterates through all panels and checks whether the panel should be 283 * activated for any of the effective elements in the selection. 284 * 285 * @param {Object} range - The Aloha.RangeObject 286 */ 287 checkActivePanels: function( range ) { 288 var effective = []; 289 290 if ( typeof range != 'undefined' && 291 typeof range.markupEffectiveAtStart != 'undefined' ) { 292 var l = range.markupEffectiveAtStart.length; 293 for ( var i = 0; i < l; ++i ) { 294 effective.push( jQuery( range.markupEffectiveAtStart[ i ] ) ); 295 } 296 } 297 298 var that = this; 299 300 jQuery.each( this.panels, function () { 301 that.showActivePanel( this, effective ); 302 } ); 303 304 this.correctHeight(); 305 }, 306 307 subscribeToEvents: function () { 308 var that = this; 309 var $container = this.container; 310 311 Aloha.bind( 'aloha-selection-changed', function( event, range ) { 312 that.checkActivePanels( range ); 313 } ); 314 315 $container.mousedown( function( e ) { 316 e.originalEvent.stopSelectionUpdate = true; 317 Aloha.eventHandled = true; 318 //e.stopSelectionUpdate = true; 319 } ); 320 321 $container.mouseup( function ( e ) { 322 e.originalEvent.stopSelectionUpdate = true; 323 Aloha.eventHandled = false; 324 } ); 325 326 Aloha.bind( 'aloha-editable-deactivated', function ( event, params ) { 327 that.checkActivePanels(); 328 } ); 329 }, 330 331 /** 332 * Dynamically set appropriate heights for panels. 333 * The height for each panel is determined by the amount of space that 334 * is available in the viewport and the number of panels that need to 335 * share that space. 336 */ 337 correctHeight: function () { 338 var height = this.container.find(nsSel('inner')).height() - (15 * 2); 339 var panels = []; 340 341 jQuery.each(this.panels, function () { 342 if (this.isActive) { 343 panels.push(this); 344 } 345 }); 346 347 if (panels.length == 0) { 348 return; 349 } 350 351 var remainingHeight = height - ((panels[0].title.outerHeight() + 10) * panels.length); 352 var panel; 353 var targetHeight; 354 var panelInner; 355 var panelText; 356 var undone; 357 var toadd = 0; 358 var math = Math; // Local reference for quicker lookup 359 360 while (panels.length > 0 && remainingHeight > 0) { 361 remainingHeight += toadd; 362 363 toadd = 0; 364 undone = []; 365 366 for (var j = panels.length - 1; j >= 0; --j) { 367 panel = panels[j]; 368 panelInner = panel.content.find(nsSel('panel-content-inner')); 369 370 targetHeight = math.min( 371 panelInner.height('auto').height(), 372 math.floor(remainingHeight / (j + 1)) 373 ); 374 375 panelInner.height(targetHeight); 376 377 remainingHeight -= targetHeight; 378 379 panelText = panelInner.find(nsSel('panel-content-inner-text')); 380 381 382 if (panelText.height() > targetHeight) { 383 undone.push(panel); 384 toadd += targetHeight; 385 panelInner.css({ 386 'overflow-x': 'hidden', 387 'overflow-y': 'scroll' 388 }); 389 } else { 390 panelInner.css('overflow-y', 'hidden'); 391 } 392 393 if (panel.expanded) { 394 panel.expand(); 395 } 396 } 397 398 panels = undone; 399 } 400 }, 401 402 /** 403 * Checks whether this panel should be activated (ie: made visible) for 404 * any of the elements specified in a given list of elements. 405 * 406 * We have to add a null object to the list of elements to allow us to 407 * check whether the panel should be visible when we have no effective 408 * elements in the current selection 409 * 410 * @param {Object} panel - The Panel object we will test 411 * @param {Array} elements - The effective elements (jQuery), any of 412 * which may activate the panel 413 */ 414 showActivePanel: function (panel, elements) { 415 elements.push(null); 416 417 var j = elements.length; 418 var count = 0; 419 var li = panel.content.parent('li'); 420 var activeOn = panel.activeOn; 421 var effective = jQuery(); 422 423 for (var i = 0; i < j; ++i) { 424 if (activeOn(elements[i])) { 425 ++count; 426 if (elements[i]) { 427 jQuery.merge(effective, elements[i]); 428 } 429 } 430 } 431 432 if (count) { 433 panel.activate(effective); 434 } else { 435 panel.deactivate(); 436 } 437 438 this.roundCorners(); 439 }, 440 441 /** 442 * Sets up the functionality, event listeners, and animation of the 443 * sidebar handle 444 */ 445 initToggler: function () { 446 var that = this; 447 var bar = this.container; 448 var icon = bar.find(nsSel('handle-icon')); 449 var toggledClass = nsClass('toggled'); 450 var bounceTimer; 451 var isRight = (this.position == 'right'); 452 453 if (this.opened) { 454 this.rotateHandleArrow(isRight ? 0 : 180, 0); 455 } 456 457 // configure the position of the sidebar handle 458 jQuery( function () { 459 if ( typeof Aloha.settings.sidebar != 'undefined' && 460 Aloha.settings.sidebar.handle && 461 Aloha.settings.sidebar.handle.top ) { 462 jQuery(bar.find(nsSel('handle'))).get(0).style.top = Aloha.settings.sidebar.handle.top; 463 } 464 } ); 465 466 bar.find(nsSel('handle')) 467 .click(function () { 468 if (bounceTimer) { 469 clearInterval(bounceTimer); 470 } 471 472 icon.stop().css('marginLeft', 4); 473 474 if (that.isOpen) { 475 jQuery(this).removeClass(toggledClass); 476 that.close(); 477 that.isOpen = false; 478 } else { 479 jQuery(this).addClass(toggledClass); 480 that.open(); 481 that.isOpen = true; 482 } 483 }).hover( 484 function () { 485 var flag = that.isOpen ? -1 : 1; 486 487 if (bounceTimer) { 488 clearInterval(bounceTimer); 489 } 490 491 icon.stop(); 492 493 jQuery(this).stop().animate( 494 isRight ? {marginLeft: '-=' + (flag * 5)} : {marginRight: '-=' + (flag * 5)}, 495 200 496 ); 497 498 bounceTimer = setInterval(function () { 499 flag *= -1; 500 icon.animate( 501 isRight ? {left: '-=' + (flag * 4)} : {right: '-=' + (flag * 4)}, 502 300 503 ); 504 }, 300); 505 }, 506 507 function () { 508 if (bounceTimer) { 509 clearInterval(bounceTimer); 510 } 511 512 icon.stop().css(isRight ? 'left' : 'right', 5); 513 514 jQuery(this).stop().animate( 515 isRight ? {marginLeft: 0} : {marginRight: 0}, 516 600, 'easeOutElastic' 517 ); 518 } 519 ); 520 }, 521 522 /** 523 * Rounds the top corners of the first visible panel, and the bottom 524 * corners of the last visible panel elements in the panels ul list 525 */ 526 roundCorners: function () { 527 var bar = this.container; 528 var lis = bar.find(nsSel('panels>li:not(', 'deactivated)')); 529 var topClass = nsClass('panel-top'); 530 var bottomClass = nsClass('panel-bottom'); 531 532 bar.find(nsSel('panel-top,', 'panel-bottom')) 533 .removeClass(topClass) 534 .removeClass(bottomClass); 535 536 lis.first().find(nsSel('panel-title')).addClass(topClass); 537 lis.last().find(nsSel('panel-content')).addClass(bottomClass); 538 }, 539 540 /** 541 * Updates the height of the inner div of the sidebar. This is done 542 * whenever the viewport is resized 543 */ 544 updateHeight: function () { 545 var h = jQuery(window).height(); 546 this.container.height(h).find(nsSel('inner')).height(h); 547 }, 548 549 /** 550 * Delegate all sidebar onclick events to the container. 551 * Then use handleBarclick method until we bubble up to the first 552 * significant element that we can interact with 553 */ 554 barClicked: function (ev) { 555 this.handleBarclick(jQuery(ev.target)); 556 }, 557 558 /** 559 * We handle all click events on the sidebar from here--dispatching 560 * calls to which ever methods that should be invoked for the each 561 * interaction 562 */ 563 handleBarclick: function (el) { 564 if (el.hasClass(nsClass('panel-title'))) { 565 this.togglePanel(el); 566 } else if (el.hasClass(nsClass('panel-content'))) { 567 // Aloha.Log.log('Content clicked'); 568 } else if (el.hasClass(nsClass('handle'))) { 569 // Aloha.Log.log('Handle clicked'); 570 } else if (el.hasClass(nsClass('bar'))) { 571 // Aloha.Log.log('Sidebar clicked'); 572 } else { 573 this.handleBarclick(el.parent()); 574 } 575 }, 576 577 getPanelById: function (id) { 578 return this.panels[id]; 579 }, 580 581 getPanelByElement: function (el) { 582 var li = (el[0].tagName == 'LI') ? el : el.parent('li'); 583 return this.getPanelById(li[0].id); 584 }, 585 586 togglePanel: function (el) { 587 this.getPanelByElement(el).toggle(); 588 }, 589 590 /** 591 * Animation to rotate the sidebar arrow 592 * 593 * @param {Number} angle - The angle two which the arrow should rotate 594 * (0 or 180) 595 * @param {Number|String} duration - (Optional) How long the animation 596 * should play for 597 */ 598 rotateHandleIcon: function (angle, duration) { 599 var arr = this.container.find(nsSel('handle-icon')); 600 arr.animate({angle: angle}, { 601 duration : (typeof duration == 'number' || typeof duration == 'string') ? duration : 500, 602 easing : 'easeOutExpo', 603 step : function (val, fx) { 604 arr.css({ 605 '-o-transform' : 'rotate(' + val + 'deg)', 606 '-webkit-transform' : 'rotate(' + val + 'deg)', 607 '-moz-transform' : 'rotate(' + val + 'deg)', 608 '-ms-transform' : 'rotate(' + val + 'deg)' 609 // We cannot use Microsoft Internet Explorer filters 610 // because Microsoft Internet Explore 8 does not support 611 // Microsoft Internet Explorer filters correctly. It 612 // breaks the layout 613 // filter : 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (angle / 90) + ')' 614 }); 615 } 616 }); 617 }, 618 619 /** 620 * Sets the handle icon to the "i am opened, click me to close the 621 * sidebar" state, or vice versa. The direction of the arrow depends 622 * on whether the sidebar is on the left or right, and whether it is 623 * in an opened state or not. 624 * 625 * Question: 626 * Given that the arrow icon is by default pointing right, should 627 * we make it point left? 628 * 629 * Answer: 630 * isRight & isOpen : no 631 * isRight & isClosed : yes 632 * isLeft & isOpen : yes 633 * isLeft & isClosed : no 634 * 635 * Truth table: 636 * isRight | isOpen | XOR 637 * ---------+--------+----- 638 * T | T | F 639 * T | F | T 640 * F | T | T 641 * F | F | F 642 * 643 * Therefore: 644 * isPointingLeft = isRight XOR isOpen 645 * 646 * @param {Boolean} isOpened - Whether or not the sidebar is in the 647 * opened state 648 */ 649 toggleHandleIcon: function (isOpen) { 650 var isPointingLeft = (this.position == 'right') ^ isOpen; 651 652 if (this.settings.rotateIcons) { 653 this.rotateHandleIcon(isPointingLeft ? 180 : 0, 0); 654 } else { 655 var icon = this.container.find(nsSel('handle-icon')); 656 657 if (isPointingLeft) { 658 icon.addClass(nsClass('handle-icon-left')); 659 } else { 660 icon.removeClass(nsClass('handle-icon-left')); 661 } 662 } 663 }, 664 665 /** 666 * Slides the sidebar into view 667 */ 668 open: function (duration, callback) { 669 if (this.isOpen) { 670 return this; 671 } 672 673 var isRight = (this.position == 'right'); 674 var anim = isRight ? {marginRight: 0} : {marginLeft: 0}; 675 676 this.toggleHandleIcon(true); 677 678 this.container.animate( 679 anim, 680 (typeof duration == 'number' || typeof duration == 'string') 681 ? duration : 500, 682 'easeOutExpo' 683 ); 684 685 if (!this.settings.overlayPage) { 686 jQuery('body').animate( 687 isRight ? {marginRight: '+=' + this.width} : {marginLeft: '+=' + this.width}, 688 500, 'easeOutExpo' 689 ); 690 } 691 692 this.isOpen = true; 693 694 jQuery('body').trigger(nsClass('opened'), this); 695 696 return this; 697 }, 698 699 /** 700 * Slides that sidebar out of view 701 */ 702 close: function (duration, callback) { 703 if (!this.isOpen) { 704 return this; 705 } 706 707 var isRight = (this.position == 'right'); 708 var anim = isRight ? {marginRight: -this.width} : {marginLeft: -this.width}; 709 710 this.toggleHandleIcon(false); 711 712 this.container.animate( 713 anim, 714 (typeof duration == 'number' || typeof duration == 'string') 715 ? duration : 500, 716 'easeOutExpo' 717 ); 718 719 if (!this.settings.overlayPage) { 720 jQuery('body').animate( 721 isRight ? {marginRight: '-=' + this.width} : {marginLeft: '-=' + this.width}, 722 500, 'easeOutExpo' 723 ); 724 } 725 726 this.isOpen = false; 727 728 return this; 729 }, 730 731 /** 732 * Activates the given panel and passes to it the given element as the 733 * the effective that we want it to think activated it. 734 * 735 * @param {Object|String} panel - Panel instance or the id of a panel 736 * object 737 * @param {jQuery} element - Element to pass to the panel as effective 738 * element (the element that activated it) 739 */ 740 activatePanel: function (panel, element) { 741 if (typeof panel == 'string') { 742 panel = this.getPanelById(panel); 743 } 744 745 if (panel){ 746 panel.activate(element); 747 } 748 749 this.roundCorners(); 750 751 return this; 752 }, 753 754 /** 755 * Invokes the expand method for the given panel so that it expands its 756 * height to display its contents 757 * 758 * @param {Object|String} panel - Panel instance or the id of a panel 759 * object 760 * @param {Funtion} callback 761 */ 762 expandPanel: function (panel, callback) { 763 if (typeof panel == 'string') { 764 panel = this.getPanelById(panel); 765 } 766 767 if (panel){ 768 panel.expand(callback); 769 } 770 771 return this; 772 }, 773 774 /** 775 * Collapses the panel contents by invoking the given panel's collapse 776 * method. 777 * 778 * @param {Object|String} panel - Panel instance or the id of a panel 779 * object 780 * @param {Funtion} callback 781 */ 782 collapsePanel: function (panel, callback) { 783 if (typeof panel == 'string') { 784 panel = this.getPanelById(panel); 785 } 786 787 if (panel){ 788 panel.collapse(callback); 789 } 790 791 return this; 792 }, 793 794 /** 795 * Adds a panel to this sidebar instance. 796 * We try and build as much of the panel DOM as we can before inserting 797 * it into the DOM in order to reduce reflow. 798 * 799 * @param {Object} panel - either a panel instance or an associative 800 * array containing settings for the construction 801 * of a new panel. 802 * @param {Boolean} deferRounding - (Optional) If true, the rounding-off 803 * of the top most and bottom most panels 804 * will not be automatically done. Set 805 * this to true when adding a lot of panels 806 * at once. 807 * @return {Object} - The newly created panel. 808 */ 809 addPanel: function (panel, deferRounding) { 810 if (!(panel instanceof Panel)) { 811 if (!panel.width) { 812 panel.width = this.width; 813 } 814 panel.sidebar = this; 815 panel = new Panel(panel); 816 } 817 818 this.panels[panel.id] = panel; 819 820 this.container.find(nsSel('panels')).append(panel.element); 821 822 if (deferRounding !== true) { 823 this.roundCorners(); 824 } 825 this.checkActivePanels(Selection.getRangeObject()); 826 return panel; 827 } 828 829 }); 830 831 // ------------------------------------------------------------------------ 832 // Panel constructor 833 // ------------------------------------------------------------------------ 834 var Panel = function Panel (opts) { 835 this.id = null; 836 this.folds = {}; 837 this.button = null; 838 this.title = jQuery(renderTemplate(' \ 839 <div class="{panel-title}"> \ 840 <span class="{panel-title-arrow}"></span> \ 841 <span class="{panel-title-text}">Untitled</span> \ 842 </div> \ 843 ')); 844 this.content = jQuery(renderTemplate(' \ 845 <div class="{panel-content}"> \ 846 <div class="{panel-content-inner}"> \ 847 <div class="{panel-content-inner-text}">\ 848 </div> \ 849 </div> \ 850 </div> \ 851 ')); 852 this.element = null; 853 this.expanded = false; 854 this.effectiveElement = null; 855 this.isActive = true; 856 857 this.init(opts); 858 }; 859 860 // ------------------------------------------------------------------------ 861 // Panel prototype 862 // ------------------------------------------------------------------------ 863 jQuery.extend(Panel.prototype, { 864 865 init: function (opts) { 866 this.setTitle(opts.title) 867 .setContent(opts.content); 868 869 delete opts.title; 870 delete opts.content; 871 872 jQuery.extend(this, opts); 873 874 if (!this.id) { 875 this.id = nsClass(++uid); 876 } 877 878 var li = this.element = 879 jQuery('<li id="' +this.id + '">') 880 .append(this.title, this.content); 881 882 if (this.expanded){ 883 this.content.height('auto'); 884 } 885 886 this.toggleTitleIcon(this.expanded); 887 888 this.coerceActiveOn(); 889 890 // Disable text selection on title element 891 this.title 892 .attr('unselectable', 'on') 893 .css('-moz-user-select', 'none') 894 .each(function() {this.onselectstart = function() {return false;};}); 895 896 if (typeof this.onInit == 'function') { 897 this.onInit.apply(this); 898 } 899 }, 900 901 /** 902 * @param {Boolean} isExpanded - Whether or not the panel is in an 903 * expanded state 904 */ 905 toggleTitleIcon: function (isExpanded) { 906 if (this.sidebar.settings.rotateIcons) { 907 this.rotateTitleIcon(isExpanded ? 90 : 0); 908 } else { 909 var icon = this.title.find(nsSel('panel-title-arrow')); 910 911 if (isExpanded) { 912 icon.addClass(nsClass('panel-title-arrow-down')); 913 } else { 914 icon.removeClass(nsClass('panel-title-arrow-down')); 915 } 916 } 917 }, 918 919 /** 920 * Normalizes the activeOn property into a predicate function 921 */ 922 coerceActiveOn: function () { 923 if (typeof this.activeOn != 'function') { 924 var activeOn = this.activeOn; 925 926 this.activeOn = (function () { 927 var typeofActiveOn = typeof activeOn, 928 fn; 929 930 if (typeofActiveOn == 'boolean') { 931 fn = function () { 932 return activeOn; 933 }; 934 } else if (typeofActiveOn == 'undefined') { 935 fn = function () { 936 return true; 937 }; 938 } else if (typeofActiveOn == 'string') { 939 fn = function (el) { 940 return el ? el.is(activeOn) : false; 941 }; 942 } else { 943 fn = function () { 944 return false; 945 }; 946 } 947 948 return fn; 949 })(); 950 } 951 }, 952 953 /** 954 * Activates (displays) this panel 955 */ 956 activate: function (effective) { 957 this.isActive = true; 958 this.content.parent('li').show().removeClass(nsClass('deactivated')); 959 this.effectiveElement = effective; 960 if (typeof this.onActivate == 'function') { 961 this.onActivate.call(this, effective); 962 } 963 }, 964 965 /** 966 * Hides this panel 967 */ 968 deactivate: function () { 969 this.isActive = false; 970 this.content.parent('li').hide().addClass(nsClass('deactivated')); 971 this.effectiveElement = null; 972 }, 973 974 toggle: function () { 975 if (this.expanded) { 976 this.collapse(); 977 } else { 978 this.expand(); 979 } 980 }, 981 982 /** 983 * Displays the panel's contents 984 */ 985 expand: function (callback) { 986 var that = this; 987 var el = this.content; 988 var old_h = el.height(); 989 var new_h = el.height('auto').height(); 990 991 el.height(old_h).stop().animate( 992 {height: new_h}, 500, 'easeOutExpo', 993 function () { 994 if (typeof callback == 'function') { 995 callback.call(that); 996 } 997 } 998 ); 999 1000 this.element.removeClass('collapsed'); 1001 this.toggleTitleIcon(true); 1002 1003 this.expanded = true; 1004 1005 return this; 1006 }, 1007 1008 /** 1009 * Hides the panel's contents--leaving only it's header 1010 */ 1011 collapse: function (duration, callback) { 1012 var that = this; 1013 this.element.addClass('collapsed'); 1014 this.content.stop().animate( 1015 {height: 5}, 250, 'easeOutExpo', 1016 function () { 1017 if (typeof callback == 'function') { 1018 callback.call(that); 1019 } 1020 } 1021 ); 1022 1023 this.toggleTitleIcon(false); 1024 1025 this.expanded = false; 1026 1027 return this; 1028 }, 1029 1030 /** 1031 * May also be called by the Sidebar to update title of panel 1032 * 1033 * @param html - Markup string, DOM object, or jQuery object 1034 */ 1035 setTitle: function (html) { 1036 this.title.find(nsSel('panel-title-text')).html(html); 1037 return this; 1038 }, 1039 1040 /** 1041 * May also be called by the Sidebar to update content of panel 1042 * 1043 * @param html - Markup string, DOM object, or jQuery object 1044 */ 1045 setContent: function (html) { 1046 // We do this so that empty panels don't appear collapsed 1047 if (!html || html == '') { 1048 html = ' '; 1049 } 1050 1051 this.content.find(nsSel('panel-content-inner-text')).html(html); 1052 return this; 1053 }, 1054 1055 rotateTitleIcon: function (angle, duration) { 1056 var arr = this.title.find(nsSel('panel-title-arrow')); 1057 arr.animate({angle: angle}, { 1058 duration : (typeof duration == 'number') ? duration : 500, 1059 easing : 'easeOutExpo', 1060 step : function (val, fx) { 1061 arr.css({ 1062 '-o-transform' : 'rotate(' + val + 'deg)', 1063 '-webkit-transform' : 'rotate(' + val + 'deg)', 1064 '-moz-transform' : 'rotate(' + val + 'deg)', 1065 '-ms-transform' : 'rotate(' + val + 'deg)' 1066 // filter : 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (angle / 90) + ')' 1067 }); 1068 } 1069 }); 1070 }, 1071 1072 /** 1073 * Walks up the ancestors chain for the given effective element, and 1074 * renders subpanels using the specified renderer function. 1075 * 1076 * @param {jQuery} effective - The effective element, whose lineage we 1077 * want to render 1078 * @param {Function} renderer - (Optional) function that will render 1079 * each element in the parental lineage 1080 * of the effective element 1081 */ 1082 renderEffectiveParents: function (effective, renderer) { 1083 var el = effective.first(); 1084 var content = []; 1085 var path = []; 1086 var activeOn = this.activeOn; 1087 var l; 1088 var pathRev; 1089 1090 while (el.length > 0 && !el.is('.aloha-editable')) { 1091 1092 if (activeOn(el)) { 1093 path.push('<span>' + el[0].tagName.toLowerCase() + '</span>'); 1094 l = path.length; 1095 pathRev = []; 1096 while (l--) { 1097 1098 pathRev.push(path[l]); 1099 } 1100 content.push(supplant( 1101 '<div class="aloha-sidebar-panel-parent">' + 1102 '<div class="aloha-sidebar-panel-parent-path">{path}</div>' + 1103 '<div class="aloha-sidebar-panel-parent-content aloha-sidebar-opened">{content}</div>' + 1104 '</div>', 1105 { 1106 path : pathRev.join(''), 1107 content : (typeof renderer == 'function') ? renderer(el) : '----' 1108 } 1109 )); 1110 } 1111 1112 el = el.parent(); 1113 } 1114 1115 this.setContent(content.join('')); 1116 1117 jQuery('.aloha-sidebar-panel-parent-path').click(function () { 1118 var c = jQuery(this).parent().find('.aloha-sidebar-panel-parent-content'); 1119 1120 if (c.hasClass('aloha-sidebar-opened')) { 1121 c.hide().removeClass('aloha-sidebar-opened'); 1122 } else { 1123 c.show().addClass('aloha-sidebar-opened'); 1124 } 1125 }); 1126 1127 this.content.height('auto').find('.aloha-sidebar-panel-content-inner').height('auto'); 1128 } 1129 1130 }); 1131 1132 var left = new Sidebar({ 1133 position : 'left', 1134 width : 250 // TODO define in config 1135 }); 1136 1137 var right = new Sidebar({ 1138 position : 'right', 1139 width : 250 // TODO define in config 1140 }); 1141 1142 Aloha.Sidebar = { 1143 left : left, 1144 right : right 1145 }; 1146 1147 return Aloha.Sidebar; 1148 1149 }); 1150