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