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