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