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