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