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