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