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