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