1 /*!
  2  * This file is part of Aloha Editor Project http://aloha-editor.org
  3  * Copyright © 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 define( [
 22 	'aloha/core',
 23 	'util/class',
 24 	'aloha/jquery',
 25 	'aloha/pluginmanager',
 26 	'aloha/floatingmenu',
 27 	'aloha/selection',
 28 	'aloha/markup',
 29 	'aloha/contenthandlermanager',
 30 	'aloha/console'
 31 ], function( Aloha, Class, jQuery, PluginManager, FloatingMenu, Selection,
 32 	         Markup, ContentHandlerManager, console ) {
 33 	'use strict';
 34 
 35 	var unescape = window.unescape,
 36 	    GENTICS = window.GENTICS,
 37 
 38 	    // True, if the next editable activate event should not be handled
 39 	    ignoreNextActivateEvent = false;
 40 
 41 	/**
 42 	 * A cache to hold information derived, and used in getContents().
 43 	 * @type {object<string,(string|jQuery.<HTMLElement>)>}
 44 	 * @private
 45 	 */
 46 	var editableContentCache = {};
 47 
 48 	// default supported and custom content handler settings
 49 	// @TODO move to new config when implemented in Aloha
 50 	Aloha.defaults.contentHandler = {};
 51 	Aloha.defaults.contentHandler.initEditable = ['blockelement', 'sanitize'];
 52 	Aloha.defaults.contentHandler.getContents = ['blockelement', 'basic'];
 53 
 54 	// The insertHtml contenthandler ( paste ) will, by default, use all
 55 	// registered content handlers.
 56 	//Aloha.defaults.contentHandler.insertHtml = void 0;
 57 
 58 	if ( typeof Aloha.settings.contentHandler === 'undefined' ) {
 59 		Aloha.settings.contentHandler = {};
 60 	}
 61 
 62 	var defaultContentSerializer = function(editableElement){
 63 		return jQuery(editableElement).html();
 64 	};
 65 
 66 	var contentSerializer = defaultContentSerializer;
 67 
 68 	var BasicContentHandler = ContentHandlerManager.createHandler({
 69 
 70 		/**
 71 		 * @param {string} content Content to process.
 72 		 * @return {string} Processed content.
 73 		 */
 74 		handleContent: function (content) {
 75 			// Remove the contenteditable attribute from the final html in IE8
 76 			// We need to do this this way because removeAttr is not working 
 77 			// in IE8 in IE8-compatibilitymode for those attributes.
 78 			if (jQuery.browser.msie && jQuery.browser.version < 8) {
 79 				content = content.replace(/(<table\s+[^>]*?)contenteditable=['\"\w]+/gi, "$1");
 80 			}
 81 			
 82 			content = this.stringFizzleSizzle(content);
 83 
 84 			return content;
 85 		},
 86 		
 87 		/**
 88 		 * Removes nodeIndex, sizcache and sizset attributes.
 89 		 * @param {string} content to process.
 90 		 */
 91 		stringFizzleSizzle: function (content) {
 92 			var replaced = content;
 93 			while (content !== (replaced = content.replace(/(<[^>]*?)(nodeIndex|sizcache|sizset)[\w\d]*="[^"]*"/gi, '$1'))) {
 94 				content = replaced;
 95 			}
 96 			return content;
 97 		}
 98 
 99 	});
100 
101 	// Register the basic contenthandler
102 	ContentHandlerManager.register('basic', BasicContentHandler);
103 
104 	/**
105 	 * Cleans the given content by manipulating the jquery content object. 
106 	 * @param {Object} $content jQuery object that represents the content
107 	 */ 
108 	function makeClean($content) {
109 		if (jQuery.browser.msie && jQuery.browser.version < 8) {
110 			$content = jQuery($content);
111 			
112 			$content.find('[hidefocus]').each(function () {
113 				jQuery(this).removeAttr('hidefocus');
114 			});
115 			
116 			$content.find('[hideFocus]').each(function () {
117 				jQuery(this).removeAttr('hideFocus');
118 			});
119 			
120 			$content.find('[tabindex]').each(function () {
121 				jQuery(this).removeAttr('tabindex');
122 			});
123 			
124 			$content.find('[tabIndex]').each(function () {
125 				jQuery(this).removeAttr('tabIndex');
126 			});
127 		}
128 	}
129 
130 	
131 	/**
132 	 * Editable object
133 	 * @namespace Aloha
134 	 * @class Editable
135 	 * @method
136 	 * @constructor
137 	 * @param {Object} obj jQuery object reference to the object
138 	 */
139 	Aloha.Editable = Class.extend( {
140 
141 		_constructor: function( obj ) {
142 			// check wheter the object has an ID otherwise generate and set
143 			// globally unique ID
144 			if ( !obj.attr( 'id' ) ) {
145 				obj.attr( 'id', GENTICS.Utils.guid() );
146 			}
147 
148 			// store object reference
149 			this.obj = obj;
150 			this.originalObj = obj;
151 			this.ready = false;
152 
153 			// delimiters, timer and idle for smartContentChange
154 			// smartContentChange triggers -- tab: '\u0009' - space: '\u0020' - enter: 'Enter'
155 			// backspace: U+0008 - delete: U+007F
156 			this.sccDelimiters = [ ':', ';', '.', '!', '?', ',',
157 				unescape( '%u0009' ), unescape( '%u0020' ), unescape( '%u0008' ), unescape( '%u007F' ), 'Enter' ];
158 			this.sccIdle = 5000;
159 			this.sccDelay = 500;
160 			this.sccTimerIdle = false;
161 			this.sccTimerDelay = false;
162 
163 			// see keyset http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html
164 			this.keyCodeMap = {
165 				 93 : 'Apps',         // The Application key
166 				 18 : 'Alt',          // The Alt ( Menu ) key.
167 				 20 : 'CapsLock',     // The Caps Lock ( Capital ) key.
168 				 17 : 'Control',      // The Control ( Ctrl ) key.
169 				 40 : 'Down',         // The Down Arrow key.
170 				 35 : 'End',          // The End key.
171 				 13 : 'Enter',        // The Enter key.
172 				112 : 'F1',           // The F1 key.
173 				113 : 'F2',           // The F2 key.
174 				114 : 'F3',           // The F3 key.
175 				115 : 'F4',           // The F4 key.
176 				116 : 'F5',           // The F5 key.
177 				117 : 'F6',           // The F6 key.
178 				118 : 'F7',           // The F7 key.
179 				119 : 'F8',           // The F8 key.
180 				120 : 'F9',           // The F9 key.
181 				121 : 'F10',          // The F10 key.
182 				122 : 'F11',          // The F11 key.
183 				123 : 'F12',          // The F12 key.
184 
185 				// Anybody knows the keycode for F13-F24?
186 				 36 : 'Home',         // The Home key.
187 				 45 : 'Insert',       // The Insert ( Ins ) key.
188 				 37 : 'Left',         // The Left Arrow key.
189 				224 : 'Meta',         // The Meta key.
190 				 34 : 'PageDown',     // The Page Down ( Next ) key.
191 				 33 : 'PageUp',       // The Page Up key.
192 				 19 : 'Pause',        // The Pause key.
193 				 44 : 'PrintScreen',  // The Print Screen ( PrintScrn, SnapShot ) key.
194 				 39 : 'Right',        // The Right Arrow key.
195 				145 : 'Scroll',       // The scroll lock key
196 				 16 : 'Shift',        // The Shift key.
197 				 38 : 'Up',           // The Up Arrow key.
198 				 91 : 'Win',          // The left Windows Logo key.
199 				 92 : 'Win'           // The right Windows Logo key.
200 			};
201 
202 			this.placeholderClass = 'aloha-placeholder';
203 
204 			Aloha.registerEditable( this );
205 
206 			this.init();
207 		},
208 
209 		/**
210 		 * Initialize the editable
211 		 * @return void
212 		 * @hide
213 		 */
214 		init: function() {
215 			var me = this;
216 
217 			// TODO make editables their own settings.
218 			this.settings = Aloha.settings;
219 
220 			// smartContentChange settings
221 			// @TODO move to new config when implemented in Aloha
222 			if ( Aloha.settings && Aloha.settings.smartContentChange ) {
223 				if ( Aloha.settings.smartContentChange.delimiters ) {
224 					this.sccDelimiters = Aloha.settings.smartContentChange.delimiters;
225 				}
226 
227 				if ( Aloha.settings.smartContentChange.idle ) {
228 					this.sccIdle = Aloha.settings.smartContentChange.idle;
229 				}
230 
231 				if ( Aloha.settings.smartContentChange.delay ) {
232 					this.sccDelay = Aloha.settings.smartContentChange.delay;
233 				}
234 			}
235 
236 			// check if Aloha can handle the obj as Editable
237 			if ( !this.check( this.obj ) ) {
238 				//Aloha.log( 'warn', this, 'Aloha cannot handle {' + this.obj[0].nodeName + '}' );
239 				this.destroy();
240 				return;
241 			}
242 
243 			// apply content handler to clean up content
244 			if ( typeof Aloha.settings.contentHandler.getContents === 'undefined' ) {
245 				Aloha.settings.contentHandler.getContents = Aloha.defaults.contentHandler.getContents;
246 			}
247 
248 			// apply content handler to clean up content
249 			if ( typeof Aloha.settings.contentHandler.initEditable === 'undefined' ) {
250 				Aloha.settings.contentHandler.initEditable = Aloha.defaults.contentHandler.initEditable;
251 			}
252 			
253 			var content = me.obj.html();
254 			content = ContentHandlerManager.handleContent( content, {
255 				contenthandler: Aloha.settings.contentHandler.initEditable,
256 				command: 'initEditable'
257 			} );
258 			me.obj.html( content );
259 
260 			// only initialize the editable when Aloha is fully ready (including plugins)
261 			Aloha.bind( 'aloha-ready', function() {
262 				// initialize the object
263 				me.obj.addClass( 'aloha-editable' ).contentEditable( true );
264 
265 				// add focus event to the object to activate
266 				me.obj.mousedown( function( e ) {
267 					// check whether the mousedown was already handled
268 					if ( !Aloha.eventHandled ) {
269 						Aloha.eventHandled = true;
270 						return me.activate( e );
271 					}
272 				} );
273 
274 				me.obj.mouseup( function( e ) {
275 					Aloha.eventHandled = false;
276 				} );
277 
278 				me.obj.focus( function( e ) {
279 					return me.activate( e );
280 				} );
281 
282 				// by catching the keydown we can prevent the browser from doing its own thing
283 				// if it does not handle the keyStroke it returns true and therefore all other
284 				// events (incl. browser's) continue
285 				//me.obj.keydown( function( event ) {
286 				//me.obj.add('.aloha-block', me.obj).live('keydown', function (event) { // live not working but would be usefull
287 				me.obj.add('.aloha-block', me.obj).keydown(function (event) {
288 					var letEventPass = Markup.preProcessKeyStrokes( event );
289 					me.keyCode = event.which;
290 
291 					if (!letEventPass) {
292 						// the event will not proceed to key press, therefore trigger smartContentChange
293 						me.smartContentChange( event );
294 					}
295 					return letEventPass;
296 				} );
297 
298 				// handle keypress
299 				me.obj.keypress( function( event ) {
300 					// triggers a smartContentChange to get the right charcode
301 					// To test try http://www.w3.org/2002/09/tests/keys.html
302 303 					Aloha.activeEditable.smartContentChange( event );
304 				} );
305 
306 				// handle shortcut keys
307 				me.obj.keyup( function( event ) {
308 					if ( event.keyCode === 27 ) {
309 						Aloha.deactivateEditable();
310 						return false;
311 					}
312 				} );
313 
314 				// register the onSelectionChange Event with the Editable field
315 				me.obj.contentEditableSelectionChange( function( event ) {
316 					Selection.onChange( me.obj, event );
317 					return me.obj;
318 				} );
319 
320 				// mark the editable as unmodified
321 				me.setUnmodified();
322 
323 				// we don't do the sanitizing on aloha ready, since some plugins add elements into the content and bind events to it.
324 				// if we sanitize by replacing the html, all events would get lost. TODO: think about a better solution for the sanitizing, without
325 				// destroying the events
326 //				// apply content handler to clean up content
327 //				var content = me.obj.html();
328 //				if ( typeof Aloha.settings.contentHandler.initEditable === 'undefined' ) {
329 //					Aloha.settings.contentHandler.initEditable = Aloha.defaults.contentHandler.initEditable;
330 //				}
331 //				content = ContentHandlerManager.handleContent( content, {
332 //					contenthandler: Aloha.settings.contentHandler.initEditable
333 //				} );
334 //				me.obj.html( content );
335 
336 				me.snapshotContent = me.getContents();
337 
338 				// FF bug: check for empty editable contents ( no <br>; no whitespace )
339 				if ( jQuery.browser.mozilla ) {
340 					me.initEmptyEditable();
341 				}
342 
343 				me.initPlaceholder();
344 
345 				me.ready = true;
346 
347 				// throw a new event when the editable has been created
348 				/**
349 				 * @event editableCreated fires after a new editable has been created, eg. via $( '#editme' ).aloha()
350 				 * The event is triggered in Aloha's global scope Aloha
351 				 * @param {Event} e the event object
352 				 * @param {Array} a an array which contains a reference to the currently created editable on its first position
353 				 */
354 				Aloha.trigger( 'aloha-editable-created', [ me ] );
355 			} );
356 		},
357 
358 		/**
359 		 * True, if this editable is active for editing
360 		 * @property
361 		 * @type boolean
362 		 */
363 		isActive: false,
364 
365 		/**
366 		 * stores the original content to determine if it has been modified
367 		 * @hide
368 		 */
369 		originalContent: null,
370 
371 		/**
372 		 * every time a selection is made in the current editable the selection has to
373 		 * be saved for further use
374 		 * @hide
375 		 */
376 		range: undefined,
377 
378 		/**
379 		 * Check if object can be edited by Aloha Editor
380 		 * @return {boolean } editable true if Aloha Editor can handle else false
381 		 * @hide
382 		 */
383 		check: function() {
384 			/* TODO check those elements
385 			'map', 'meter', 'object', 'output', 'progress', 'samp',
386 			'time', 'area', 'datalist', 'figure', 'kbd', 'keygen',
387 			'mark', 'math', 'wbr', 'area',
388 			*/
389 
390 			// Extract El
391 			var	me = this,
392 			    obj = this.obj,
393 			    el = obj.get( 0 ),
394 			    nodeName = el.nodeName.toLowerCase(),
395 
396 				// supported elements
397 			    textElements = [ 'a', 'abbr', 'address', 'article', 'aside',
398 						'b', 'bdo', 'blockquote',  'cite', 'code', 'command',
399 						'del', 'details', 'dfn', 'div', 'dl', 'em', 'footer',
400 						'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'i',
401 						'ins', 'menu', 'nav', 'p', 'pre', 'q', 'ruby',
402 						'section', 'small', 'span', 'strong', 'sub', 'sup',
403 						'var' ],
404 			    i, div;
405 
406 			for ( i = 0; i < textElements.length; ++i ) {
407 				if ( nodeName === textElements[ i ] ) {
408 					return true;
409 				}
410 			}
411 
412 			// special handled elements
413 			switch ( nodeName ) {
414 				case 'label':
415 				case 'button':
416 					// TODO need some special handling.
417 					break;
418 				case 'textarea':
419 					// Create a div alongside the textarea
420 					div = jQuery( '<div id="' + this.getId() +
421 							'-aloha" class="aloha-textarea" />' )
422 								.insertAfter( obj );
423 
424 					// Resize the div to the textarea and
425 					// Populate the div with the value of the textarea
426 					// Then, hide the textarea
427 					div.height( obj.height() )
428 					   .width( obj.width() )
429 					   .html( obj.val() );
430 
431 					obj.hide();
432 
433 					// Attach a onsubmit to the form to place the HTML of the
434 					// div back into the textarea
435 					obj.parents( 'form:first' ).submit( function() {
436 						obj.val( me.getContents() );
437 					} );
438 
439 					// Swap textarea reference with the new div
440 					this.obj = div;
441 
442 					// Supported
443 					return true;
444 				default:
445 					break;
446 			}
447 
448 			// the following elements are not supported
449 			/*
450 			'canvas', 'audio', 'br', 'embed', 'fieldset', 'hgroup', 'hr',
451 			'iframe', 'img', 'input', 'map', 'script', 'select', 'style',
452 			'svg', 'table', 'ul', 'video', 'ol', 'form', 'noscript',
453 			 */
454 			return false;
455 		},
456 
457 		/**
458 		 * Init Placeholder
459 		 *
460 		 * @return void
461 		 */
462 		initPlaceholder: function() {
463 			if ( Aloha.settings.placeholder && this.isEmpty() ) {
464 				this.addPlaceholder();
465 			}
466 		},
467 
468 		/**
469 		 * Check if the conteneditable is empty.
470 		 *
471 		 * @return {Boolean}
472 		 */
473 		isEmpty: function() {
474 			var editableTrimedContent = jQuery.trim( this.getContents() ),
475 				onlyBrTag = ( editableTrimedContent === '<br>' ) ? true : false;
476 			return ( editableTrimedContent.length === 0 || onlyBrTag );
477 		},
478 
479 		/**
480 		 * Check if the editable div is not empty. Fixes a FF browser bug
481 		 * see issue: https://github.com/alohaeditor/Aloha-Editor/issues/269
482 		 *
483 		 * @return {undefined}
484 		 */
485 		initEmptyEditable: function( ) {
486 			var obj = this.obj;
487 			if ( this.empty( this.getContents() ) ) {
488 				jQuery( obj ).prepend( '<br class="aloha-cleanme" />' );
489 			}
490 		},
491 
492 		/**
493 		 * Add placeholder in editable
494 		 *
495 		 * @return void
496 		 */
497 		addPlaceholder: function() {
498 			var div = jQuery( '<div>' ),
499 			    span = jQuery( '<span>' ),
500 			    el,
501 			    obj = this.obj;
502 
503 			if ( GENTICS.Utils.Dom.allowsNesting( obj[0], div[0] ) ) {
504 				el = div;
505 			} else {
506 				el = span;
507 			}
508 
509 			jQuery( obj ).append( el.addClass( this.placeholderClass ) );
510 			jQuery.each(
511 				Aloha.settings.placeholder,
512 				function( selector, selectorConfig ) {
513 					if ( obj.is( selector ) ) {
514 						el.html( selectorConfig );
515 					}
516 				}
517 			);
518 
519 			// remove browser br
520 			jQuery( 'br', obj ).remove();
521 
522 			// delete div, span, el;
523 		},
524 
525 		/**
526 		 * remove placeholder from contenteditable. If setCursor is true,
527 		 * will also set the cursor to the start of the selection. However,
528 		 * this will be ASYNCHRONOUS, so if you rely on the fact that
529 		 * the placeholder is removed after calling this method, setCursor
530 		 * should be false ( or not set )
531 		 *
532 		 * @return void
533 		 */
534 		removePlaceholder: function( obj, setCursor ) {
535 			var placeholderClass = this.placeholderClass,
536 			    range;
537 
538 			// remove browser br
539 			// jQuery( 'br', obj ).remove();
540 
541 			// set the cursor // remove placeholder
542 			if ( setCursor === true ) {
543 				range = Selection.getRangeObject();
544 				if ( !range.select ) {
545 					return;
546 				}
547 				range.startContainer = range.endContainer = obj.get( 0 );
548 				range.startOffset = range.endOffset = 0;
549 				range.select();
550 
551 				window.setTimeout( function() {
552 					jQuery( '.' + placeholderClass, obj ).remove();
553 				}, 20 );
554 
555 			} else {
556 				jQuery( '.' + placeholderClass, obj ).remove();
557 			}
558 		},
559 
560 		/**
561 		 * destroy the editable
562 		 * @return void
563 		 */
564 		destroy: function() {
565 			// leave the element just to get sure
566 			if ( this === Aloha.getActiveEditable() ) {
567 				this.blur();
568 
569 				// also hide the floating menu if the current editable was active
570 				FloatingMenu.hide();
571 			}
572 
573 			// special handled elements
574 			switch ( this.originalObj.get( 0 ).nodeName.toLowerCase() ) {
575 				case 'label':
576 				case 'button':
577 					// TODO need some special handling.
578 					break;
579 				case 'textarea':
580 					// restore content to original textarea
581 					this.originalObj.val( this.getContents() );
582 					this.obj.remove();
583 					this.originalObj.show();
584 					break;
585 				default:
586 					break;
587 			}
588 
589 			// now the editable is not ready any more
590 			this.ready = false;
591 
592 			// remove the placeholder if needed.
593 			this.removePlaceholder( this.obj );
594 
595 			// initialize the object and disable contentEditable
596 			// unbind all events
597 			// TODO should only unbind the specific handlers.
598 			this.obj.removeClass( 'aloha-editable' )
599 			    .contentEditable( false )
600 			    .unbind( 'mousedown click dblclick focus keydown keypress keyup' );
601 
602 			/* TODO remove this event, it should implemented as bind and unbind
603 			// register the onSelectionChange Event with the Editable field
604 			this.obj.contentEditableSelectionChange( function( event ) {
605 				Aloha.Selection.onChange( me.obj, event );
606 				return me.obj;
607 			} );
608 			*/
609 
610 			// throw a new event when the editable has been created
611 			/**
612 			 * @event editableCreated fires after a new editable has been destroyes, eg. via $( '#editme' ).mahalo()
613 			 * The event is triggered in Aloha's global scope Aloha
614 			 * @param {Event} e the event object
615 			 * @param {Array} a an array which contains a reference to the currently created editable on its first position
616 			 */
617 			Aloha.trigger( 'aloha-editable-destroyed', [ this ] );
618 
619 			// finally register the editable with Aloha
620 			Aloha.unregisterEditable( this );
621 		},
622 
623 		/**
624 		 * marks the editables current state as unmodified. Use this method to inform the editable
625 		 * that it's contents have been saved
626 		 * @method
627 		 */
628 		setUnmodified: function() {
629 			this.originalContent = this.getContents();
630 		},
631 
632 		/**
633 		 * check if the editable has been modified during the edit process#
634 		 * @method
635 		 * @return boolean true if the editable has been modified, false otherwise
636 		 */
637 		isModified: function() {
638 			return this.originalContent !== this.getContents();
639 		},
640 
641 		/**
642 		 * String representation of the object
643 		 * @method
644 		 * @return Aloha.Editable
645 		 */
646 		toString: function() {
647 			return 'Aloha.Editable';
648 		},
649 
650 		/**
651 		 * check whether the editable has been disabled
652 		 */
653 		isDisabled: function() {
654 			return !this.obj.contentEditable()
655 				|| this.obj.contentEditable() === 'false';
656 		},
657 
658 		/**
659 		 * disable this editable
660 		 * a disabled editable cannot be written on by keyboard
661 		 */
662 		disable: function() {
663 			return this.isDisabled() || this.obj.contentEditable( false );
664 		},
665 
666 		/**
667 		 * enable this editable
668 		 * reenables a disabled editable to be writteable again
669 		 */
670 		enable: function() {
671 			return this.isDisabled() && this.obj.contentEditable( true );
672 		},
673 
674 
675 		/**
676 		 * activates an Editable for editing
677 		 * disables all other active items
678 		 * @method
679 		 */
680 		activate: function( e ) {
681 			// get active Editable before setting the new one.
682 			var oldActive = Aloha.getActiveEditable();
683 
684 			// We need to ommit this call when this flag is set to true.
685 			// This flag will only be set to true before the removePlaceholder method
686 			// is called since that method invokes a focus event which will again trigger
687 			// this method. We want to avoid double invokation of this method.
688 			if ( ignoreNextActivateEvent ) {
689 				ignoreNextActivateEvent = false;
690 				return;
691 			}
692 
693 			// handle special case in which a nested editable is focused by a click
694 			// in this case the "focus" event would be triggered on the parent element
695 			// which actually shifts the focus away to it's parent. this if is here to
696 			// prevent this situation
697 			if ( e && e.type === 'focus' && oldActive !== null
698 			     && oldActive.obj.parent().get( 0 ) === e.currentTarget ) {
699 				return;
700 			}
701 
702 			// leave immediately if this is already the active editable
703 			if ( this.isActive || this.isDisabled() ) {
704 				// we don't want parent editables to be triggered as well, so return false
705 				return;
706 			}
707 
708 			this.obj.addClass( 'aloha-editable-active' );
709 
710 			Aloha.activateEditable( this );
711 
712 			ignoreNextActivateEvent = true;
713 			this.removePlaceholder ( this.obj, true );
714 			ignoreNextActivateEvent = false;
715 
716 			this.isActive = true;
717 
718 			/**
719 			 * @event editableActivated fires after the editable has been activated by clicking on it.
720 			 * This event is triggered in Aloha's global scope Aloha
721 			 * @param {Event} e the event object
722 			 * @param {Array} a an array which contains a reference to last active editable on its first position, as well
723 			 * as the currently active editable on it's second position
724 			 */
725 			// trigger a 'general' editableActivated event
726 			Aloha.trigger( 'aloha-editable-activated', {
727 				'oldActive' : oldActive,
728 				'editable'  : this
729 			} );
730 		},
731 
732 		/**
733 		 * handle the blur event
734 		 * this must not be attached to the blur event, which will trigger far too often
735 		 * eg. when a table within an editable is selected
736 		 * @hide
737 		 */
738 		blur: function() {
739 			this.obj.blur();
740 			this.isActive = false;
741 			this.initPlaceholder();
742 			this.obj.removeClass( 'aloha-editable-active' );
743 
744 			/**
745 			 * @event editableDeactivated fires after the editable has been activated by clicking on it.
746 			 * This event is triggered in Aloha's global scope Aloha
747 			 * @param {Event} e the event object
748 			 * @param {Array} a an array which contains a reference to this editable
749 			 */
750 			Aloha.trigger( 'aloha-editable-deactivated', { editable : this } );
751 
752 			/**
753 			 * @event smartContentChanged
754 			 */
755 			Aloha.activeEditable.smartContentChange( { type : 'blur' }, null );
756 		},
757 
758 		/**
759 		 * check if the string is empty
760 		 * used for zerowidth check
761 		 * @return true if empty or string is null, false otherwise
762 		 * @hide
763 		 */
764 		empty: function( str ) {
765 			// br is needed for chrome
766 			return ( null === str )
767 				|| ( jQuery.trim( str ) === '' || str === '<br/>' );
768 		},
769 
770 		/**
771 		 * Get the contents of this editable as a HTML string or child node DOM
772 		 * objects.
773 		 *
774 		 * @param {boolean} asObject Whether or not to retreive the contents of
775 		 *                           this editable as child node objects or as
776 		 *                           HTML string.
777 		 * @return {string|jQuery.<HTMLElement>} Contents of the editable as
778 		 *                                       DOM objects or an HTML string.
779 		 */
780 		getContents: function (asObject) {
781 			var raw = this.obj.html();
782 			var cache = editableContentCache[this.getId()];
783 			if (cache && raw === cache.raw) {
784 				return asObject ? cache.elements : cache.clean;
785 			}
786 
787 			var $clone = this.obj.clone(false);
788 			$clone.find( '.aloha-cleanme' ).remove();
789 			this.removePlaceholder($clone);
790 			PluginManager.makeClean($clone);
791 
792 			makeClean($clone);
793 
794 			$clone = jQuery('<div>' + ContentHandlerManager.handleContent($clone.html(), {
795 				contenthandler: Aloha.settings.contentHandler.getContents,
796 				command: 'getContents'
797 			}) + '</div>');
798 
799 			cache = editableContentCache[this.getId()] = {};
800 			cache.raw = raw;
801 			cache.clean = contentSerializer($clone[0]);
802 			cache.elements = $clone.contents();
803 
804 			return asObject ? cache.elements : cache.clean;
805 		},
806 
807 		/**
808 		 * Set the contents of this editable as a HTML string
809 		 * @param content as html
810 		 * @param return as object or html string
811 		 * @return contents of the editable
812 		 */
813 		setContents: function( content, asObject ) {
814 			var reactivate = null;
815 
816 			if ( Aloha.getActiveEditable() === this ) {
817 				Aloha.deactivateEditable();
818 				reactivate = this;
819 			}
820 
821 			this.obj.html( content );
822 
823 			if ( null !== reactivate ) {
824 				reactivate.activate();
825 			}
826 
827 			this.smartContentChange({type : 'set-contents'});
828 
829 			return asObject ? this.obj.contents() : contentSerializer(this.obj[0]);
830 		},
831 
832 		/**
833 		 * Get the id of this editable
834 		 * @method
835 		 * @return id of this editable
836 		 */
837 		getId: function() {
838 			return this.obj.attr( 'id' );
839 		},
840 
841 		/**
842 		 * Generates and signals a smartContentChange event.
843 		 *
844 		 * A smart content change occurs when a special editing action, or a
845 		 * combination of interactions are performed by the user during the
846 		 * course of editing within an editable.
847 		 * The smart content change event would therefore signal to any
848 		 * component that is listening to this event, that content has been
849 		 * inserted into the editable that may need to be prococessed in a
850 		 * special way
851 		 * This is used for smart actions within the content/while editing.
852 		 * @param {Event} event
853 		 * @hide
854 		 */
855 		smartContentChange: function( event ) {
856 			var me = this,
857 			    uniChar = null,
858 			    re,
859 			    match;
860 
861 			// ignore meta keys like crtl+v or crtl+l and so on
862 			if ( event && ( event.metaKey || event.crtlKey || event.altKey ) ) {
863 				return false;
864 			}
865 
866 			if ( event && event.originalEvent ) {
867 				// regex to strip unicode
868 				re = new RegExp( "U\\+(\\w{4})" );
869 				match = re.exec( event.originalEvent.keyIdentifier );
870 
871 				// Use keyIdentifier if available
872 				if ( event.originalEvent.keyIdentifier && 1 === 2 ) {
873 					// @fixme: Because of "&& 1 === 2" above, this block is
874 					// unreachable code
875 					if ( match !== null ) {
876 						uniChar = unescape( '%u' + match[1] );
877 					}
878 					if ( uniChar === null ) {
879 						uniChar = event.originalEvent.keyIdentifier;
880 					}
881 
882 				// FF & Opera don't support keyIdentifier
883 				} else {
884 					// Use among browsers reliable which http://api.jquery.com/keypress
885 					uniChar = ( this.keyCodeMap[ this.keyCode ] ||
886 								String.fromCharCode( event.which ) || 'unknown' );
887 				}
888 			}
889 
890 			var snapshot = null;
891 			function getSnapshotContent() {
892 				if (null == snapshot) {
893 					snapshot = me.getSnapshotContent();
894 				}
895 				return snapshot;
896 			}
897 
898 			// handle "Enter" -- it's not "U+1234" -- when returned via "event.originalEvent.keyIdentifier"
899 			// reference: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html
900 			if ( jQuery.inArray( uniChar, this.sccDelimiters ) >= 0 ) {
901 				clearTimeout( this.sccTimerIdle );
902 				clearTimeout( this.sccTimerDelay );
903 904 
				this.sccTimerDelay = window.setTimeout( function() {
905 					Aloha.trigger( 'aloha-smart-content-changed', {
906 						'editable'        : me,
907 						'keyIdentifier'   : event.originalEvent.keyIdentifier,
908 						'keyCode'         : event.keyCode,
909 						'char'            : uniChar,
910 						'triggerType'     : 'keypress', // keypress, timer, blur, paste
911 						'getSnapshotContent' : getSnapshotContent
912 					} );
913 
914 					console.debug( 'Aloha.Editable',
915 						'smartContentChanged: event type keypress triggered' );
916 				}, this.sccDelay );
917 			} else if ( event && event.type === 'paste' ) {
918 				Aloha.trigger( 'aloha-smart-content-changed', {
919 					'editable'        : me,
920 					'keyIdentifier'   : null,
921 					'keyCode'         : null,
922 					'char'            : null,
923 					'triggerType'     : 'paste',
924 					'getSnapshotContent' : getSnapshotContent
925 				} );
926 
927 			} else if ( event && event.type === 'blur' ) {
928 				Aloha.trigger( 'aloha-smart-content-changed', {
929 					'editable'        : me,
930 					'keyIdentifier'   : null,
931 					'keyCode'         : null,
932 					'char'            : null,
933 					'triggerType'     : 'blur',
934 					'getSnapshotContent' : getSnapshotContent
935 				} );
936 
937 			} else if ( uniChar !== null ) {
938 				// in the rare case idle time is lower then delay time
939 				clearTimeout( this.sccTimerDelay );
940 				clearTimeout( this.sccTimerIdle );
941 				this.sccTimerIdle = window.setTimeout( function() {
942 					Aloha.trigger( 'aloha-smart-content-changed', {
943 						'editable'        : me,
944 						'keyIdentifier'   : null,
945 						'keyCode'         : null,
946 						'char'            : null,
947 						'triggerType'     : 'idle',
948 						'getSnapshotContent' : getSnapshotContent
949 					} );
950 				}, this.sccIdle );
951 			}
952 		},
953 
954 		/**
955 		 * Get a snapshot of the active editable as a HTML string
956 		 * @hide
957 		 * @return snapshot of the editable
958 		 */
959 		getSnapshotContent: function() {
960 			var ret = this.snapshotContent;
961 			this.snapshotContent = this.getContents();
962 			return ret;
963 		}
964 	} );
965 
966 	/**
967 	 * Sets the serializer function to be used for the contents of all editables.
968 	 *
969 	 * The default content serializer will just call the jQuery.html()
970 	 * function on the editable element (which gets the innerHTML property).
971 	 *
972 	 * This method is a static class method and will affect the result
973 	 * of editable.getContents() for all editables that have been or
974 	 * will be constructed.
975 	 *
976 	 * @param serializerFunction
977 	 *        A function that accepts a DOM element and returns the serialized
978 	 *        XHTML of the element contents (excluding the start and end tag of
979 	 *        the passed element).
980 	 */
981 	Aloha.Editable.setContentSerializer = function( serializerFunction ) {
982 		contentSerializer = serializerFunction;
983 	};
984 } );
985