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 // Sidebar prototype 195 // All properties to be shared across Sidebar instances can be placed in 196 // the prototype object 197 // ------------------------------------------------------------------------ 198 jQuery.extend(Sidebar.prototype, { 199 200 // Build as much of the sidebar as we can before appending it to DOM to 201 // minimize reflow. 202 init: function (opts) { 203 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 this.container.css( 'display', 'block' ); 267 //.animate({opacity: 1}, 1000); 268 return this; 269 }, 270 271 hide: function () { 272 this.container.css( 'display','none' ); 273 // .animate({opacity: 0}, 1000, function () { 274 // jQuery(this).css('display', 'block') 275 // }); 276 return this; 277 }, 278 279 /** 280 * Determines the effective elements at the current selection. 281 * Iterates through all panels and checks whether the panel should be 282 * activated for any of the effective elements in the selection. 283 * 284 * @param {Object} range - The Aloha.RangeObject 285 */ 286 checkActivePanels: function( range ) { 287 var effective = []; 288 289 if ( typeof range != 'undefined' && 290 typeof range.markupEffectiveAtStart != 'undefined' ) { 291 var l = range.markupEffectiveAtStart.length; 292 for ( var i = 0; i < l; ++i ) { 293 effective.push( jQuery( range.markupEffectiveAtStart[ i ] ) ); 294 } 295 } 296 297 var that = this; 298 299 jQuery.each( this.panels, function () { 300 that.showActivePanel( this, effective ); 301 } ); 302 303 this.correctHeight(); 304 }, 305 306 subscribeToEvents: function () { 307 var that = this; 308 var $container = this.container; 309 310 Aloha.bind( 'aloha-selection-changed', function( event, range ) { 311 that.checkActivePanels( range ); 312 } ); 313 314 $container.mousedown( function( e ) { 315 e.originalEvent.stopSelectionUpdate = true; 316 Aloha.eventHandled = true; 317 //e.stopSelectionUpdate = true; 318 } ); 319 320 $container.mouseup( function ( e ) { 321 e.originalEvent.stopSelectionUpdate = true; 322 Aloha.eventHandled = false; 323 } ); 324 325 Aloha.bind( 'aloha-editable-deactivated', function ( event, params ) { 326 that.checkActivePanels(); 327 } ); 328 }, 329 330 /** 331 * Dynamically set appropriate heights for panels. 332 * The height for each panel is determined by the amount of space that 333 * is available in the viewport and the number of panels that need to 334 * share that space. 335 */ 336 correctHeight: function () { 337 var height = this.container.find(nsSel('inner')).height() - (15 * 2); 338 var panels = []; 339 340 jQuery.each(this.panels, function () { 341 if (this.isActive) { 342 panels.push(this); 343 } 344 }); 345 346 if (panels.length == 0) { 347 return; 348 } 349 350 var remainingHeight = height - ((panels[0].title.outerHeight() + 10) * panels.length); 351 var panel; 352 var targetHeight; 353 var panelInner; 354 var panelText; 355 var undone; 356 var toadd = 0; 357 var math = Math; // Local reference for quicker lookup 358 359 while (panels.length > 0 && remainingHeight > 0) { 360 remainingHeight += toadd; 361 362 toadd = 0; 363 undone = []; 364 365 for (var j = panels.length - 1; j >= 0; --j) { 366 panel = panels[j]; 367 panelInner = panel.content.find(nsSel('panel-content-inner')); 368 369 targetHeight = math.min( 370 panelInner.height('auto').height(), 371 math.floor(remainingHeight / (j + 1)) 372 ); 373 374 panelInner.height(targetHeight); 375 376 remainingHeight -= targetHeight; 377 378 panelText = panelInner.find(nsSel('panel-content-inner-text')); 379 380 if (panelText.height() > targetHeight) { 381 undone.push(panel); 382 toadd += targetHeight; 383 panelInner.css({ 384 'overflow-x': 'hidden', 385 'overflow-y': 'scroll' 386 }); 387 } else { 388 panelInner.css('overflow-y', 'hidden'); 389 } 390 391 if (panel.expanded) { 392 panel.expand(); 393 } 394 } 395 396 panels = undone; 397 } 398 }, 399 400 /** 401 * Checks whether this panel should be activated (ie: made visible) for 402 * any of the elements specified in a given list of elements. 403 * 404 * We have to add a null object to the list of elements to allow us to 405 * check whether the panel should be visible when we have no effective 406 * elements in the current selection 407 * 408 * @param {Object} panel - The Panel object we will test 409 * @param {Array} elements - The effective elements (jQuery), any of 410 * which may activate the panel 411 */ 412 showActivePanel: function (panel, elements) { 413 elements.push(null); 414 415 var j = elements.length; 416 var count = 0; 417 var li = panel.content.parent('li'); 418 var activeOn = panel.activeOn; 419 var effective = jQuery(); 420 421 for (var i = 0; i < j; ++i) { 422 if (activeOn(elements[i])) { 423 ++count; 424 if (elements[i]) { 425 jQuery.merge(effective, elements[i]); 426 } 427 } 428 } 429 430 if (count) { 431 panel.activate(effective); 432 } else { 433 panel.deactivate(); 434 } 435 436 this.roundCorners(); 437 }, 438 439 /** 440 * Sets up the functionality, event listeners, and animation of the 441 * sidebar handle 442 */ 443 initToggler: function () { 444 var that = this; 445 var bar = this.container; 446 var icon = bar.find(nsSel('handle-icon')); 447 var toggledClass = nsClass('toggled'); 448 var bounceTimer; 449 var isRight = (this.position == 'right'); 450 451 if (this.opened) { 452 this.rotateHandleArrow(isRight ? 0 : 180, 0); 453 } 454 455 bar.find(nsSel('handle')) 456 .click(function () { 457 if (bounceTimer) { 458 clearInterval(bounceTimer); 459 } 460 461 icon.stop().css('marginLeft', 4); 462 463 if (that.isOpen) { 464 jQuery(this).removeClass(toggledClass); 465 that.close(); 466 that.isOpen = false; 467 } else { 468 jQuery(this).addClass(toggledClass); 469 that.open(); 470 that.isOpen = true; 471 } 472 }).hover( 473 function () { 474 var flag = that.isOpen ? -1 : 1; 475 476 if (bounceTimer) { 477 clearInterval(bounceTimer); 478 } 479 480 icon.stop(); 481 482 jQuery(this).stop().animate( 483 isRight ? {marginLeft: '-=' + (flag * 5)} : {marginRight: '-=' + (flag * 5)}, 484 200 485 ); 486 487 bounceTimer = setInterval(function () { 488 flag *= -1; 489 icon.animate( 490 isRight ? {left: '-=' + (flag * 4)} : {right: '-=' + (flag * 4)}, 491 300 492 ); 493 }, 300); 494 }, 495 496 function () { 497 if (bounceTimer) { 498 clearInterval(bounceTimer); 499 } 500 501 icon.stop().css(isRight ? 'left' : 'right', 5); 502 503 jQuery(this).stop().animate( 504 isRight ? {marginLeft: 0} : {marginRight: 0}, 505 600, 'easeOutElastic' 506 ); 507 } 508 ); 509 }, 510 511 /** 512 * Rounds the top corners of the first visible panel, and the bottom 513 * corners of the last visible panel elements in the panels ul list 514 */ 515 roundCorners: function () { 516 var bar = this.container; 517 var lis = bar.find(nsSel('panels>li:not(', 'deactivated)')); 518 var topClass = nsClass('panel-top'); 519 var bottomClass = nsClass('panel-bottom'); 520 521 bar.find(nsSel('panel-top,', 'panel-bottom')) 522 .removeClass(topClass) 523 .removeClass(bottomClass); 524 525 lis.first().find(nsSel('panel-title')).addClass(topClass); 526 lis.last().find(nsSel('panel-content')).addClass(bottomClass); 527 }, 528 529 /** 530 * Updates the height of the inner div of the sidebar. This is done 531 * whenever the viewport is resized 532 */ 533 updateHeight: function () { 534 var h = jQuery(window).height(); 535 this.container.height(h).find(nsSel('inner')).height(h); 536 }, 537 538 /** 539 * Delegate all sidebar onclick events to the container. 540 * Then use handleBarclick method until we bubble up to the first 541 * significant element that we can interact with 542 */ 543 barClicked: function (ev) { 544 this.handleBarclick(jQuery(ev.target)); 545 }, 546 547 /** 548 * We handle all click events on the sidebar from here--dispatching 549 * calls to which ever methods that should be invoked for the each 550 * interaction 551 */ 552 handleBarclick: function (el) { 553 if (el.hasClass(nsClass('panel-title'))) { 554 this.togglePanel(el); 555 } else if (el.hasClass(nsClass('panel-content'))) { 556 // Aloha.Log.log('Content clicked'); 557 } else if (el.hasClass(nsClass('handle'))) { 558 // Aloha.Log.log('Handle clicked'); 559 } else if (el.hasClass(nsClass('bar'))) { 560 // Aloha.Log.log('Sidebar clicked'); 561 } else { 562 this.handleBarclick(el.parent()); 563 } 564 }, 565 566 getPanelById: function (id) { 567 return this.panels[id]; 568 }, 569 570 getPanelByElement: function (el) { 571 var li = (el[0].tagName == 'LI') ? el : el.parent('li'); 572 return this.getPanelById(li[0].id); 573 }, 574 575 togglePanel: function (el) { 576 this.getPanelByElement(el).toggle(); 577 }, 578 579 /** 580 * Animation to rotate the sidebar arrow 581 582 * 583 * @param {Number} angle - The angle two which the arrow should rotate 584 * (0 or 180) 585 * @param {Number|String} duration - (Optional) How long the animation 586 * should play for 587 */ 588 rotateHandleIcon: function (angle, duration) { 589 var arr = this.container.find(nsSel('handle-icon')); 590 arr.animate({angle: angle}, { 591 duration : (typeof duration == 'number' || typeof duration == 'string') ? duration : 500, 592 easing : 'easeOutExpo', 593 step : function (val, fx) { 594 arr.css({ 595 '-o-transform' : 'rotate(' + val + 'deg)', 596 '-webkit-transform' : 'rotate(' + val + 'deg)', 597 '-moz-transform' : 'rotate(' + val + 'deg)', 598 '-ms-transform' : 'rotate(' + val + 'deg)' 599 // We cannot use Microsoft Internet Explorer filters 600 // because Microsoft Internet Explore 8 does not support 601 // Microsoft Internet Explorer filters correctly. It 602 // breaks the layout 603 // filter : 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (angle / 90) + ')' 604 }); 605 } 606 }); 607 }, 608 609 /** 610 * Sets the handle icon to the "i am opened, click me to close the 611 * sidebar" state, or vice versa. The direction of the arrow depends 612 * on whether the sidebar is on the left or right, and whether it is 613 * in an opened state or not. 614 * 615 * Question: 616 * Given that the arrow icon is by default pointing right, should 617 * we make it point left? 618 * 619 * Answer: 620 * isRight & isOpen : no 621 * isRight & isClosed : yes 622 * isLeft & isOpen : yes 623 * isLeft & isClosed : no 624 * 625 * Truth table: 626 * isRight | isOpen | XOR 627 * ---------+--------+----- 628 * T | T | F 629 * T | F | T 630 * F | T | T 631 * F | F | F 632 * 633 * Therefore: 634 * isPointingLeft = isRight XOR isOpen 635 * 636 * @param {Boolean} isOpened - Whether or not the sidebar is in the 637 * opened state 638 */ 639 toggleHandleIcon: function (isOpen) { 640 var isPointingLeft = (this.position == 'right') ^ isOpen; 641 642 if (this.settings.rotateIcons) { 643 this.rotateHandleIcon(isPointingLeft ? 180 : 0, 0); 644 } else { 645 var icon = this.container.find(nsSel('handle-icon')); 646 647 if (isPointingLeft) { 648 icon.addClass(nsClass('handle-icon-left')); 649 } else { 650 icon.removeClass(nsClass('handle-icon-left')); 651 } 652 } 653 }, 654 655 /** 656 * Slides the sidebar into view 657 */ 658 open: function (duration, callback) { 659 if (this.isOpen) { 660 return this; 661 } 662 663 var isRight = (this.position == 'right'); 664 var anim = isRight ? {marginRight: 0} : {marginLeft: 0}; 665 666 this.toggleHandleIcon(true); 667 668 this.container.animate( 669 anim, 670 (typeof duration == 'number' || typeof duration == 'string') 671 ? duration : 500, 672 'easeOutExpo' 673 ); 674 675 if (!this.settings.overlayPage) { 676 jQuery('body').animate( 677 isRight ? {marginRight: '+=' + this.width} : {marginLeft: '+=' + this.width}, 678 500, 'easeOutExpo' 679 ); 680 } 681 682 this.isOpen = true; 683 684 jQuery('body').trigger(nsClass('opened'), this); 685 686 return this; 687 }, 688 689 /** 690 * Slides that sidebar out of view 691 */ 692 close: function (duration, callback) { 693 if (!this.isOpen) { 694 return this; 695 } 696 697 var isRight = (this.position == 'right'); 698 var anim = isRight ? {marginRight: -this.width} : {marginLeft: -this.width}; 699 700 this.toggleHandleIcon(false); 701 702 this.container.animate( 703 anim, 704 (typeof duration == 'number' || typeof duration == 'string') 705 ? duration : 500, 706 'easeOutExpo' 707 ); 708 709 if (!this.settings.overlayPage) { 710 jQuery('body').animate( 711 isRight ? {marginRight: '-=' + this.width} : {marginLeft: '-=' + this.width}, 712 500, 'easeOutExpo' 713 ); 714 } 715 716 this.isOpen = false; 717 718 return this; 719 }, 720 721 /** 722 * Activates the given panel and passes to it the given element as the 723 * the effective that we want it to think activated it. 724 * 725 * @param {Object|String} panel - Panel instance or the id of a panel 726 * object 727 * @param {jQuery} element - Element to pass to the panel as effective 728 * element (the element that activated it) 729 */ 730 activatePanel: function (panel, element) { 731 if (typeof panel == 'string') { 732 panel = this.getPanelById(panel); 733 } 734 735 if (panel){ 736 panel.activate(element); 737 } 738 739 this.roundCorners(); 740 741 return this; 742 }, 743 744 /** 745 746 * Invokes the expand method for the given panel so that it expands its 747 * height to display its contents 748 * 749 * @param {Object|String} panel - Panel instance or the id of a panel 750 * object 751 * @param {Funtion} callback 752 */ 753 expandPanel: function (panel, callback) { 754 if (typeof panel == 'string') { 755 panel = this.getPanelById(panel); 756 } 757 758 if (panel){ 759 panel.expand(callback); 760 } 761 762 return this; 763 }, 764 765 /** 766 * Collapses the panel contents by invoking the given panel's collapse 767 * method. 768 * 769 * @param {Object|String} panel - Panel instance or the id of a panel 770 * object 771 * @param {Funtion} callback 772 */ 773 collapsePanel: function (panel, callback) { 774 if (typeof panel == 'string') { 775 panel = this.getPanelById(panel); 776 } 777 778 if (panel){ 779 panel.collapse(callback); 780 } 781 782 return this; 783 }, 784 785 /** 786 * Adds a panel to this sidebar instance. 787 * We try and build as much of the panel DOM as we can before inserting 788 * it into the DOM in order to reduce reflow. 789 * 790 * @param {Object} panel - either a panel instance or an associative 791 * array containing settings for the construction 792 * of a new panel. 793 * @param {Boolean} deferRounding - (Optional) If true, the rounding-off 794 * of the top most and bottom most panels 795 * will not be automatically done. Set 796 * this to true when adding a lot of panels 797 * at once. 798 * @return {Object} - The newly created panel. 799 */ 800 addPanel: function (panel, deferRounding) { 801 if (!(panel instanceof Panel)) { 802 if (!panel.width) { 803 panel.width = this.width; 804 } 805 panel.sidebar = this; 806 panel = new Panel(panel); 807 } 808 809 this.panels[panel.id] = panel; 810 811 this.container.find(nsSel('panels')).append(panel.element); 812 813 if (deferRounding !== true) { 814 this.roundCorners(); 815 } 816 this.checkActivePanels(Selection.getRangeObject()); 817 return panel; 818 } 819 820 }); 821 822 // ------------------------------------------------------------------------ 823 // Panel constructor 824 // ------------------------------------------------------------------------ 825 var Panel = function Panel (opts) { 826 this.id = null; 827 this.folds = {}; 828 this.button = null; 829 this.title = jQuery(renderTemplate(' \ 830 <div class="{panel-title}"> \ 831 <span class="{panel-title-arrow}"></span> \ 832 <span class="{panel-title-text}">Untitled</span> \ 833 </div> \ 834 ')); 835 this.content = jQuery(renderTemplate(' \ 836 <div class="{panel-content}"> \ 837 <div class="{panel-content-inner}"> \ 838 <div class="{panel-content-inner-text}">\ 839 </div> \ 840 </div> \ 841 </div> \ 842 ')); 843 this.element = null; 844 this.expanded = false; 845 this.effectiveElement = null; 846 this.isActive = true; 847 848 this.init(opts); 849 }; 850 851 // ------------------------------------------------------------------------ 852 // Panel prototype 853 // ------------------------------------------------------------------------ 854 jQuery.extend(Panel.prototype, { 855 856 init: function (opts) { 857 this.setTitle(opts.title) 858 .setContent(opts.content); 859 860 delete opts.title; 861 delete opts.content; 862 863 jQuery.extend(this, opts); 864 865 if (!this.id) { 866 this.id = nsClass(++uid); 867 } 868 869 var li = this.element = 870 jQuery('<li id="' +this.id + '">') 871 .append(this.title, this.content); 872 873 if (this.expanded){ 874 this.content.height('auto'); 875 } 876 877 this.toggleTitleIcon(this.expanded); 878 879 this.coerceActiveOn(); 880 881 // Disable text selection on title element 882 this.title 883 .attr('unselectable', 'on') 884 .css('-moz-user-select', 'none') 885 .each(function() {this.onselectstart = function() {return false;};}); 886 887 if (typeof this.onInit == 'function') { 888 this.onInit.apply(this); 889 } 890 }, 891 892 /** 893 * @param {Boolean} isExpanded - Whether or not the panel is in an 894 * expanded state 895 */ 896 toggleTitleIcon: function (isExpanded) { 897 if (this.sidebar.settings.rotateIcons) { 898 this.rotateTitleIcon(isExpanded ? 90 : 0); 899 } else { 900 var icon = this.title.find(nsSel('panel-title-arrow')); 901 902 if (isExpanded) { 903 icon.addClass(nsClass('panel-title-arrow-down')); 904 } else { 905 icon.removeClass(nsClass('panel-title-arrow-down')); 906 } 907 } 908 }, 909 910 /** 911 * Normalizes the activeOn property into a predicate function 912 */ 913 coerceActiveOn: function () { 914 if (typeof this.activeOn != 'function') { 915 var activeOn = this.activeOn; 916 917 this.activeOn = (function () { 918 var typeofActiveOn = typeof activeOn, 919 fn; 920 921 if (typeofActiveOn == 'boolean') { 922 fn = function () { 923 return activeOn; 924 }; 925 } else if (typeofActiveOn == 'undefined') { 926 fn = function () { 927 return true; 928 }; 929 } else if (typeofActiveOn == 'string') { 930 fn = function (el) { 931 return el ? el.is(activeOn) : false; 932 }; 933 } else { 934 fn = function () { 935 return false; 936 }; 937 } 938 939 return fn; 940 })(); 941 } 942 }, 943 944 /** 945 * Activates (displays) this panel 946 */ 947 activate: function (effective) { 948 this.isActive = true; 949 this.content.parent('li').show().removeClass(nsClass('deactivated')); 950 this.effectiveElement = effective; 951 if (typeof this.onActivate == 'function') { 952 this.onActivate.call(this, effective); 953 } 954 }, 955 956 /** 957 * Hides this panel 958 */ 959 deactivate: function () { 960 this.isActive = false; 961 this.content.parent('li').hide().addClass(nsClass('deactivated')); 962 this.effectiveElement = null; 963 }, 964 965 toggle: function () { 966 if (this.expanded) { 967 this.collapse(); 968 } else { 969 this.expand(); 970 } 971 }, 972 973 /** 974 * Displays the panel's contents 975 */ 976 expand: function (callback) { 977 var that = this; 978 var el = this.content; 979 var old_h = el.height(); 980 var new_h = el.height('auto').height(); 981 982 el.height(old_h).stop().animate( 983 {height: new_h}, 500, 'easeOutExpo', 984 function () { 985 if (typeof callback == 'function') { 986 callback.call(that); 987 } 988 } 989 ); 990 991 this.element.removeClass('collapsed'); 992 this.toggleTitleIcon(true); 993 994 this.expanded = true; 995 996 return this; 997 }, 998 999 /** 1000 * Hides the panel's contents--leaving only it's header 1001 */ 1002 collapse: function (duration, callback) { 1003 var that = this; 1004 this.element.addClass('collapsed'); 1005 this.content.stop().animate( 1006 {height: 5}, 250, 'easeOutExpo', 1007 function () { 1008 if (typeof callback == 'function') { 1009 callback.call(that); 1010 } 1011 } 1012 ); 1013 1014 this.toggleTitleIcon(false); 1015 1016 this.expanded = false; 1017 1018 return this; 1019 }, 1020 1021 /** 1022 * May also be called by the Sidebar to update title of panel 1023 * 1024 * @param html - Markup string, DOM object, or jQuery object 1025 */ 1026 setTitle: function (html) { 1027 this.title.find(nsSel('panel-title-text')).html(html); 1028 return this; 1029 }, 1030 1031 /** 1032 * May also be called by the Sidebar to update content of panel 1033 * 1034 * @param html - Markup string, DOM object, or jQuery object 1035 */ 1036 setContent: function (html) { 1037 // We do this so that empty panels don't appear collapsed 1038 if (!html || html == '') { 1039 html = ' '; 1040 } 1041 1042 this.content.find(nsSel('panel-content-inner-text')).html(html); 1043 return this; 1044 }, 1045 1046 rotateTitleIcon: function (angle, duration) { 1047 var arr = this.title.find(nsSel('panel-title-arrow')); 1048 arr.animate({angle: angle}, { 1049 duration : (typeof duration == 'number') ? duration : 500, 1050 easing : 'easeOutExpo', 1051 step : function (val, fx) { 1052 arr.css({ 1053 '-o-transform' : 'rotate(' + val + 'deg)', 1054 '-webkit-transform' : 'rotate(' + val + 'deg)', 1055 '-moz-transform' : 'rotate(' + val + 'deg)', 1056 '-ms-transform' : 'rotate(' + val + 'deg)' 1057 // filter : 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (angle / 90) + ')' 1058 }); 1059 } 1060 }); 1061 }, 1062 1063 /** 1064 * Walks up the ancestors chain for the given effective element, and 1065 * renders subpanels using the specified renderer function. 1066 * 1067 * @param {jQuery} effective - The effective element, whose lineage we 1068 * want to render 1069 * @param {Function} renderer - (Optional) function that will render 1070 * each element in the parental lineage 1071 * of the effective element 1072 */ 1073 renderEffectiveParents: function (effective, renderer) { 1074 var el = effective.first(); 1075 var content = []; 1076 var path = []; 1077 var activeOn = this.activeOn; 1078 var l; 1079 var pathRev; 1080 1081 while (el.length > 0 && !el.is('.aloha-editable')) { 1082 1083 if (activeOn(el)) { 1084 path.push('<span>' + el[0].tagName.toLowerCase() + '</span>'); 1085 l = path.length; 1086 pathRev = []; 1087 while (l--) { 1088 pathRev.push(path[l]); 1089 } 1090 content.push(supplant( 1091 '<div class="aloha-sidebar-panel-parent">' + 1092 '<div class="aloha-sidebar-panel-parent-path">{path}</div>' + 1093 '<div class="aloha-sidebar-panel-parent-content aloha-sidebar-opened">{content}</div>' + 1094 '</div>', 1095 { 1096 path : pathRev.join(''), 1097 content : (typeof renderer == 'function') ? renderer(el) : '----' 1098 } 1099 )); 1100 } 1101 1102 el = el.parent(); 1103 } 1104 1105 this.setContent(content.join('')); 1106 1107 jQuery('.aloha-sidebar-panel-parent-path').click(function () { 1108 var c = jQuery(this).parent().find('.aloha-sidebar-panel-parent-content'); 1109 1110 if (c.hasClass('aloha-sidebar-opened')) { 1111 c.hide().removeClass('aloha-sidebar-opened'); 1112 } else { 1113 c.show().addClass('aloha-sidebar-opened'); 1114 } 1115 }); 1116 1117 this.content.height('auto').find('.aloha-sidebar-panel-content-inner').height('auto'); 1118 } 1119 1120 }); 1121 1122 var left = new Sidebar({ 1123 position : 'left', 1124 width : 250 // TODO define in config 1125 }); 1126 1127 var right = new Sidebar({ 1128 position : 'right', 1129 width : 250 // TODO define in config 1130 }); 1131 1132 Aloha.Sidebar = { 1133 left : left, 1134 right : right 1135 }; 1136 1137 return Aloha.Sidebar; 1138 1139 }); 1140