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 var $container = this.container; 222 223 // add the event handler for context selection change 224 PubSub.sub('aloha.selection.context-change', function (message) { 225 that.checkActivePanels(message.range); 226 }); 227 228 229 $container.mousedown(function (e) { 230 e.originalEvent.stopSelectionUpdate = true; 231 Aloha.eventHandled = true; 232 //e.stopSelectionUpdate = true; 233 }); 234 235 $container.mouseup(function (e) { 236 e.originalEvent.stopSelectionUpdate = true; 237 Aloha.eventHandled = false; 238 }); 239 240 Aloha.bind('aloha-editable-deactivated', function (event, params) { 241 that.checkActivePanels(); 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 517 * Animation to rotate the sidebar arrow 518 * 519 * @param {number} angle The angle two which the arrow should rotate 520 * (0 or 180). 521 * @param {number|String} duration (Optional) How long the animation 522 * should play for. 523 */ 524 rotateHandleIcon: function (angle, duration) { 525 var arr = this.container.find('.aloha-sidebar-handle-icon'); 526 arr.animate({angle: angle}, { 527 duration : (typeof duration === 'number' || 528 typeof duration === 'string') ? duration : 500, 529 easing : 'easeOutExpo', 530 step : function (val, fx) { 531 arr.css({ 532 '-o-transform' : 'rotate(' + val + 'deg)', 533 '-webkit-transform' : 'rotate(' + val + 'deg)', 534 '-moz-transform' : 'rotate(' + val + 'deg)', 535 '-ms-transform' : 'rotate(' + val + 'deg)' 536 // We cannot use Microsoft Internet Explorer filters 537 // because Microsoft Internet Explore 8 does not support 538 // Microsoft Internet Explorer filters correctly. It 539 // breaks the layout 540 // filter : 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (angle / 90) + ')' 541 }); 542 } 543 }); 544 }, 545 546 /** 547 * Sets the handle icon to the "i am opened, click me to close the 548 * sidebar" state, or vice versa. The direction of the arrow depends 549 * on whether the sidebar is on the left or right, and whether it is 550 * in an opened state or not. 551 * 552 * @param {boolean} isOpen Whether or not the sidebar is in the opened 553 * state. 554 */ 555 toggleHandleIcon: function (isOpen) { 556 var isPointingLeft = (this.position === 'right') ^ isOpen; 557 558 if (this.settings.rotateIcons) { 559 this.rotateHandleIcon(isPointingLeft ? 180 : 0, 0); 560 } else { 561 var icon = this.container.find('.aloha-sidebar-handle-icon'); 562 563 if (isPointingLeft) { 564 icon.addClass('aloha-sidebar-handle-icon-left'); 565 } else { 566 icon.removeClass('aloha-sidebar-handle-icon-left'); 567 } 568 } 569 }, 570 571 /** 572 * Slides the sidebar into view 573 */ 574 open: function (duration, callback) { 575 if (this.isOpen) { 576 return this; 577 } 578 579 var isRight = (this.position === 'right'); 580 var anim = isRight ? {marginRight: 0} : {marginLeft: 0}; 581 582 this.toggleHandleIcon(true); 583 this.container.animate(anim, 584 (typeof duration === 'number' || typeof duration === 'string') 585 ? duration : 500, 586 'easeOutExpo'); 587 588 if (!this.settings.overlayPage) { 589 $('body').animate( 590 isRight ? {marginRight: '+=' + this.width} 591 : {marginLeft: '+=' + this.width}, 592 500, 'easeOutExpo'); 593 } 594 595 this.isOpen = true; 596 this.correctHeight(); 597 598 $('body').trigger('aloha-sidebar-opened', this); 599 600 return this; 601 }, 602 603 /** 604 * Slides that sidebar out of view. 605 */ 606 close: function (duration, callback) { 607 if (!this.isOpen) { 608 return this; 609 } 610 611 var isRight = (this.position === 'right'); 612 var anim = isRight ? {marginRight: -this.width} : {marginLeft: -this.width}; 613 614 this.toggleHandleIcon(false); 615 616 this.container.animate(anim, 617 (typeof duration === 'number' || typeof duration === 'string') 618 ? duration : 500, 619 'easeOutExpo'); 620 621 if (!this.settings.overlayPage) { 622 $('body').animate( 623 isRight ? {marginRight: '-=' + this.width} 624 : {marginLeft: '-=' + this.width}, 625 500, 'easeOutExpo'); 626 } 627 628 this.isOpen = false; 629 630 return this; 631 }, 632 633 /** 634 * Activates the given panel and passes to it the given element as the 635 * the effective that we want it to think activated it. 636 * 637 * @param {object|String} panel Panel instance or the id of a panel 638 * object. 639 * @param {jQuery} element Element to pass to the panel as effective 640 * element (the element that activated it). 641 */ 642 activatePanel: function (panel, element) { 643 if (typeof panel === 'string') { 644 panel = this.getPanelById(panel); 645 } 646 647 if (panel) { 648 panel.activate(element); 649 } 650 651 this.roundCorners(); 652 653 return this; 654 }, 655 656 /** 657 * Invokes the expand method for the given panel so that it expands its 658 * height to display its contents 659 * 660 * @param {object|String} panel Panel instance or the id of a panel 661 * object. 662 * @param {funtion} callback 663 */ 664 expandPanel: function (panel, callback) { 665 if (typeof panel === 'string') { 666 panel = this.getPanelById(panel); 667 } 668 669 if (panel) { 670 panel.expand(callback); 671 } 672 673 return this; 674 }, 675 676 /** 677 * Collapses the panel contents by invoking the given panel's collapse 678 * method. 679 * 680 * @param {object|String} panel Panel instance or the id of a panel 681 * object. 682 * @param {funtion} callback 683 */ 684 collapsePanel: function (panel, callback) { 685 if (typeof panel === 'string') { 686 panel = this.getPanelById(panel); 687 } 688 689 if (panel) { 690 panel.collapse(callback); 691 } 692 693 return this; 694 }, 695 696 /** 697 * Adds a panel to this sidebar instance. 698 * We try and build as much of the panel DOM as we can before inserting 699 * it into the DOM in order to reduce reflow. 700 * 701 * @param {object} panel - either a panel instance or an associative 702 * array containing settings for the construction 703 * of a new panel. 704 * @param {boolean} deferRounding - (Optional) If true, the rounding-off 705 * of the top most and bottom most panels 706 * will not be automatically done. Set 707 * this to true when adding a lot of panels 708 * at once. 709 * @return {object} The newly created panel. 710 */ 711 addPanel: function (panel, deferRounding) { 712 if (!(panel instanceof Panel)) { 713 if (!panel.width) { 714 panel.width = this.width; 715 } 716 panel.sidebar = this; 717 panel = new Panel(panel); 718 } 719 720 this.panels[panel.id] = panel; 721 this.container.find('.aloha-sidebar-panels').append(panel.element); 722 this.checkActivePanels(Selection.getRangeObject()); 723 return panel; 724 } 725 726 }); 727 728 // ------------------------------------------------------------------------ 729 // Panel prototype 730 // ------------------------------------------------------------------------ 731 $.extend(Panel.prototype, { 732 733 init: function (opts) { 734 this.setTitle(opts.title).setContent(opts.content); 735 736 delete opts.title; 737 delete opts.content; 738 739 $.extend(this, opts); 740 741 if (!this.id) { 742 this.id = 'aloha-sidebar-' + (++uid); 743 } 744 745 var li = this.element = 746 $('<li id="' + this.id + '">').append(this.title, this.content); 747 748 if (this.expanded) { 749 this.content.height('auto'); 750 } 751 752 this.toggleTitleIcon(this.expanded); 753 this.coerceActiveOn(); 754 755 // Disable text selection on title element. 756 this.title 757 .attr('unselectable', 'on') 758 .css('-moz-user-select', 'none') 759 .each(function () { this.onselectstart = function () { return false; }; }); 760 761 if (typeof this.onInit === 'function') { 762 this.onInit.apply(this); 763 764 } 765 }, 766 767 /** 768 * @param {boolean} isExpanded Whether or not the panel is in an 769 * expanded state. 770 */ 771 toggleTitleIcon: function (isExpanded) { 772 if (this.sidebar.settings.rotateIcons) { 773 this.rotateTitleIcon(isExpanded ? 90 : 0); 774 } else { 775 var icon = this.title.find('.aloha-sidebar-panel-title-arrow'); 776 777 if (isExpanded) { 778 icon.addClass('aloha-sidebar-panel-title-arrow-down'); 779 } else { 780 icon.removeClass('aloha-sidebar-panel-title-arrow-down'); 781 } 782 } 783 }, 784 785 /** 786 * Normalizes the activeOn property into a predicate function. 787 */ 788 coerceActiveOn: function () { 789 if (typeof this.activeOn !== 'function') { 790 var activeOn = this.activeOn; 791 792 this.activeOn = (function () { 793 var typeofActiveOn = typeof activeOn, 794 fn; 795 796 if (typeofActiveOn === 'boolean') { 797 fn = function () { 798 return activeOn; 799 }; 800 } else if (typeofActiveOn === 'undefined') { 801 fn = function () { 802 return true; 803 }; 804 } else if (typeofActiveOn === 'string') { 805 fn = function (el) { 806 return el ? el.is(activeOn) : false; 807 }; 808 } else { 809 fn = function () { 810 return false; 811 }; 812 } 813 814 return fn; 815 }()); 816 } 817 }, 818 819 /** 820 * Activates (displays) this panel. 821 */ 822 activate: function (effective) { 823 this.isActive = true; 824 this.content.parent('li').show().removeClass('aloha-sidebar-deactivated'); 825 this.effectiveElement = effective; 826 if (typeof this.onActivate === 'function') { 827 this.onActivate.call(this, effective); 828 } 829 }, 830 831 /** 832 * Hides this panel. 833 */ 834 deactivate: function () { 835 if (!this.isActive) { 836 return; 837 } 838 this.isActive = false; 839 this.content.parent('li').hide().addClass('aloha-sidebar-deactivated'); 840 this.effectiveElement = null; 841 }, 842 843 toggle: function () { 844 if (this.expanded) { 845 this.collapse(); 846 } else { 847 this.expand(); 848 } 849 }, 850 851 /** 852 * Displays the panel's contents. 853 */ 854 expand: function (callback) { 855 var that = this; 856 var el = this.content; 857 var old_h = el.height(); 858 var new_h = el.height('auto').height(); 859 el.height(old_h).stop().animate( 860 {height: new_h}, 500, 'easeOutExpo', 861 function () { 862 if (typeof callback === 'function') { 863 callback.call(that); 864 } 865 } 866 ); 867 this.element.removeClass('collapsed'); 868 this.toggleTitleIcon(true); 869 this.expanded = true; 870 return this; 871 }, 872 873 /** 874 * Hides the panel's contents--leaving only it's header. 875 */ 876 collapse: function (duration, callback) { 877 var that = this; 878 this.element.addClass('collapsed'); 879 this.content.stop().animate({height: 5}, 250, 'easeOutExpo', 880 function () { 881 if (typeof callback === 'function') { 882 callback.call(that); 883 } 884 }); 885 this.toggleTitleIcon(false); 886 this.expanded = false; 887 return this; 888 }, 889 890 /** 891 * May also be called by the Sidebar to update title of panel 892 * 893 * @param {string} html Markup string, DOM object, or jQuery object. 894 */ 895 setTitle: function (html) { 896 this.title.find('.aloha-sidebar-panel-title-text').html(html); 897 return this; 898 }, 899 900 /** 901 * May also be called by the Sidebar to update content of panel 902 * 903 * @param {string|jQuery.<HTMLElement>|HTMLElement} html Markup string, 904 * DOM object, or 905 * jQuery object. 906 */ 907 setContent: function (html) { 908 // We do this so that empty panels don't appear collapsed 909 if (!html || html === '') { 910 html = ' '; 911 } 912 913 this.content.find('.aloha-sidebar-panel-content-inner-text').html(html); 914 return this; 915 }, 916 917 rotateTitleIcon: function (angle, duration) { 918 var arr = this.title.find('.aloha-sidebar-panel-title-arrow'); 919 arr.animate({angle: angle}, { 920 duration : (typeof duration === 'number') ? duration : 500, 921 easing : 'easeOutExpo', 922 step : function (val, fx) { 923 arr.css({ 924 '-o-transform' : 'rotate(' + val + 'deg)', 925 '-webkit-transform' : 'rotate(' + val + 'deg)', 926 '-moz-transform' : 'rotate(' + val + 'deg)', 927 '-ms-transform' : 'rotate(' + val + 'deg)' 928 // filter : 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (angle / 90) + ')' 929 }); 930 } 931 }); 932 }, 933 934 /** 935 * Walks up the ancestors chain for the given effective element, and 936 * renders subpanels using the specified renderer function. 937 * 938 * @param {jQuery.<HTMLElement>} effective The effective element, whose 939 * lineage we want to render. 940 * @param {function} renderer (Optional) function that will render each 941 * element in the parental 942 * lineage of the effective 943 * element. 944 */ 945 renderEffectiveParents: function (effective, renderer) { 946 var el = effective.first(); 947 var content = []; 948 var path = []; 949 var activeOn = this.activeOn; 950 var l; 951 var pathRev; 952 953 while (el.length > 0 && !el.is('.aloha-editable')) { 954 if (activeOn(el)) { 955 path.push('<span>' + el[0].tagName.toLowerCase() + '</span>'); 956 l = path.length; 957 pathRev = []; 958 while (l--) { 959 pathRev.push(path[l]); 960 } 961 content.push('<div class="aloha-sidebar-panel-parent">' + 962 '<div class="aloha-sidebar-panel-parent-path">' + 963 pathRev.join('') + 964 '</div>' + 965 '<div class="aloha-sidebar-panel-parent-content' + 966 'aloha-sidebar-opened">' + ( 967 (typeof renderer === 'function') 968 ? renderer(el) 969 : '----' 970 ) + 971 '</div>' + 972 '</div>'); 973 } 974 el = el.parent(); 975 } 976 977 this.setContent(content.join('')); 978 979 $('.aloha-sidebar-panel-parent-path').click(function () { 980 var $content = $(this).parent().find( 981 '.aloha-sidebar-panel-parent-content'); 982 if ($content.hasClass('aloha-sidebar-opened')) { 983 $content.hide().removeClass('aloha-sidebar-opened'); 984 } else { 985 $content.show().addClass('aloha-sidebar-opened'); 986 } 987 }); 988 989 this.content.height('auto').find( 990 '.aloha-sidebar-panel-content-inner').height('auto'); 991 } 992 993 }); 994 995 var left = new Sidebar({ 996 position : 'left', 997 width : 250 // TODO define in config 998 }); 999 1000 var right = new Sidebar({ 1001 position : 'right', 1002 width : 250 // TODO define in config 1003 }); 1004 1005 Aloha.Sidebar = { 1006 left : left, 1007 right : right 1008 }; 1009 1010 return Aloha.Sidebar; 1011 }); 1012