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