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