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