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