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