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 
381 				me.initPlaceholder();
382 
383 				me.ready = true;
384 
385 				// disable object resizing.
386 				// we do this in here and with a slight delay, because
387 				// starting with FF 15, this would cause a JS error
388 				// if done before the first DOM object is made contentEditable.
389 				window.setTimeout(function () {
390 					Aloha.disableObjectResizing();
391 				}, 20);
392 
393 				// throw a new event when the editable has been created
394 				/**
395 				 * @event editableCreated fires after a new editable has been created, eg. via $( '#editme' ).aloha()
396 				 * The event is triggered in Aloha's global scope Aloha
397 				 * @param {Event} e the event object
398 				 * @param {Array} a an array which contains a reference to the currently created editable on its first position
399 				 */
400 				Aloha.trigger('aloha-editable-created', [me]);
401 				PubSub.pub('aloha.editable.created', {data: me});
402 			});
403 		},
404 
405 		/**
406 		 * True, if this editable is active for editing
407 		 * @property
408 		 * @type boolean
409 		 */
410 		isActive: false,
411 
412 		/**
413 		 * stores the original content to determine if it has been modified
414 		 * @hide
415 		 */
416 		originalContent: null,
417 
418 		/**
419 		 * every time a selection is made in the current editable the selection has to
420 		 * be saved for further use
421 		 * @hide
422 		 */
423 		range: undefined,
424 
425 		/**
426 		 * Check if object can be edited by Aloha Editor
427 		 * @return {boolean } editable true if Aloha Editor can handle else false
428 		 * @hide
429 		 */
430 		check: function () {
431 			/* TODO check those elements
432 			'map', 'meter', 'object', 'output', 'progress', 'samp',
433 			'time', 'area', 'datalist', 'figure', 'kbd', 'keygen',
434 			'mark', 'math', 'wbr', 'area',
435 			*/
436 
437 			// Extract El
438 			var me = this,
439 				obj = this.obj,
440 				el = obj.get(0),
441 				nodeName = el.nodeName.toLowerCase(),
442 
443 				// supported elements
444 				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'],
445 				i,
446 			    div;
447 
448 			for (i = 0; i < textElements.length; ++i) {
449 				if (nodeName === textElements[i]) {
450 					return true;
451 				}
452 			}
453 
454 			// special handled elements
455 			switch (nodeName) {
456 			case 'label':
457 			case 'button':
458 				// TODO need some special handling.
459 				break;
460 			case 'textarea':
461 			case 'input':
462 				// Create a div alongside the textarea
463 				div = jQuery('<div id="' + this.getId() + '-aloha" class="aloha-' + nodeName + '" />').insertAfter(obj);
464 
465 				// Resize the div to the textarea and
466 				// Populate the div with the value of the textarea
467 				// Then, hide the textarea
468 				div.height(obj.height()).width(obj.width()).html(obj.val());
469 
470 				obj.hide();
471 
472 				// Attach a onsubmit to the form to place the HTML of the
473 				// div back into the textarea
474 				obj.parents('form:first').submit(function () {
475 					obj.val(me.getContents());
476 				});
477 
478 				// Swap textarea reference with the new div
479 				this.obj = div;
480 
481 				// Supported
482 				return true;
483 			default:
484 				break;
485 			}
486 
487 			// the following elements are not supported
488 			/*
489 			'canvas', 'audio', 'br', 'embed', 'fieldset', 'hgroup', 'hr',
490 			'iframe', 'img', 'input', 'map', 'script', 'select', 'style',
491 			'svg', 'table', 'ul', 'video', 'ol', 'form', 'noscript',
492 			 */
493 			return false;
494 		},
495 
496 		/**
497 		 * Init Placeholder
498 		 *
499 		 * @return void
500 		 */
501 		initPlaceholder: function () {
502 			if (Aloha.settings.placeholder && this.isEmpty()) {
503 				this.addPlaceholder();
504 			}
505 		},
506 
507 		/**
508 		 * Check if the conteneditable is empty.
509 		 *
510 		 * @return {Boolean}
511 		 */
512 		isEmpty: function () {
513 			var editableTrimedContent = jQuery.trim(this.getContents()),
514 				onlyBrTag = (editableTrimedContent === '<br>') ? true : false;
515 			return (editableTrimedContent.length === 0 || onlyBrTag);
516 		},
517 
518 		/**
519 		 * Add placeholder in editable
520 		 *
521 		 * @return void
522 		 */
523 		addPlaceholder: function () {
524 525 			var div = jQuery('<div>'),
526 				span = jQuery('<span>'),
527 				el,
528 				obj = this.obj;
529 			if (GENTICS.Utils.Dom.allowsNesting(obj[0], div[0])) {
530 				el = div;
531 			} else {
532 				el = span;
533 			}
534 			if (jQuery("." + this.placeholderClass, obj).length !== 0) {
535 				return;
536 			}
537 			jQuery.each(Aloha.settings.placeholder, function (selector, selectorConfig) {
538 				if (obj.is(selector)) {
539 					el.html(selectorConfig);
540 				}
541 			});
542 			if (!el.is(':empty')) {
543 				el.addClass(this.placeholderClass).addClass('aloha-ephemera');
544 				jQuery(obj).append(el);
545 			}
546 			jQuery('br', obj).remove();
547 		},
548 
549 		/**
550 		 * remove placeholder from contenteditable. If setCursor is true,
551 		 * will also set the cursor to the start of the selection. However,
552 		 * this will be ASYNCHRONOUS, so if you rely on the fact that
553 		 * the placeholder is removed after calling this method, setCursor
554 		 * should be false ( or not set )
555 		 *
556 		 * @return void
557 		 */
558 559 		removePlaceholder: function (obj, setCursor) {
560 			var placeholderClass = this.placeholderClass,
561 				range;
562 			if (jQuery("." + this.placeholderClass, obj).length === 0) {
563 				return;
564 			}
565 			// set the cursor // remove placeholder
566 			if (setCursor === true) {
567 				window.setTimeout(function () {
568 569 					range = new Selection.SelectionRange();
570 					range.startContainer = range.endContainer = obj.get(0);
571 					range.startOffset = range.endOffset = 0;
572 					jQuery('.' + placeholderClass, obj).remove();
573 					range.select();
574 
575 				}, 100);
576 			} else {
577 				jQuery('.' + placeholderClass, obj).remove();
578 			}
579 		},
580 
581 		/**
582 		 * destroy the editable
583 		 * @return void
584 		 */
585 		destroy: function () {
586 			// leave the element just to get sure
587 			if (this === Aloha.getActiveEditable()) {
588 				this.blur();
589 			}
590 
591 			// special handled elements
592 			switch (this.originalObj.get(0).nodeName.toLowerCase()) {
593 			case 'label':
594 			case 'button':
595 				// TODO need some special handling.
596 				break;
597 			case 'textarea':
598 			case 'input':
599 				// restore content to original textarea
600 				this.originalObj.val(this.getContents());
601 				this.obj.remove();
602 				this.originalObj.show();
603 				break;
604 			default:
605 				break;
606 			}
607 
608 			// now the editable is not ready any more
609 			this.ready = false;
610 
611 			// remove the placeholder if needed.
612 			this.removePlaceholder(this.obj);
613 
614 			// initialize the object and disable contentEditable
615 			// unbind all events
616 			// TODO should only unbind the specific handlers.
617 			this.obj.removeClass('aloha-editable').contentEditable(false).unbind('mousedown click dblclick focus keydown keypress keyup');
618 
619 			/* TODO remove this event, it should implemented as bind and unbind
620 			// register the onSelectionChange Event with the Editable field
621 			this.obj.contentEditableSelectionChange( function( event ) {
622 				Aloha.Selection.onChange( me.obj, event );
623 				return me.obj;
624 			} );
625 			*/
626 
627 			// throw a new event when the editable has been created
628 			/**
629 			 * @event editableCreated fires after a new editable has been destroyes, eg. via $( '#editme' ).mahalo()
630 			 * The event is triggered in Aloha's global scope Aloha
631 			 * @param {Event} e the event object
632 			 * @param {Array} a an array which contains a reference to the currently created editable on its first position
633 			 */
634 			Aloha.trigger('aloha-editable-destroyed', [this]);
635 			PubSub.pub('aloha.editable.destroyed', {data: this});
636 
637 			// finally register the editable with Aloha
638 			Aloha.unregisterEditable(this);
639 		},
640 
641 		/**
642 		 * marks the editables current state as unmodified. Use this method to inform the editable
643 		 * that it's contents have been saved
644 		 * @method
645 		 */
646 		setUnmodified: function () {
647 			this.originalContent = this.getContents();
648 		},
649 
650 		/**
651 		 * check if the editable has been modified during the edit process#
652 		 * @method
653 		 * @return boolean true if the editable has been modified, false otherwise
654 		 */
655 		isModified: function () {
656 			return this.originalContent !== this.getContents();
657 		},
658 
659 		/**
660 		 * String representation of the object
661 		 * @method
662 		 * @return Aloha.Editable
663 		 */
664 		toString: function () {
665 			return 'Aloha.Editable';
666 		},
667 
668 		/**
669 		 * check whether the editable has been disabled
670 		 */
671 		isDisabled: function () {
672 			return !this.obj.contentEditable() || this.obj.contentEditable() === 'false';
673 		},
674 
675 		/**
676 		 * disable this editable
677 		 * a disabled editable cannot be written on by keyboard
678 		 */
679 		disable: function () {
680 			return this.isDisabled() || this.obj.contentEditable(false);
681 		},
682 
683 		/**
684 		 * enable this editable
685 		 * reenables a disabled editable to be writteable again
686 		 */
687 		enable: function () {
688 			return this.isDisabled() && this.obj.contentEditable(true);
689 		},
690 
691 
692 		/**
693 		 * activates an Editable for editing
694 		 * disables all other active items
695 		 * @method
696 		 */
697 		activate: function (e) {
698 			// get active Editable before setting the new one.
699 			var oldActive = Aloha.getActiveEditable();
700 
701 			// We need to ommit this call when this flag is set to true.
702 			// This flag will only be set to true before the removePlaceholder method
703 			// is called since that method invokes a focus event which will again trigger
704 			// this method. We want to avoid double invokation of this method.
705 			if (ignoreNextActivateEvent) {
706 				ignoreNextActivateEvent = false;
707 				return;
708 			}
709 
710 			// handle special case in which a nested editable is focused by a click
711 			// in this case the "focus" event would be triggered on the parent element
712 			// which actually shifts the focus away to it's parent. this if is here to
713 			// prevent this situation
714 			if (e && e.type === 'focus' && oldActive !== null && oldActive.obj.parent().get(0) === e.currentTarget) {
715 				return;
716 			}
717 
718 			// leave immediately if this is already the active editable
719 			if (this.isActive || this.isDisabled()) {
720 				// we don't want parent editables to be triggered as well, so return false
721 				return;
722 			}
723 
724 			this.obj.addClass('aloha-editable-active');
725 
726 			Aloha.activateEditable(this);
727 
728 			ignoreNextActivateEvent = true;
729 			this.removePlaceholder(this.obj, true);
730 			ignoreNextActivateEvent = false;
731 
732 			this.isActive = true;
733 
734 			/**
735 			 * @event editableActivated fires after the editable has been activated by clicking on it.
736 			 * This event is triggered in Aloha's global scope Aloha
737 			 * @param {Event} e the event object
738 			 * @param {Array} a an array which contains a reference to last active editable on its first position, as well
739 			 * as the currently active editable on it's second position
740 			 */
741 			// trigger a 'general' editableActivated event
742 			Aloha.trigger('aloha-editable-activated', {
743 				'oldActive': oldActive,
744 				'editable': this
745 			});
746 			PubSub.pub('aloha.editable.activated', {
747 				data: {
748 					old: oldActive,
749 					editable: this
750 				}
751 			});
752 		},
753 
754 		/**
755 		 * handle the blur event
756 		 * this must not be attached to the blur event, which will trigger far too often
757 		 * eg. when a table within an editable is selected
758 		 * @hide
759 		 */
760 		blur: function () {
761 			this.obj.blur();
762 			this.isActive = false;
763 			this.initPlaceholder();
764 			this.obj.removeClass('aloha-editable-active');
765 
766 			/**
767 			 * @event editableDeactivated fires after the editable has been activated by clicking on it.
768 			 * This event is triggered in Aloha's global scope Aloha
769 			 * @param {Event} e the event object
770 			 * @param {Array} a an array which contains a reference to this editable
771 			 */
772 			Aloha.trigger('aloha-editable-deactivated', {
773 				editable: this
774 			});
775 			PubSub.pub('aloha.editable.deactivated', {
776 				data: {
777 					editable: this
778 				}
779 			});
780 
781 			/**
782 			 * @event smartContentChanged
783 			 */
784 			Aloha.activeEditable.smartContentChange({
785 				type: 'blur'
786 			}, null);
787 		},
788 
789 		/**
790 		 * check if the string is empty
791 		 * used for zerowidth check
792 		 * @return true if empty or string is null, false otherwise
793 		 * @hide
794 		 */
795 		empty: function (str) {
796 			// br is needed for chrome
797 			return (null === str) || (jQuery.trim(str) === '' || str === '<br/>');
798 		},
799 
800 		/**
801 		 * Get the contents of this editable as a HTML string or child node DOM
802 		 * objects.
803 		 *
804 		 * @param {boolean} asObject Whether or not to retreive the contents of
805 		 *                           this editable as child node objects or as
806 		 *                           HTML string.
807 		 * @return {string|jQuery.<HTMLElement>} Contents of the editable as
808 		 *                                       DOM objects or an HTML string.
809 		 */
810 		getContents: function (asObject) {
811 			var raw = this.obj.html();
812 			var cache = editableContentCache[this.getId()];
813 
814 			if (!cache || raw !== cache.raw) {
815 
816 				BlockJump.removeZeroWidthTextNodeFix();
817 
818 				var $clone = this.obj.clone(false);
819 				this.removePlaceholder($clone);
820 				$clone = jQuery(Ephemera.prune($clone[0]));
821 				PluginManager.makeClean($clone);
822 
823 				// TODO rewrite ContentHandlerManager to accept DOM trees instead of strings
824 				$clone = jQuery('<div>' + ContentHandlerManager.handleContent($clone.html(), {
825 					contenthandler: Aloha.settings.contentHandler.getContents,
826 					command: 'getContents'
827 				}, this) + '</div>');
828 
829 				cache = editableContentCache[this.getId()] = {};
830 				cache.raw = raw;
831 				cache.element = $clone;
832 			}
833 
834 			if (asObject) {
835 				return cache.element.clone().contents();
836 			}
837 
838 			if (null == cache.serialized) {
839 				cache.serialized = contentSerializer(cache.element[0]);
840 			}
841 			return cache.serialized;
842 		},
843 
844 		/**
845 		 * Set the contents of this editable as a HTML string
846 		 * @param content as html
847 		 * @param return as object or html string
848 		 * @return contents of the editable
849 		 */
850 		setContents: function (content, asObject) {
851 			var reactivate = null;
852 
853 			if (Aloha.getActiveEditable() === this) {
854 				Aloha.deactivateEditable();
855 				reactivate = this;
856 			}
857 
858 			this.obj.html(content);
859 
860 			if (null !== reactivate) {
861 				reactivate.activate();
862 			}
863 
864 			this.smartContentChange({
865 				type: 'set-contents'
866 			});
867 
868 			return asObject ? this.obj.contents() : contentSerializer(this.obj[0]);
869 		},
870 
871 		/**
872 		 * Get the id of this editable
873 		 * @method
874 		 * @return id of this editable
875 		 */
876 		getId: function () {
877 			return this.obj.attr('id');
878 		},
879 
880 		/**
881 		 * Generates and signals a smartContentChange event.
882 		 *
883 		 * A smart content change occurs when a special editing action, or a
884 		 * combination of interactions are performed by the user during the
885 		 * course of editing within an editable.
886 		 * The smart content change event would therefore signal to any
887 		 * component that is listening to this event, that content has been
888 		 * inserted into the editable that may need to be prococessed in a
889 		 * special way
890 		 * This is used for smart actions within the content/while editing.
891 		 * @param {Event} event
892 		 * @hide
893 		 */
894 		smartContentChange: function (event) {
895 			var me = this,
896 				uniChar = null,
897 				re,
898 				match;
899 
900 			// ignore meta keys like crtl+v or crtl+l and so on
901 			if (event && (event.metaKey || event.crtlKey || event.altKey)) {
902 				return false;
903 			}
904 
905 			if (event && event.originalEvent) {
906 				// regex to strip unicode
907 				re = new RegExp("U\\+(\\w{4})");
908 				match = re.exec(event.originalEvent.keyIdentifier);
909 
910 				// Use among browsers reliable which http://api.jquery.com/keypress
911 				uniChar = (this.keyCodeMap[this.keyCode] || String.fromCharCode(event.which) || 'unknown');
912 			}
913 
914 			var snapshot = null;
915 
916 			function getSnapshotContent() {
917 				if (null == snapshot) {
918 					snapshot = me.getSnapshotContent();
919 				}
920 				return snapshot;
921 			}
922 
923 			// handle "Enter" -- it's not "U+1234" -- when returned via "event.originalEvent.keyIdentifier"
924 			// reference: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html
925 			if (jQuery.inArray(uniChar, this.sccDelimiters) >= 0) {
926 				clearTimeout(this.sccTimerIdle);
927 				clearTimeout(this.sccTimerDelay);
928 
929 				this.sccTimerDelay = window.setTimeout(function () {
930 					Aloha.trigger('aloha-smart-content-changed', {
931 						'editable': me,
932 						'keyIdentifier': event.originalEvent.keyIdentifier,
933 						'keyCode': event.keyCode,
934 						'char': uniChar,
935 						'triggerType': 'keypress', // keypress, timer, blur, paste
936 						'getSnapshotContent': getSnapshotContent
937 					});
938 					handleSmartContentChange(me);
939 
940 					console.debug('Aloha.Editable',
941 							'smartContentChanged: event type keypress triggered');
942 				}, this.sccDelay);
943 
944 			} else if (event && event.type === 'paste') {
945 				Aloha.trigger('aloha-smart-content-changed', {
946 					'editable': me,
947 					'keyIdentifier': null,
948 					'keyCode': null,
949 					'char': null,
950 					'triggerType': 'paste',
951 					'getSnapshotContent': getSnapshotContent
952 				});
953 				handleSmartContentChange(me);
954 
955 			} else if (event && event.type === 'blur') {
956 				Aloha.trigger('aloha-smart-content-changed', {
957 					'editable': me,
958 					'keyIdentifier': null,
959 					'keyCode': null,
960 					'char': null,
961 					'triggerType': 'blur',
962 					'getSnapshotContent': getSnapshotContent
963 				});
964 				handleSmartContentChange(me);
965 
966 			} else if (event && event.type === 'block-change') {
967 				Aloha.trigger('aloha-smart-content-changed', {
968 					'editable': me,
969 					'keyIdentifier': null,
970 					'keyCode': null,
971 					'char': null,
972 					'triggerType': 'block-change',
973 					'getSnapshotContent': getSnapshotContent
974 				});
975 				handleSmartContentChange(me);
976 
977 			} else if (uniChar !== null) {
978 				// in the rare case idle time is lower then delay time
979 				clearTimeout(this.sccTimerDelay);
980 				clearTimeout(this.sccTimerIdle);
981 				this.sccTimerIdle = window.setTimeout(function () {
982 					Aloha.trigger('aloha-smart-content-changed', {
983 						'editable': me,
984 						'keyIdentifier': null,
985 						'keyCode': null,
986 						'char': null,
987 						'triggerType': 'idle',
988 						'getSnapshotContent': getSnapshotContent
989 					});
990 					handleSmartContentChange(me);
991 				}, this.sccIdle);
992 			}
993 		},
994 
995 		/**
996 		 * Get a snapshot of the active editable as a HTML string
997 		 * @hide
998 		 * @return snapshot of the editable
999 		 */
1000 		getSnapshotContent: function () {
1001 			var ret = this.snapshotContent;
1002 			this.snapshotContent = this.getContents();
1003 			return ret;
1004 		}
1005 	});
1006 
1007 	/**
1008 	 * Sets the content serializer function.
1009 	 *
1010 	 * The default content serializer will just call the jQuery.html()
1011 	 * function on the editable element (which gets the innerHTML property).
1012 	 *
1013 	 * This method is a static class method and will affect the result
1014 	 * of editable.getContents() for all editables that have been or
1015 	 * will be constructed.
1016 	 *
1017 	 * @param {!Function} serializerFunction
1018 	 *        A function that accepts a DOM element and returns the serialized
1019 	 *        XHTML of the element contents (excluding the start and end tag of
1020 	 *        the passed element).
1021 	 * @api
1022 	 */
1023 	Aloha.Editable.setContentSerializer = function (serializerFunction) {
1024 		contentSerializer = serializerFunction;
1025 	};
1026 
1027 	/**
1028 	 * Gets the content serializer function.
1029 	 *
1030 	 * @see Aloha.Editable.setContentSerializer()
1031 	 * @api
1032 	 * @return {!Function}
1033 	 *        The serializer function.
1034 	 */
1035 	Aloha.Editable.getContentSerializer = function () {
1036 		return contentSerializer;
1037 	};
1038 
1039 	Aloha.Editable.registerEvents = registerEvents;
1040 
1041 });
1042