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