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|jquery)[\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 212 		 * @return void
213 		 * @hide
214 		 */
215 		init: function() {
216 			var me = this;
217 
218 			// TODO make editables their own settings.
219 			this.settings = Aloha.settings;
220 
221 			// smartContentChange settings
222 			// @TODO move to new config when implemented in Aloha
223 			if ( Aloha.settings && Aloha.settings.smartContentChange ) {
224 				if ( Aloha.settings.smartContentChange.delimiters ) {
225 					this.sccDelimiters = Aloha.settings.smartContentChange.delimiters;
226 				}
227 
228 				if ( Aloha.settings.smartContentChange.idle ) {
229 					this.sccIdle = Aloha.settings.smartContentChange.idle;
230 				}
231 
232 				if ( Aloha.settings.smartContentChange.delay ) {
233 					this.sccDelay = Aloha.settings.smartContentChange.delay;
234 				}
235 			}
236 
237 			// check if Aloha can handle the obj as Editable
238 			if ( !this.check( this.obj ) ) {
239 				//Aloha.log( 'warn', this, 'Aloha cannot handle {' + this.obj[0].nodeName + '}' );
240 				this.destroy();
241 				return;
242 			}
243 
244 			// apply content handler to clean up content
245 			if ( typeof Aloha.settings.contentHandler.getContents === 'undefined' ) {
246 				Aloha.settings.contentHandler.getContents = Aloha.defaults.contentHandler.getContents;
247 			}
248 
249 			// apply content handler to clean up content
250 			if ( typeof Aloha.settings.contentHandler.initEditable === 'undefined' ) {
251 				Aloha.settings.contentHandler.initEditable = Aloha.defaults.contentHandler.initEditable;
252 			}
253 			
254 			var content = me.obj.html();
255 			content = ContentHandlerManager.handleContent( content, {
256 				contenthandler: Aloha.settings.contentHandler.initEditable,
257 				command: 'initEditable'
258 			} );
259 			me.obj.html( content );
260 
261 			// only initialize the editable when Aloha is fully ready (including plugins)
262 			Aloha.bind( 'aloha-ready', function() {
263 				// initialize the object
264 				me.obj.addClass( 'aloha-editable' ).contentEditable( true );
265 
266 				// add focus event to the object to activate
267 				me.obj.mousedown( function( e ) {
268 					// check whether the mousedown was already handled
269 					if ( !Aloha.eventHandled ) {
270 						Aloha.eventHandled = true;
271 						return me.activate( e );
272 					}
273 				} );
274 
275 				me.obj.mouseup( function( e ) {
276 					Aloha.eventHandled = false;
277 				} );
278 
279 				me.obj.focus( function( e ) {
280 					return me.activate( e );
281 				} );
282 
283 				// by catching the keydown we can prevent the browser from doing its own thing
284 				// if it does not handle the keyStroke it returns true and therefore all other
285 				// events (incl. browser's) continue
286 				//me.obj.keydown( function( event ) {
287 				//me.obj.add('.aloha-block', me.obj).live('keydown', function (event) { // live not working but would be usefull
288 				me.obj.add('.aloha-block', me.obj).keydown(function (event) {
289 					var letEventPass = Markup.preProcessKeyStrokes( event );
290 					me.keyCode = event.which;
291 
292 					if (!letEventPass) {
293 						// the event will not proceed to key press, therefore trigger smartContentChange
294 						me.smartContentChange( event );
295 					}
296 					return letEventPass;
297 				} );
298 
299 				// handle keypress
300 				me.obj.keypress( function( event ) {
301 					// triggers a smartContentChange to get the right charcode
302 					// To test try http://www.w3.org/2002/09/tests/keys.html
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 751 			Aloha.trigger( 'aloha-editable-deactivated', { editable : this } );
752 
753 			/**
754 			 * @event smartContentChanged
755 			 */
756 			Aloha.activeEditable.smartContentChange( { type : 'blur' }, null );
757 		},
758 
759 		/**
760 		 * check if the string is empty
761 		 * used for zerowidth check
762 		 * @return true if empty or string is null, false otherwise
763 		 * @hide
764 		 */
765 		empty: function( str ) {
766 			// br is needed for chrome
767 			return ( null === str )
768 				|| ( jQuery.trim( str ) === '' || str === '<br/>' );
769 		},
770 
771 		/**
772 		 * Get the contents of this editable as a HTML string or child node DOM
773 		 * objects.
774 		 *
775 		 * @param {boolean} asObject Whether or not to retreive the contents of
776 		 *                           this editable as child node objects or as
777 		 *                           HTML string.
778 		 * @return {string|jQuery.<HTMLElement>} Contents of the editable as
779 		 *                                       DOM objects or an HTML string.
780 		 */
781 		getContents: function (asObject) {
782 			var raw = this.obj.html();
783 			var cache = editableContentCache[this.getId()];
784 			if (cache && raw === cache.raw) {
785 				return asObject ? cache.elements : cache.clean;
786 			}
787 
788 			var $clone = this.obj.clone(false);
789 			$clone.find( '.aloha-cleanme' ).remove();
790 			this.removePlaceholder($clone);
791 			PluginManager.makeClean($clone);
792 
793 			makeClean($clone);
794 
795 			$clone = jQuery('<div>' + ContentHandlerManager.handleContent($clone.html(), {
796 				contenthandler: Aloha.settings.contentHandler.getContents,
797 				command: 'getContents'
798 			}) + '</div>');
799 800 
			cache = editableContentCache[this.getId()] = {};
801 			cache.raw = raw;
802 			cache.clean = contentSerializer($clone[0]);
803 			cache.elements = $clone.contents();
804 
805 			return asObject ? cache.elements : cache.clean;
806 		},
807 
808 		/**
809 		 * Set the contents of this editable as a HTML string
810 		 * @param content as html
811 		 * @param return as object or html string
812 		 * @return contents of the editable
813 		 */
814 		setContents: function( content, asObject ) {
815 			var reactivate = null;
816 
817 			if ( Aloha.getActiveEditable() === this ) {
818 				Aloha.deactivateEditable();
819 				reactivate = this;
820 			}
821 
822 			this.obj.html( content );
823 
824 			if ( null !== reactivate ) {
825 				reactivate.activate();
826 			}
827 
828 			this.smartContentChange({type : 'set-contents'});
829 
830 			return asObject ? this.obj.contents() : contentSerializer(this.obj[0]);
831 		},
832 
833 		/**
834 		 * Get the id of this editable
835 		 * @method
836 		 * @return id of this editable
837 		 */
838 		getId: function() {
839 			return this.obj.attr( 'id' );
840 		},
841 
842 		/**
843 		 * Generates and signals a smartContentChange event.
844 		 *
845 		 * A smart content change occurs when a special editing action, or a
846 		 * combination of interactions are performed by the user during the
847 		 * course of editing within an editable.
848 		 * The smart content change event would therefore signal to any
849 		 * component that is listening to this event, that content has been
850 		 * inserted into the editable that may need to be prococessed in a
851 		 * special way
852 		 * This is used for smart actions within the content/while editing.
853 		 * @param {Event} event
854 		 * @hide
855 		 */
856 		smartContentChange: function( event ) {
857 			var me = this,
858 			    uniChar = null,
859 			    re,
860 			    match;
861 
862 			// ignore meta keys like crtl+v or crtl+l and so on
863 			if ( event && ( event.metaKey || event.crtlKey || event.altKey ) ) {
864 				return false;
865 			}
866 
867 			if ( event && event.originalEvent ) {
868 				// regex to strip unicode
869 				re = new RegExp( "U\\+(\\w{4})" );
870 				match = re.exec( event.originalEvent.keyIdentifier );
871 
872 				// Use keyIdentifier if available
873 				if ( event.originalEvent.keyIdentifier && 1 === 2 ) {
874 					// @fixme: Because of "&& 1 === 2" above, this block is
875 					// unreachable code
876 					if ( match !== null ) {
877 						uniChar = unescape( '%u' + match[1] );
878 					}
879 					if ( uniChar === null ) {
880 						uniChar = event.originalEvent.keyIdentifier;
881 					}
882 
883 				// FF & Opera don't support keyIdentifier
884 				} else {
885 					// Use among browsers reliable which http://api.jquery.com/keypress
886 					uniChar = ( this.keyCodeMap[ this.keyCode ] ||
887 								String.fromCharCode( event.which ) || 'unknown' );
888 				}
889 			}
890 
891 			var snapshot = null;
892 			function getSnapshotContent() {
893 				if (null == snapshot) {
894 					snapshot = me.getSnapshotContent();
895 				}
896 				return snapshot;
897 			}
898 
899 			// handle "Enter" -- it's not "U+1234" -- when returned via "event.originalEvent.keyIdentifier"
900 			// reference: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html
901 			if ( jQuery.inArray( uniChar, this.sccDelimiters ) >= 0 ) {
902 				clearTimeout( this.sccTimerIdle );
903 				clearTimeout( this.sccTimerDelay );
904 
905 				this.sccTimerDelay = window.setTimeout( function() {
906 					Aloha.trigger( 'aloha-smart-content-changed', {
907 						'editable'        : me,
908 						'keyIdentifier'   : event.originalEvent.keyIdentifier,
909 						'keyCode'         : event.keyCode,
910 						'char'            : uniChar,
911 						'triggerType'     : 'keypress', // keypress, timer, blur, paste
912 						'getSnapshotContent' : getSnapshotContent
913 					} );
914 
915 					console.debug( 'Aloha.Editable',
916 						'smartContentChanged: event type keypress triggered' );
917 				}, this.sccDelay );
918 			} else if ( event && event.type === 'paste' ) {
919 				Aloha.trigger( 'aloha-smart-content-changed', {
920 					'editable'        : me,
921 					'keyIdentifier'   : null,
922 					'keyCode'         : null,
923 					'char'            : null,
924 					'triggerType'     : 'paste',
925 					'getSnapshotContent' : getSnapshotContent
926 				} );
927 
928 			} else if ( event && event.type === 'blur' ) {
929 				Aloha.trigger( 'aloha-smart-content-changed', {
930 					'editable'        : me,
931 					'keyIdentifier'   : null,
932 					'keyCode'         : null,
933 					'char'            : null,
934 					'triggerType'     : 'blur',
935 					'getSnapshotContent' : getSnapshotContent
936 				} );
937 
938 			} else if ( uniChar !== null ) {
939 				// in the rare case idle time is lower then delay time
940 				clearTimeout( this.sccTimerDelay );
941 				clearTimeout( this.sccTimerIdle );
942 				this.sccTimerIdle = window.setTimeout( function() {
943 					Aloha.trigger( 'aloha-smart-content-changed', {
944 						'editable'        : me,
945 						'keyIdentifier'   : null,
946 						'keyCode'         : null,
947 						'char'            : null,
948 						'triggerType'     : 'idle',
949 						'getSnapshotContent' : getSnapshotContent
950 					} );
951 				}, this.sccIdle );
952 			}
953 		},
954 
955 		/**
956 		 * Get a snapshot of the active editable as a HTML string
957 		 * @hide
958 		 * @return snapshot of the editable
959 		 */
960 		getSnapshotContent: function() {
961 			var ret = this.snapshotContent;
962 			this.snapshotContent = this.getContents();
963 			return ret;
964 		}
965 	} );
966 
967 	/**
968 	 * Sets the serializer function to be used for the contents of all editables.
969 	 *
970 	 * The default content serializer will just call the jQuery.html()
971 	 * function on the editable element (which gets the innerHTML property).
972 	 *
973 	 * This method is a static class method and will affect the result
974 	 * of editable.getContents() for all editables that have been or
975 	 * will be constructed.
976 	 *
977 	 * @param serializerFunction
978 	 *        A function that accepts a DOM element and returns the serialized
979 	 *        XHTML of the element contents (excluding the start and end tag of
980 	 *        the passed element).
981 	 */
982 	Aloha.Editable.setContentSerializer = function( serializerFunction ) {
983 		contentSerializer = serializerFunction;
984 	};
985 } );
986