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