/*!
* This file is part of Aloha Editor Project http://aloha-editor.org
* Copyright © 2010-2011 Gentics Software GmbH, aloha@gentics.com
* Contributors http://aloha-editor.org/contribution.php
* Licensed unter the terms of http://www.aloha-editor.org/license.html
*
* Aloha Editor is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* ( at your option ) any later version.*
*
* Aloha Editor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
define( [
'aloha/core',
'util/class',
'aloha/jquery',
'aloha/pluginmanager',
'aloha/floatingmenu',
'aloha/selection',
'aloha/markup',
'aloha/contenthandlermanager',
'aloha/console'
], function( Aloha, Class, jQuery, PluginManager, FloatingMenu, Selection,
Markup, ContentHandlerManager, console ) {
var unescape = window.unescape,
GENTICS = window.GENTICS,
// True, if the next editable activate event should not be handled
ignoreNextActivateEvent = false;
// default supported and custom content handler settings
// @TODO move to new config when implemented in Aloha
Aloha.defaults.contentHandler = {};
Aloha.defaults.contentHandler.initEditable = [ 'sanitize' ];
Aloha.defaults.contentHandler.getContents = [ 'sanitize' ];
// The insertHtml contenthandler ( paste ) will, by default, use all
// registered content handlers.
//Aloha.defaults.contentHandler.insertHtml = void 0;
if ( typeof Aloha.settings.contentHandler === 'undefined' ) {
Aloha.settings.contentHandler = {};
}
var defaultContentSerializer = function(editableElement){
return jQuery(editableElement).html();
};
var contentSerializer = defaultContentSerializer;
/**
* Editable object
* @namespace Aloha
* @class Editable
* @method
* @constructor
* @param {Object} obj jQuery object reference to the object
*/
Aloha.Editable = Class.extend( {
_constructor: function( obj ) {
// check wheter the object has an ID otherwise generate and set
// globally unique ID
if ( !obj.attr( 'id' ) ) {
obj.attr( 'id', GENTICS.Utils.guid() );
}
// store object reference
this.obj = obj;
this.originalObj = obj;
this.ready = false;
// delimiters, timer and idle for smartContentChange
// smartContentChange triggers -- tab: '\u0009' - space: '\u0020' - enter: 'Enter'
// backspace: U+0008 - delete: U+007F
this.sccDelimiters = [ ':', ';', '.', '!', '?', ',',
unescape( '%u0009' ), unescape( '%u0020' ), unescape( '%u0008' ), unescape( '%u007F' ), 'Enter' ];
this.sccIdle = 5000;
this.sccDelay = 500;
this.sccTimerIdle = false;
this.sccTimerDelay = false;
// see keyset http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html
this.keyCodeMap = {
93 : 'Apps', // The Application key
18 : 'Alt', // The Alt ( Menu ) key.
20 : 'CapsLock', // The Caps Lock ( Capital ) key.
17 : 'Control', // The Control ( Ctrl ) key.
40 : 'Down', // The Down Arrow key.
35 : 'End', // The End key.
13 : 'Enter', // The Enter key.
112 : 'F1', // The F1 key.
113 : 'F2', // The F2 key.
114 : 'F3', // The F3 key.
115 : 'F4', // The F4 key.
116 : 'F5', // The F5 key.
117 : 'F6', // The F6 key.
118 : 'F7', // The F7 key.
119 : 'F8', // The F8 key.
120 : 'F9', // The F9 key.
121 : 'F10', // The F10 key.
122 : 'F11', // The F11 key.
123 : 'F12', // The F12 key.
// Anybody knows the keycode for F13-F24?
36 : 'Home', // The Home key.
45 : 'Insert', // The Insert ( Ins ) key.
37 : 'Left', // The Left Arrow key.
224 : 'Meta', // The Meta key.
34 : 'PageDown', // The Page Down ( Next ) key.
33 : 'PageUp', // The Page Up key.
19 : 'Pause', // The Pause key.
44 : 'PrintScreen', // The Print Screen ( PrintScrn, SnapShot ) key.
39 : 'Right', // The Right Arrow key.
145 : 'Scroll', // The scroll lock key
16 : 'Shift', // The Shift key.
38 : 'Up', // The Up Arrow key.
91 : 'Win', // The left Windows Logo key.
92 : 'Win' // The right Windows Logo key.
};
this.placeholderClass = 'aloha-placeholder';
Aloha.registerEditable( this );
this.init();
},
/**
* Initialize the editable
* @return void
* @hide
*/
init: function() {
var me = this;
// TODO make editables their own settings.
this.settings = Aloha.settings;
// smartContentChange settings
// @TODO move to new config when implemented in Aloha
if ( Aloha.settings && Aloha.settings.smartContentChange ) {
if ( Aloha.settings.smartContentChange.delimiters ) {
this.sccDelimiters = Aloha.settings.smartContentChange.delimiters;
}
if ( Aloha.settings.smartContentChange.idle ) {
this.sccIdle = Aloha.settings.smartContentChange.idle;
}
if ( Aloha.settings.smartContentChange.delay ) {
this.sccDelay = Aloha.settings.smartContentChange.delay;
}
}
// check if Aloha can handle the obj as Editable
if ( !this.check( this.obj ) ) {
//Aloha.log( 'warn', this, 'Aloha cannot handle {' + this.obj[0].nodeName + '}' );
this.destroy();
return;
}
// apply content handler to clean up content
if ( typeof Aloha.settings.contentHandler.initEditable === 'undefined' ) {
Aloha.settings.contentHandler.initEditable = Aloha.defaults.contentHandler.initEditable;
}
var content = me.obj.html();
content = ContentHandlerManager.handleContent( content, {
contenthandler: Aloha.settings.contentHandler.initEditable
} );
me.obj.html( content );
// only initialize the editable when Aloha is fully ready (including plugins)
Aloha.bind( 'aloha-ready', function() {
// initialize the object
me.obj.addClass( 'aloha-editable' ).contentEditable( true );
// add focus event to the object to activate
me.obj.mousedown( function( e ) {
// check whether the mousedown was already handled
if ( !Aloha.eventHandled ) {
Aloha.eventHandled = true;
return me.activate( e );
}
} );
me.obj.mouseup( function( e ) {
Aloha.eventHandled = false;
} );
me.obj.focus( function( e ) {
return me.activate( e );
} );
// by catching the keydown we can prevent the browser from doing its own thing
// if it does not handle the keyStroke it returns true and therefore all other
// events (incl. browser's) continue
me.obj.keydown( function( event ) {
var letEventPass = Markup.preProcessKeyStrokes( event );
me.keyCode = event.which;
if (!letEventPass) {
// the event will not proceed to key press, therefore trigger smartContentChange
me.smartContentChange( event );
}
return letEventPass;
} );
// handle keypress
me.obj.keypress( function( event ) {
// triggers a smartContentChange to get the right charcode
// To test try http://www.w3.org/2002/09/tests/keys.html
Aloha.activeEditable.smartContentChange( event );
} );
// handle shortcut keys
me.obj.keyup( function( event ) {
if ( event.keyCode === 27 ) {
Aloha.deactivateEditable();
return false;
}
} );
// register the onSelectionChange Event with the Editable field
me.obj.contentEditableSelectionChange( function( event ) {
Selection.onChange( me.obj, event );
return me.obj;
} );
// mark the editable as unmodified
me.setUnmodified();
// we don't do the sanitizing on aloha ready, since some plugins add elements into the content and bind events to it.
// if we sanitize by replacing the html, all events would get lost. TODO: think about a better solution for the sanitizing, without
// destroying the events
// // apply content handler to clean up content
// var content = me.obj.html();
// if ( typeof Aloha.settings.contentHandler.initEditable === 'undefined' ) {
// Aloha.settings.contentHandler.initEditable = Aloha.defaults.contentHandler.initEditable;
// }
// content = ContentHandlerManager.handleContent( content, {
// contenthandler: Aloha.settings.contentHandler.initEditable
// } );
// me.obj.html( content );
me.snapshotContent = me.getContents();
// FF bug: check for empty editable contents ( no
; no whitespace )
if ( jQuery.browser.mozilla ) {
me.initEmptyEditable();
}
me.initPlaceholder();
me.ready = true;
// throw a new event when the editable has been created
/**
* @event editableCreated fires after a new editable has been created, eg. via $( '#editme' ).aloha()
* The event is triggered in Aloha's global scope Aloha
* @param {Event} e the event object
* @param {Array} a an array which contains a reference to the currently created editable on its first position
*/
Aloha.trigger( 'aloha-editable-created', [ me ] );
} );
},
/**
* True, if this editable is active for editing
* @property
* @type boolean
*/
isActive: false,
/**
* stores the original content to determine if it has been modified
* @hide
*/
originalContent: null,
/**
* every time a selection is made in the current editable the selection has to
* be saved for further use
* @hide
*/
range: undefined,
/**
* Check if object can be edited by Aloha Editor
* @return {boolean } editable true if Aloha Editor can handle else false
* @hide
*/
check: function() {
/* TODO check those elements
'map', 'meter', 'object', 'output', 'progress', 'samp',
'time', 'area', 'datalist', 'figure', 'kbd', 'keygen',
'mark', 'math', 'wbr', 'area',
*/
// Extract El
var me = this,
obj = this.obj,
el = obj.get( 0 ),
nodeName = el.nodeName.toLowerCase(),
// supported elements
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' ],
i, div;
for ( i = 0; i < textElements.length; ++i ) {
if ( nodeName === textElements[ i ] ) {
return true;
}
}
// special handled elements
switch ( nodeName ) {
case 'label':
case 'button':
// TODO need some special handling.
break;
case 'textarea':
// Create a div alongside the textarea
div = jQuery( '
' )
.insertAfter( obj );
// Resize the div to the textarea and
// Populate the div with the value of the textarea
// Then, hide the textarea
div.height( obj.height() )
.width( obj.width() )
.html( obj.val() );
obj.hide();
// Attach a onsubmit to the form to place the HTML of the
// div back into the textarea
obj.parents( 'form:first' ).submit( function() {
obj.val( me.getContents() );
} );
// Swap textarea reference with the new div
this.obj = div;
// Supported
return true;
default:
break;
}
// the following elements are not supported
/*
'canvas', 'audio', 'br', 'embed', 'fieldset', 'hgroup', 'hr',
'iframe', 'img', 'input', 'map', 'script', 'select', 'style',
'svg', 'table', 'ul', 'video', 'ol', 'form', 'noscript',
*/
return false;
},
/**
* Init Placeholder
*
* @return void
*/
initPlaceholder: function() {
if ( Aloha.settings.placeholder && this.isEmpty() ) {
this.addPlaceholder();
}
},
/**
* Check if the conteneditable is empty.
*
* @return {Boolean}
*/
isEmpty: function() {
var editableTrimedContent = jQuery.trim( this.getContents() ),
onlyBrTag = ( editableTrimedContent === '
' ) ? true : false;
return ( editableTrimedContent.length === 0 || onlyBrTag );
},
/**
* Check if the editable div is not empty. Fixes a FF browser bug
* see issue: https://github.com/alohaeditor/Aloha-Editor/issues/269
*
* @return {undefined}
*/
initEmptyEditable: function( ) {
var obj = this.obj;
if ( this.empty( this.getContents() ) ) {
jQuery( obj ).prepend( '
' );
}
},
/**
* Add placeholder in editable
*
* @return void
*/
addPlaceholder: function() {
var div = jQuery( '' ),
span = jQuery( '' ),
el,
obj = this.obj;
if ( GENTICS.Utils.Dom.allowsNesting( obj[0], div[0] ) ) {
el = div;
} else {
el = span;
}
jQuery( obj ).append( el.addClass( this.placeholderClass ) );
jQuery.each(
Aloha.settings.placeholder,
function( selector, selectorConfig ) {
if ( obj.is( selector ) ) {
el.html( selectorConfig );
}
}
);
// remove browser br
jQuery( 'br', obj ).remove();
// delete div, span, el;
},
/**
* remove placeholder from contenteditable. If setCursor is true,
* will also set the cursor to the start of the selection. However,
* this will be ASYNCHRONOUS, so if you rely on the fact that
* the placeholder is removed after calling this method, setCursor
* should be false ( or not set )
*
* @return void
*/
removePlaceholder: function( obj, setCursor ) {
var placeholderClass = this.placeholderClass,
range;
// remove browser br
// jQuery( 'br', obj ).remove();
// set the cursor // remove placeholder
if ( setCursor === true ) {
range = Selection.getRangeObject();
if ( !range.select ) {
return;
}
range.startContainer = range.endContainer = obj.get( 0 );
range.startOffset = range.endOffset = 0;
range.select();
window.setTimeout( function() {
jQuery( '.' + placeholderClass, obj ).remove();
}, 20 );
} else {
jQuery( '.' + placeholderClass, obj ).remove();
}
},
/**
* destroy the editable
* @return void
*/
destroy: function() {
// leave the element just to get sure
if ( this === Aloha.getActiveEditable() ) {
this.blur();
// also hide the floating menu if the current editable was active
FloatingMenu.hide();
}
// special handled elements
switch ( this.originalObj.get( 0 ).nodeName.toLowerCase() ) {
case 'label':
case 'button':
// TODO need some special handling.
break;
case 'textarea':
// restore content to original textarea
this.originalObj.val( this.getContents() );
this.obj.remove();
this.originalObj.show();
break;
default:
break;
}
// now the editable is not ready any more
this.ready = false;
// remove the placeholder if needed.
this.removePlaceholder( this.obj );
// initialize the object and disable contentEditable
// unbind all events
// TODO should only unbind the specific handlers.
this.obj.removeClass( 'aloha-editable' )
.contentEditable( false )
.unbind( 'mousedown click dblclick focus keydown keypress keyup' );
/* TODO remove this event, it should implemented as bind and unbind
// register the onSelectionChange Event with the Editable field
this.obj.contentEditableSelectionChange( function( event ) {
Aloha.Selection.onChange( me.obj, event );
return me.obj;
} );
*/
// throw a new event when the editable has been created
/**
* @event editableCreated fires after a new editable has been destroyes, eg. via $( '#editme' ).mahalo()
* The event is triggered in Aloha's global scope Aloha
* @param {Event} e the event object
* @param {Array} a an array which contains a reference to the currently created editable on its first position
*/
Aloha.trigger( 'aloha-editable-destroyed', [ this ] );
// finally register the editable with Aloha
Aloha.unregisterEditable( this );
},
/**
* marks the editables current state as unmodified. Use this method to inform the editable
* that it's contents have been saved
* @method
*/
setUnmodified: function() {
this.originalContent = this.getContents();
},
/**
* check if the editable has been modified during the edit process#
* @method
* @return boolean true if the editable has been modified, false otherwise
*/
isModified: function() {
return this.originalContent !== this.getContents();
},
/**
* String representation of the object
* @method
* @return Aloha.Editable
*/
toString: function() {
return 'Aloha.Editable';
},
/**
* check whether the editable has been disabled
*/
isDisabled: function() {
return !this.obj.contentEditable()
|| this.obj.contentEditable() === 'false';
},
/**
* disable this editable
* a disabled editable cannot be written on by keyboard
*/
disable: function() {
return this.isDisabled() || this.obj.contentEditable( false );
},
/**
* enable this editable
* reenables a disabled editable to be writteable again
*/
enable: function() {
return this.isDisabled() && this.obj.contentEditable( true );
},
/**
* activates an Editable for editing
* disables all other active items
* @method
*/
activate: function( e ) {
// get active Editable before setting the new one.
var oldActive = Aloha.getActiveEditable();
// We need to ommit this call when this flag is set to true.
// This flag will only be set to true before the removePlaceholder method
// is called since that method invokes a focus event which will again trigger
// this method. We want to avoid double invokation of this method.
if ( ignoreNextActivateEvent ) {
ignoreNextActivateEvent = false;
return;
}
// handle special case in which a nested editable is focused by a click
// in this case the "focus" event would be triggered on the parent element
// which actually shifts the focus away to it's parent. this if is here to
// prevent this situation
if ( e && e.type === 'focus' && oldActive !== null
&& oldActive.obj.parent().get( 0 ) === e.currentTarget ) {
return;
}
// leave immediately if this is already the active editable
if ( this.isActive || this.isDisabled() ) {
// we don't want parent editables to be triggered as well, so return false
return;
}
this.obj.addClass( 'aloha-editable-active' );
Aloha.activateEditable( this );
ignoreNextActivateEvent = true;
this.removePlaceholder ( this.obj, true );
ignoreNextActivateEvent = false;
this.isActive = true;
/**
* @event editableActivated fires after the editable has been activated by clicking on it.
* This event is triggered in Aloha's global scope Aloha
* @param {Event} e the event object
* @param {Array} a an array which contains a reference to last active editable on its first position, as well
* as the currently active editable on it's second position
*/
// trigger a 'general' editableActivated event
Aloha.trigger( 'aloha-editable-activated', {
'oldActive' : oldActive,
'editable' : this
} );
},
/**
* handle the blur event
* this must not be attached to the blur event, which will trigger far too often
* eg. when a table within an editable is selected
* @hide
*/
blur: function() {
this.obj.blur();
this.isActive = false;
this.initPlaceholder();
this.obj.removeClass( 'aloha-editable-active' );
/**
* @event editableDeactivated fires after the editable has been activated by clicking on it.
* This event is triggered in Aloha's global scope Aloha
* @param {Event} e the event object
* @param {Array} a an array which contains a reference to this editable
*/
Aloha.trigger( 'aloha-editable-deactivated', { editable : this } );
/**
* @event smartContentChanged
*/
Aloha.activeEditable.smartContentChange( { type : 'blur' }, null );
},
/**
* check if the string is empty
* used for zerowidth check
* @return true if empty or string is null, false otherwise
* @hide
*/
empty: function( str ) {
// br is needed for chrome
return ( null === str )
|| ( jQuery.trim( str ) === '' || str === '
' );
},
/**
* Get the contents of this editable as a HTML string
* @method
* @return contents of the editable
*/
getContents: function( asObject ) {
var clonedObj = this.obj.clone( false );
// do core cleanup
clonedObj.find( '.aloha-cleanme' ).remove();
this.removePlaceholder( clonedObj );
PluginManager.makeClean( clonedObj );
return asObject ? clonedObj.contents() : contentSerializer(clonedObj[0]);
},
/**
* Set the contents of this editable as a HTML string
* @param content as html
* @param return as object or html string
* @return contents of the editable
*/
setContents: function( content, asObject ) {
var reactivate = null;
if ( Aloha.getActiveEditable() === this ) {
Aloha.deactivateEditable();
reactivate = this;
}
this.obj.html( content );
if ( null !== reactivate ) {
reactivate.activate();
}
this.smartContentChange({type : 'set-contents'});
return asObject ? this.obj.contents() : contentSerializer(this.obj[0]);
},
/**
* Get the id of this editable
* @method
* @return id of this editable
*/
getId: function() {
return this.obj.attr( 'id' );
},
/**
* Generates and signals a smartContentChange event.
*
* A smart content change occurs when a special editing action, or a
* combination of interactions are performed by the user during the
* course of editing within an editable.
* The smart content change event would therefore signal to any
* component that is listening to this event, that content has been
* inserted into the editable that may need to be prococessed in a
* special way
* This is used for smart actions within the content/while editing.
* @param {Event} event
* @hide
*/
smartContentChange: function( event ) {
var me = this,
uniChar = null,
re,
match;
// ignore meta keys like crtl+v or crtl+l and so on
if ( event && ( event.metaKey || event.crtlKey || event.altKey ) ) {
return false;
}
if ( event && event.originalEvent ) {
// regex to strip unicode
re = new RegExp( "U\\+(\\w{4})" );
match = re.exec( event.originalEvent.keyIdentifier );
// Use keyIdentifier if available
if ( event.originalEvent.keyIdentifier && 1 === 2 ) {
// @fixme: Because of "&& 1 === 2" above, this block is
// unreachable code
if ( match !== null ) {
uniChar = unescape( '%u' + match[1] );
}
if ( uniChar === null ) {
uniChar = event.originalEvent.keyIdentifier;
}
// FF & Opera don't support keyIdentifier
} else {
// Use among browsers reliable which http://api.jquery.com/keypress
uniChar = ( this.keyCodeMap[ this.keyCode ] ||
String.fromCharCode( event.which ) || 'unknown' );
}
}
// handle "Enter" -- it's not "U+1234" -- when returned via "event.originalEvent.keyIdentifier"
// reference: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html
if ( jQuery.inArray( uniChar, this.sccDelimiters ) >= 0 ) {
clearTimeout( this.sccTimerIdle );
clearTimeout( this.sccTimerDelay );
this.sccTimerDelay = setTimeout( function() {
Aloha.trigger( 'aloha-smart-content-changed', {
'editable' : me,
'keyIdentifier' : event.originalEvent.keyIdentifier,
'keyCode' : event.keyCode,
'char' : uniChar,
'triggerType' : 'keypress', // keypress, timer, blur, paste
'snapshotContent' : me.getSnapshotContent()
} );
console.debug( 'Aloha.Editable',
'smartContentChanged: event type keypress triggered' );
/*
var r = Aloha.Selection.rangeObject;
if ( r.isCollapsed() && r.startContainer.nodeType == 3 ) {
var posDummy = jQuery( '' );
GENTICS.Utils.Dom.insertIntoDOM(
posDummy,
r,
this.obj,
null,
false,
false
);
console.log( posDummy.offset().top, posDummy.offset().left );
GENTICS.Utils.Dom.removeFromDOM(
posDummy,
r,
false
);
r.select();
}
*/
}, this.sccDelay );
} else if ( event && event.type === 'paste' ) {
Aloha.trigger( 'aloha-smart-content-changed', {
'editable' : me,
'keyIdentifier' : null,
'keyCode' : null,
'char' : null,
'triggerType' : 'paste',
'snapshotContent' : me.getSnapshotContent()
} );
} else if ( event && event.type === 'blur' ) {
Aloha.trigger( 'aloha-smart-content-changed', {
'editable' : me,
'keyIdentifier' : null,
'keyCode' : null,
'char' : null,
'triggerType' : 'blur',
'snapshotContent' : me.getSnapshotContent()
} );
} else if ( uniChar !== null ) {
// in the rare case idle time is lower then delay time
clearTimeout( this.sccTimerDelay );
clearTimeout( this.sccTimerIdle );
this.sccTimerIdle = setTimeout( function() {
Aloha.trigger( 'aloha-smart-content-changed', {
'editable' : me,
'keyIdentifier' : null,
'keyCode' : null,
'char' : null,
'triggerType' : 'idle',
'snapshotContent' : me.getSnapshotContent()
} );
}, this.sccIdle );
}
},
/**
* Get a snapshot of the active editable as a HTML string
* @hide
* @return snapshot of the editable
*/
getSnapshotContent: function() {
var ret = this.snapshotContent;
this.snapshotContent = this.getContents();
return ret;
}
} );
/**
* Sets the serializer function to be used for the contents of all editables.
*
* The default content serializer will just call the jQuery.html()
* function on the editable element (which gets the innerHTML property).
*
* This method is a static class method and will affect the result
* of editable.getContents() for all editables that have been or
* will be constructed.
*
* @param serializerFunction
* A function that accepts a DOM element and returns the serialized
* XHTML of the element contents (excluding the start and end tag of
* the passed element).
*/
Aloha.Editable.setContentSerializer = function( serializerFunction ) {
contentSerializer = serializerFunction;
};
} );