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