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