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