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