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