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 	// ------------------------------------------------------------------------
195 	// Sidebar prototype
196 	// All properties to be shared across Sidebar instances can be placed in
197 	// the prototype object
198 	// ------------------------------------------------------------------------
199 	jQuery.extend(Sidebar.prototype, {
200 		
201 		// Build as much of the sidebar as we can before appending it to DOM to
202 		// minimize reflow.
203 		init: function (opts) {
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 267 			this.container.css( 'display', 'block' );
268 				//.animate({opacity: 1}, 1000);
269 			return this;
270 		},
271 		
272 		hide: function () {
273 			this.container.css( 'display','none' );
274 			//	.animate({opacity: 0}, 1000, function () {
275 			//		jQuery(this).css('display', 'block')
276 			//	});
277 			return this;
278 		},
279 		
280 		/**
281 		 * Determines the effective elements at the current selection.
282 		 * Iterates through all panels and checks whether the panel should be
283 		 * activated for any of the effective elements in the selection.
284 		 *
285 		 * @param {Object} range - The Aloha.RangeObject
286 		 */
287 		checkActivePanels: function( range ) {
288 			var effective = [];
289 			
290 			if ( typeof range != 'undefined' &&
291 					typeof range.markupEffectiveAtStart != 'undefined' ) {
292 				var l = range.markupEffectiveAtStart.length;
293 				for ( var i = 0; i < l; ++i ) {
294 					effective.push( jQuery( range.markupEffectiveAtStart[ i ] ) );
295 				}
296 			}
297 			
298 			var that = this;
299 			
300 			jQuery.each( this.panels, function () {
301 				that.showActivePanel( this, effective );
302 			} );
303 			
304 			this.correctHeight();
305 		},
306 		
307 		subscribeToEvents: function () {
308 			var that = this;
309 			var $container = this.container;
310 			
311 			Aloha.bind( 'aloha-selection-changed', function( event, range ) {
312 				that.checkActivePanels( range );
313 			} );
314 			
315 			$container.mousedown( function( e ) {
316 				e.originalEvent.stopSelectionUpdate = true;
317 				Aloha.eventHandled = true;
318 				//e.stopSelectionUpdate = true;
319 			} );
320 			
321 			$container.mouseup( function ( e ) {
322 				e.originalEvent.stopSelectionUpdate = true;
323 				Aloha.eventHandled = false;
324 			} );
325 			
326 			Aloha.bind( 'aloha-editable-deactivated', function ( event, params ) { 
327 				that.checkActivePanels();
328 			} );
329 		},
330 		
331 		/**
332 		 * Dynamically set appropriate heights for panels.
333 		 * The height for each panel is determined by the amount of space that
334 		 * is available in the viewport and the number of panels that need to
335 		 * share that space.
336 		 */
337 		correctHeight: function () {
338 			var height = this.container.find(nsSel('inner')).height() - (15 * 2);
339 			var panels = [];
340 			
341 			jQuery.each(this.panels, function () {
342 				if (this.isActive) {
343 					panels.push(this);
344 				}
345 			});
346 			
347 			if (panels.length == 0) {
348 				return;
349 			}
350 			
351 			var remainingHeight = height - ((panels[0].title.outerHeight() + 10) * panels.length);
352 			var panel;
353 			var targetHeight;
354 			var panelInner;
355 			var panelText;
356 			var undone;
357 			var toadd = 0;
358 			var math = Math; // Local reference for quicker lookup
359 			
360 			while (panels.length > 0 && remainingHeight > 0) {
361 				remainingHeight += toadd;
362 				
363 				toadd = 0;
364 				undone = [];
365 				
366 				for (var j = panels.length - 1; j >= 0; --j) {
367 					panel = panels[j];
368 					panelInner = panel.content.find(nsSel('panel-content-inner'));
369 					
370 					targetHeight = math.min(
371 						panelInner.height('auto').height(),
372 						math.floor(remainingHeight / (j + 1))
373 					);
374 					
375 					panelInner.height(targetHeight);
376 					
377 					remainingHeight -= targetHeight;
378 					
379 					panelText = panelInner.find(nsSel('panel-content-inner-text'));
380 					
381 382 					if (panelText.height() > targetHeight) {
383 						undone.push(panel);
384 						toadd += targetHeight;
385 						panelInner.css({
386 							'overflow-x': 'hidden',
387 							'overflow-y': 'scroll'
388 						});
389 					} else {
390 						panelInner.css('overflow-y', 'hidden');
391 					}
392 					
393 					if (panel.expanded) {
394 						panel.expand();
395 					}
396 				}
397 				
398 				panels = undone;
399 			}
400 		},
401 		
402 		/**
403 		 * Checks whether this panel should be activated (ie: made visible) for
404 		 * any of the elements specified in a given list of elements.
405 		 *
406 		 * We have to add a null object to the list of elements to allow us to
407 		 * check whether the panel should be visible when we have no effective
408 		 * elements in the current selection
409 		 *
410 		 * @param {Object} panel - The Panel object we will test
411 		 * @param {Array} elements - The effective elements (jQuery), any of
412 		 *							 which may activate the panel
413 		 */
414 		showActivePanel: function (panel, elements) {
415 			elements.push(null);
416 			
417 			var j = elements.length;
418 			var count = 0;
419 			var li = panel.content.parent('li');
420 			var activeOn = panel.activeOn;
421 			var effective = jQuery();
422 			
423 			for (var i = 0; i < j; ++i) {
424 				if (activeOn(elements[i])) {
425 					++count;
426 					if (elements[i]) {
427 						jQuery.merge(effective, elements[i]);
428 					}
429 				}
430 			}
431 			
432 			if (count) { 
433 				panel.activate(effective);
434 			} else {
435 				panel.deactivate();
436 			}
437 			
438 			this.roundCorners();
439 		},
440 		
441 		/**
442 		 * Sets up the functionality, event listeners, and animation of the
443 		 * sidebar handle
444 		 */
445 		initToggler: function () {
446 			var that = this;
447 			var bar = this.container;
448 			var icon = bar.find(nsSel('handle-icon'));
449 			var toggledClass = nsClass('toggled');
450 			var bounceTimer;
451 			var isRight = (this.position == 'right');
452 			
453 			if (this.opened) {
454 				this.rotateHandleArrow(isRight ? 0 : 180, 0);
455 			}
456 
457 			// configure the position of the sidebar handle
458 			jQuery( function () {
459 				if ( typeof Aloha.settings.sidebar != 'undefined' &&
460 						Aloha.settings.sidebar.handle &&
461 						Aloha.settings.sidebar.handle.top ) {
462 					jQuery(bar.find(nsSel('handle'))).get(0).style.top = Aloha.settings.sidebar.handle.top;
463 				}
464 			} );
465 
466 			bar.find(nsSel('handle'))
467 				.click(function () {
468 					if (bounceTimer) {
469 						clearInterval(bounceTimer);
470 					}
471 					
472 					icon.stop().css('marginLeft', 4);
473 					
474 					if (that.isOpen) {
475 						jQuery(this).removeClass(toggledClass);
476 						that.close();
477 						that.isOpen = false;
478 					} else {
479 						jQuery(this).addClass(toggledClass);
480 						that.open();
481 						that.isOpen = true;
482 					}
483 				}).hover(
484 					function () {
485 						var flag = that.isOpen ? -1 : 1;
486 						
487 						if (bounceTimer) {
488 							clearInterval(bounceTimer);
489 						}
490 						
491 						icon.stop();
492 						
493 						jQuery(this).stop().animate(
494 							isRight ? {marginLeft: '-=' + (flag * 5)} : {marginRight: '-=' + (flag * 5)},
495 							200
496 						);
497 						
498 						bounceTimer = setInterval(function () {
499 							flag *= -1;
500 							icon.animate(
501 								isRight ? {left: '-=' + (flag * 4)} : {right: '-=' + (flag * 4)},
502 								300
503 							);
504 						}, 300);
505 					},
506 					
507 					function () {
508 						if (bounceTimer) {
509 							clearInterval(bounceTimer);
510 						}
511 						
512 						icon.stop().css(isRight ? 'left' : 'right', 5);
513 						
514 						jQuery(this).stop().animate(
515 							isRight ? {marginLeft: 0} : {marginRight: 0},
516 							600, 'easeOutElastic'
517 						);
518 					}
519 				);
520 		},
521 		
522 		/**
523 		 * Rounds the top corners of the first visible panel, and the bottom
524 		 * corners of the last visible panel elements in the panels ul list
525 		 */
526 		roundCorners: function () {
527 			var bar = this.container;
528 			var lis = bar.find(nsSel('panels>li:not(', 'deactivated)'));
529 			var topClass = nsClass('panel-top');
530 			var bottomClass = nsClass('panel-bottom');
531 			
532 			bar.find(nsSel('panel-top,', 'panel-bottom'))
533 			   .removeClass(topClass)
534 			   .removeClass(bottomClass);
535 			
536 			lis.first().find(nsSel('panel-title')).addClass(topClass);
537 			lis.last().find(nsSel('panel-content')).addClass(bottomClass);
538 		},
539 		
540 		/**
541 		 * Updates the height of the inner div of the sidebar. This is done
542 		 * whenever the viewport is resized
543 		 */
544 		updateHeight: function () {
545 			var h = jQuery(window).height();
546 			this.container.height(h).find(nsSel('inner')).height(h);
547 		},
548 		
549 		/**
550 		 * Delegate all sidebar onclick events to the container.
551 		 * Then use handleBarclick method until we bubble up to the first
552 		 * significant element that we can interact with
553 		 */
554 		barClicked: function (ev) {
555 			this.handleBarclick(jQuery(ev.target));
556 		},
557 		
558 		/**
559 		 * We handle all click events on the sidebar from here--dispatching
560 		 * calls to which ever methods that should be invoked for the each
561 		 * interaction
562 		 */
563 		handleBarclick: function (el) {
564 			if (el.hasClass(nsClass('panel-title'))) {
565 				this.togglePanel(el);
566 			} else if (el.hasClass(nsClass('panel-content'))) {
567 				// Aloha.Log.log('Content clicked');
568 			} else if (el.hasClass(nsClass('handle'))) {
569 				// Aloha.Log.log('Handle clicked');
570 			} else if (el.hasClass(nsClass('bar'))) {
571 				// Aloha.Log.log('Sidebar clicked');
572 			} else {
573 				this.handleBarclick(el.parent());
574 			}
575 		},
576 		
577 		getPanelById: function (id) {
578 			return this.panels[id];
579 		},
580 		
581 		getPanelByElement: function (el) {
582 			var li = (el[0].tagName == 'LI') ? el : el.parent('li');
583 			return this.getPanelById(li[0].id);
584 		},
585 		
586 		togglePanel: function (el) {
587 			this.getPanelByElement(el).toggle();
588 		},
589 		
590 		/**
591 		 * Animation to rotate the sidebar arrow
592 		 *
593 		 * @param {Number} angle - The angle two which the arrow should rotate
594 		 *						   (0 or 180)
595 		 * @param {Number|String} duration - (Optional) How long the animation
596 		 *									 should play for
597 		 */
598 		rotateHandleIcon: function (angle, duration) {
599 			var arr = this.container.find(nsSel('handle-icon'));
600 			arr.animate({angle: angle}, {
601 				duration : (typeof duration == 'number' || typeof duration == 'string') ? duration : 500,
602 				easing   : 'easeOutExpo',
603 				step     : function (val, fx) {
604 					arr.css({
605 						'-o-transform'      : 'rotate(' + val + 'deg)',
606 						'-webkit-transform' : 'rotate(' + val + 'deg)',
607 						'-moz-transform'    : 'rotate(' + val + 'deg)',
608 						'-ms-transform'     : 'rotate(' + val + 'deg)'
609 					  // We cannot use Microsoft Internet Explorer filters
610 					  // because Microsoft Internet Explore 8 does not support
611 					  // Microsoft Internet Explorer filters correctly. It
612 					  // breaks the layout
613 					  // filter             : 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (angle / 90) + ')'
614 					});
615 				}
616 			});
617 		},
618 		
619 		/**
620 		 * Sets the handle icon to the "i am opened, click me to close the
621 		 * sidebar" state, or vice versa. The direction of the arrow depends
622 		 * on whether the sidebar is on the left or right, and whether it is
623 		 * in an opened state or not.
624 		 *
625 		 *	Question:
626 		 *		Given that the arrow icon is by default pointing right, should
627 		 *		we make it point left?
628 		 *
629 		 *	Answer:
630 		 *		isRight & isOpen   : no
631 		 *		isRight & isClosed : yes
632 		 *		isLeft  & isOpen   : yes
633 		 *		isLeft  & isClosed : no
634 		 *
635 		 *	Truth table:
636 		 *		 isRight | isOpen | XOR
637 		 *      ---------+--------+-----
638 		 *		 T       | T      | F
639 		 *		 T       | F      | T
640 		 *		 F       | T      | T
641 		 *		 F       | F      | F
642 		 *
643 		 * Therefore:
644 		 *		isPointingLeft = isRight XOR isOpen
645 		 *
646 		 * @param {Boolean} isOpened - Whether or not the sidebar is in the
647 		 *							   opened state
648 		 */
649 		toggleHandleIcon: function (isOpen) {
650 			var isPointingLeft = (this.position == 'right') ^ isOpen;
651 			
652 			if (this.settings.rotateIcons) {
653 				this.rotateHandleIcon(isPointingLeft ? 180 : 0, 0);
654 			} else {
655 				var icon = this.container.find(nsSel('handle-icon'));
656 				
657 				if (isPointingLeft) {
658 					icon.addClass(nsClass('handle-icon-left'));
659 				} else {
660 					icon.removeClass(nsClass('handle-icon-left'));
661 				}
662 			}
663 		},
664 		
665 		/**
666 		 * Slides the sidebar into view
667 		 */
668 		open: function (duration, callback) {
669 			if (this.isOpen) {
670 				return this;
671 			}
672 			
673 			var isRight = (this.position == 'right');
674 			var anim = isRight ? {marginRight: 0} : {marginLeft: 0};
675 			
676 			this.toggleHandleIcon(true);
677 			
678 			this.container.animate(
679 				anim,
680 				(typeof duration == 'number' || typeof duration == 'string')
681 					? duration : 500,
682 				'easeOutExpo'
683 			);
684 			
685 			if (!this.settings.overlayPage) {
686 				jQuery('body').animate(
687 					isRight ? {marginRight: '+=' + this.width} : {marginLeft: '+=' + this.width},
688 					500, 'easeOutExpo'
689 				);
690 			}
691 			
692 			this.isOpen = true;
693 
694 			jQuery('body').trigger(nsClass('opened'), this);
695 			
696 			return this;
697 		},
698 		
699 		/**
700 		 * Slides that sidebar out of view
701 		 */
702 		close: function (duration, callback) {
703 			if (!this.isOpen) {
704 				return this;
705 			}
706 
707 			var isRight = (this.position == 'right');
708 			var anim = isRight ? {marginRight: -this.width} : {marginLeft: -this.width};
709 			
710 			this.toggleHandleIcon(false);
711 			
712 			this.container.animate(
713 				anim,
714 				(typeof duration == 'number' || typeof duration == 'string')
715 					? duration : 500,
716 				'easeOutExpo'
717 			);
718 			
719 			if (!this.settings.overlayPage) {
720 				jQuery('body').animate(
721 					isRight ? {marginRight: '-=' + this.width} : {marginLeft: '-=' + this.width},
722 					500, 'easeOutExpo'
723 				);
724 			}
725 
726 			this.isOpen = false;
727 
728 			return this;
729 		},
730 		
731 		/**
732 		 * Activates the given panel and passes to it the given element as the
733 		 * the effective that we want it to think activated it.
734 		 *
735 		 * @param {Object|String} panel - Panel instance or the id of a panel
736 		 *								  object
737 		 * @param {jQuery} element - Element to pass to the panel as effective
738 		 *							 element (the element that activated it)
739 		 */
740 		activatePanel: function (panel, element) {
741 			if (typeof panel == 'string') {
742 				panel = this.getPanelById(panel);
743 			}
744 			
745 			if (panel){
746 				panel.activate(element);
747 			}
748 			
749 			this.roundCorners();
750 			
751 			return this;
752 		},
753 		
754 		/**
755 		 * Invokes the expand method for the given panel so that it expands its
756 		 * height to display its contents
757 		 *
758 		 * @param {Object|String} panel - Panel instance or the id of a panel
759 		 *								  object
760 		 * @param {Funtion} callback
761 		 */
762 		expandPanel: function (panel, callback) {
763 			if (typeof panel == 'string') {
764 				panel = this.getPanelById(panel);
765 			}
766 			
767 			if (panel){
768 				panel.expand(callback);
769 			}
770 			
771 			return this;
772 		},
773 		
774 		/**
775 		 * Collapses the panel contents by invoking the given panel's collapse
776 		 * method.
777 		 *
778 		 * @param {Object|String} panel - Panel instance or the id of a panel
779 		 *								  object
780 		 * @param {Funtion} callback
781 		 */
782 		collapsePanel: function (panel, callback) {
783 			if (typeof panel == 'string') {
784 				panel = this.getPanelById(panel);
785 			}
786 			
787 			if (panel){
788 				panel.collapse(callback);
789 			}
790 			
791 			return this;
792 		},
793 		
794 		/**
795 		 * Adds a panel to this sidebar instance.
796 		 * We try and build as much of the panel DOM as we can before inserting
797 		 * it into the DOM in order to reduce reflow.
798 		 *
799 		 * @param {Object} panel - either a panel instance or an associative
800 		 *			   array containing settings for the construction
801 		 *			   of a new panel.
802 		 * @param {Boolean} deferRounding - (Optional) If true, the rounding-off
803 		 *				    of the top most and bottom most panels
804 		 *				    will not be automatically done. Set
805 		 *				    this to true when adding a lot of panels
806 		 *				    at once.
807 		 * @return {Object} - The newly created panel.
808 		 */
809 		addPanel: function (panel, deferRounding) {
810 			if (!(panel instanceof Panel)) {
811 				if (!panel.width) {
812 					panel.width = this.width;
813 				}
814 				panel.sidebar = this;
815 				panel = new Panel(panel);
816 			}
817 			
818 			this.panels[panel.id] = panel;
819 			
820 			this.container.find(nsSel('panels')).append(panel.element);
821 			
822 			if (deferRounding !== true) {
823 				this.roundCorners();
824 			}
825 			this.checkActivePanels(Selection.getRangeObject());
826 			return panel;
827 		}
828 		
829 	});
830 	
831 	// ------------------------------------------------------------------------
832 	// Panel constructor
833 	// ------------------------------------------------------------------------
834 	var Panel = function Panel (opts) {
835 		this.id		  = null;
836 		this.folds	  = {};
837 		this.button	  = null;
838 		this.title	  = jQuery(renderTemplate('						 \
839 			<div class="{panel-title}">							 \
840 				<span class="{panel-title-arrow}"></span>		 \
841 				<span class="{panel-title-text}">Untitled</span> \
842 			</div>												 \
843 		'));
844 		this.content  = jQuery(renderTemplate('					\
845 			<div class="{panel-content}">					\
846 				<div class="{panel-content-inner}">			\
847 					<div class="{panel-content-inner-text}">\
848 					</div>									\
849 				</div>										\
850 			</div>											\
851 		'));
852 		this.element  = null;
853 		this.expanded = false;
854 		this.effectiveElement = null;
855 		this.isActive = true;
856 		
857 		this.init(opts);
858 	};
859 	
860 	// ------------------------------------------------------------------------
861 	// Panel prototype
862 	// ------------------------------------------------------------------------
863 	jQuery.extend(Panel.prototype, {
864 		
865 		init: function (opts) {
866 			this.setTitle(opts.title)
867 				.setContent(opts.content);
868 			
869 			delete opts.title;
870 			delete opts.content;
871 			
872 			jQuery.extend(this, opts);
873 			
874 			if (!this.id) {
875 				this.id = nsClass(++uid);
876 			}
877 			
878 			var li = this.element =
879 				jQuery('<li id="' +this.id + '">')
880 					.append(this.title, this.content);
881 			
882 			if (this.expanded){
883 				this.content.height('auto');
884 			}
885 			
886 			this.toggleTitleIcon(this.expanded);
887 			
888 			this.coerceActiveOn();
889 			
890 			// Disable text selection on title element
891 			this.title
892 				.attr('unselectable', 'on')
893 				.css('-moz-user-select', 'none')
894 				.each(function() {this.onselectstart = function() {return false;};});
895 			
896 			if (typeof this.onInit == 'function') {
897 				this.onInit.apply(this);
898 			}
899 		},
900 		
901 		/**
902 		 * @param {Boolean} isExpanded - Whether or not the panel is in an
903 		 *								 expanded state
904 		 */
905 		toggleTitleIcon: function (isExpanded) {
906 			if (this.sidebar.settings.rotateIcons) {
907 				this.rotateTitleIcon(isExpanded ? 90 : 0);
908 			} else {
909 				var icon = this.title.find(nsSel('panel-title-arrow'));
910 				
911 				if (isExpanded) {
912 					icon.addClass(nsClass('panel-title-arrow-down'));
913 				} else {
914 					icon.removeClass(nsClass('panel-title-arrow-down'));
915 				}
916 			}
917 		},
918 		
919 		/**
920 		 * Normalizes the activeOn property into a predicate function
921 		 */
922 		coerceActiveOn: function () {
923 			if (typeof this.activeOn != 'function') {
924 				var activeOn = this.activeOn;
925 				
926 				this.activeOn = (function () {
927 					var typeofActiveOn = typeof activeOn,
928 						fn;
929 					
930 					if (typeofActiveOn == 'boolean') {
931 						fn = function () {
932 							return activeOn;
933 						};
934 					} else if (typeofActiveOn == 'undefined') {
935 						fn = function () {
936 							return true;
937 						};
938 					} else if (typeofActiveOn == 'string') {
939 						fn = function (el) {
940 							return el ? el.is(activeOn) : false;
941 						};
942 					} else {
943 						fn = function () {
944 							return false;
945 						};
946 					}
947 					
948 					return fn;
949 				})();
950 			}
951 		},
952 		
953 		/**
954 		 * Activates (displays) this panel
955 		 */
956 		activate: function (effective) {
957 			this.isActive = true;
958 			this.content.parent('li').show().removeClass(nsClass('deactivated'));
959 			this.effectiveElement = effective;
960 			if (typeof this.onActivate == 'function') {
961 				this.onActivate.call(this, effective);
962 			}
963 		},
964 		
965 		/**
966 		 * Hides this panel
967 		 */
968 		deactivate: function () {
969 			this.isActive = false;
970 			this.content.parent('li').hide().addClass(nsClass('deactivated'));
971 			this.effectiveElement = null;
972 		},
973 		
974 		toggle: function () {
975 			if (this.expanded) {
976 				this.collapse();
977 			} else {
978 				this.expand();
979 			}
980 		},
981 		
982 		/**
983 		 * Displays the panel's contents
984 		 */
985 		expand: function (callback) {
986 			var that = this;
987 			var el = this.content;
988 			var old_h = el.height();
989 			var new_h = el.height('auto').height();
990 			
991 			el.height(old_h).stop().animate(
992 				{height: new_h}, 500, 'easeOutExpo',
993 				function () {
994 					if (typeof callback == 'function') {
995 						callback.call(that);
996 					}
997 				}
998 			);
999 			
1000 			this.element.removeClass('collapsed');
1001 			this.toggleTitleIcon(true);
1002 			
1003 			this.expanded = true;
1004 			
1005 			return this;
1006 		},
1007 		
1008 		/**
1009 		 * Hides the panel's contents--leaving only it's header
1010 		 */
1011 		collapse: function (duration, callback) {
1012 			var that = this;
1013 			this.element.addClass('collapsed');
1014 			this.content.stop().animate(
1015 				{height: 5}, 250, 'easeOutExpo',
1016 				function () {
1017 					if (typeof callback == 'function') {
1018 						callback.call(that);
1019 					}
1020 				}
1021 			);
1022 			
1023 			this.toggleTitleIcon(false);
1024 			
1025 			this.expanded = false;
1026 			
1027 			return this;
1028 		},
1029 		
1030 		/**
1031 		 * May also be called by the Sidebar to update title of panel
1032 		 *
1033 		 * @param html - Markup string, DOM object, or jQuery object
1034 		 */
1035 		setTitle: function (html) {
1036 			this.title.find(nsSel('panel-title-text')).html(html);
1037 			return this;
1038 		},
1039 		
1040 		/**
1041 		 * May also be called by the Sidebar to update content of panel
1042 		 *
1043 		 * @param html - Markup string, DOM object, or jQuery object
1044 		 */
1045 		setContent: function (html) {
1046 			// We do this so that empty panels don't appear collapsed
1047 			if (!html || html == '') {
1048 				html = ' ';
1049 			}
1050 			
1051 			this.content.find(nsSel('panel-content-inner-text')).html(html);
1052 			return this;
1053 		},
1054 		
1055 		rotateTitleIcon: function (angle, duration) {
1056 			var arr = this.title.find(nsSel('panel-title-arrow'));
1057 			arr.animate({angle: angle}, {
1058 				duration : (typeof duration == 'number') ? duration : 500,
1059 				easing   : 'easeOutExpo',
1060 				step     : function (val, fx) {
1061 					arr.css({
1062 						'-o-transform'      : 'rotate(' + val + 'deg)',
1063 						'-webkit-transform' : 'rotate(' + val + 'deg)',
1064 						'-moz-transform'    : 'rotate(' + val + 'deg)',
1065 						'-ms-transform'     : 'rotate(' + val + 'deg)'
1066 					 // filter              : 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (angle / 90) + ')'
1067 					});
1068 				}
1069 			});
1070 		},
1071 		
1072 		/**
1073 		 * Walks up the ancestors chain for the given effective element, and
1074 		 * renders subpanels using the specified renderer function.
1075 		 *
1076 		 * @param {jQuery} effective - The effective element, whose lineage we
1077 		 *							   want to render
1078 		 * @param {Function} renderer - (Optional) function that will render
1079 		 *								 each element in the parental lineage
1080 		 *								 of the effective element
1081 		 */
1082 		renderEffectiveParents: function (effective, renderer) {
1083 			var el = effective.first();
1084 			var content = [];
1085 			var path = [];
1086 			var activeOn = this.activeOn;
1087 			var l;
1088 			var pathRev;
1089 			
1090 			while (el.length > 0 && !el.is('.aloha-editable')) {
1091 				
1092 				if (activeOn(el)) {
1093 					path.push('<span>' + el[0].tagName.toLowerCase() + '</span>');
1094 					l = path.length;
1095 					pathRev = [];
1096 					while (l--) {
1097 1098 						pathRev.push(path[l]);
1099 					}
1100 					content.push(supplant(
1101 						'<div class="aloha-sidebar-panel-parent">' +
1102 							'<div class="aloha-sidebar-panel-parent-path">{path}</div>' +
1103 							'<div class="aloha-sidebar-panel-parent-content aloha-sidebar-opened">{content}</div>' +
1104 						 '</div>',
1105 						{
1106 							path	: pathRev.join(''),
1107 							content	: (typeof renderer == 'function') ? renderer(el) : '----'
1108 						}
1109 					));
1110 				}
1111 				
1112 				el = el.parent();
1113 			}
1114 			
1115 			this.setContent(content.join(''));
1116 			
1117 			jQuery('.aloha-sidebar-panel-parent-path').click(function () {
1118 				var c = jQuery(this).parent().find('.aloha-sidebar-panel-parent-content');
1119 				
1120 				if (c.hasClass('aloha-sidebar-opened')) {
1121 					c.hide().removeClass('aloha-sidebar-opened');
1122 				} else {
1123 					c.show().addClass('aloha-sidebar-opened');
1124 				}
1125 			});
1126 			
1127 			this.content.height('auto').find('.aloha-sidebar-panel-content-inner').height('auto');
1128 		}
1129 		
1130 	});
1131 	
1132 	var left = new Sidebar({
1133 		position : 'left',
1134 		width	 : 250 // TODO define in config
1135 	});
1136 	
1137 	var right = new Sidebar({
1138 		position : 'right',
1139 		width	 : 250 // TODO define in config
1140 	});
1141 	
1142 	Aloha.Sidebar = {
1143 		left  : left,
1144 		right : right
1145 	};
1146 	
1147 	return Aloha.Sidebar;
1148 	
1149 });
1150