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