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