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