vendor/assets/javascripts/aloha/lib/aloha/markup.js in locomotive-aloha-rails-0.20.1.5 vs vendor/assets/javascripts/aloha/lib/aloha/markup.js in locomotive-aloha-rails-0.23.2.1
- old
+ new
@@ -1,1255 +1,1272 @@
-/*!
-* 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 <http://www.gnu.org/licenses/>.
-*/
-
-define([
- 'aloha/core',
- 'util/class',
- 'aloha/jquery',
- 'aloha/ecma5shims'
-],
-function( Aloha, Class, jQuery, shims ) {
-
-
-
-var GENTICS = window.GENTICS;
-
-var isOldIE = !!( jQuery.browser.msie &&
- 9 > parseInt( jQuery.browser.version, 10 ) );
-
-function isBR( node ) {
- return 'BR' === node.nodeName;
-}
-
-function isBlock( node ) {
- return 'false' === jQuery( node ).attr( 'contenteditable' );
-}
-
-function isTextNode( node ) {
- return node && 3 === node.nodeType; // Node.TEXT_NODE
-}
-
-function nodeLength( node ) {
- return !node ? 0
- : ( isTextNode( node ) ? node.length
- : node.childNodes.length );
-}
-
-function nextVisibleNode( node ) {
- if ( !node ) {
- return null;
- }
-
- if ( node.nextSibling ) {
- // Skip over nodes that the user cannot see ...
- if ( isTextNode( node.nextSibling ) &&
- !isVisibleTextNode( node.nextSibling ) ) {
- return nextVisibleNode( node.nextSibling );
- }
-
- // Skip over propping <br>s ...
- if ( isBR( node.nextSibling ) &&
- node.nextSibling === node.parentNode.lastChild ) {
- return nextVisibleNode( node.nextSibling );
- }
-
- // Skip over empty editable elements ...
- if ( '' === node.nextSibling.innerHTML &&
- !isBlock( node.nextSibling ) ) {
- return nextVisibleNode( node.nextSibling );
- }
-
- return node.nextSibling;
- }
-
- if ( node.parentNode ) {
- return nextVisibleNode( node.parentNode );
- }
-
- return null;
-}
-
-function prevVisibleNode( node ) {
- if ( !node ) {
- return null;
- }
-
- if ( node.previousSibling ) {
- // Skip over nodes that the user cannot see...
- if ( isTextNode( node.previousSibling ) &&
- !isVisibleTextNode( node.previousSibling ) ) {
- return prevVisibleNode( node.previousSibling );
- }
-
- // Skip over empty editable elements ...
- if ( '' === node.previousSibling.innerHTML &&
- !isBlock( node.previousSibling ) ) {
- return prevVisibleNode( node.previouSibling );
- }
-
- return node.previousSibling;
- }
-
- if ( node.parentNode ) {
- return prevVisibleNode( node.parentNode );
- }
-
- return null;
-}
-
-/**
- * Determines whether the given text node is visible to the the user,
- * based on our understanding that browsers will not display
- * superfluous white spaces.
- *
- * @param {HTMLEmenent} node The text node to be checked.
- */
-function isVisibleTextNode( node ) {
- return 0 < node.data.replace( /\s+/g, '' ).length;
-}
-
-function isFrontPosition( node, offset ) {
- return ( 0 === offset ) ||
- ( offset <= node.data.length -
- node.data.replace( /^\s+/, '' ).length );
-}
-
-function isBlockInsideEditable( $block ) {
- return $block.parent().hasClass( 'aloha-editable' );
-}
-
-function isEndPosition( node, offset ) {
- var length = nodeLength( node );
-
- if ( length === offset ) {
- return true;
- }
-
- var isText = isTextNode( node );
-
- // If within a text node, then ignore superfluous white-spaces,
- // since they are invisible to the user.
- if ( isText &&
- node.data.replace( /\s+$/, '' ).length === offset ) {
- return true;
- }
-
- if ( 1 === length && !isText ) {
- return isBR( node.childNodes[0] );
- }
-
- return false;
-}
-
-function blink( node ) {
- jQuery( node )
- .stop( true )
- .css({ opacity: 0 })
- .fadeIn( 0 ).delay( 100 )
- .fadeIn(function () {
- jQuery( node ).css({ opacity: 1 });
- });
-
- return node;
-}
-
-/**
- * @TODO(petro): We need to be more intelligent about whether we insert a
- * block-level placeholder or a phrasing level element.
- * @TODO(petro): test with <pre>
- */
-function jumpBlock( block, isGoingLeft ) {
- var range = new GENTICS.Utils.RangeObject();
- var sibling = isGoingLeft ? prevVisibleNode( block )
- : nextVisibleNode( block );
-
- if ( !sibling || isBlock( sibling ) ) {
- var $landing = jQuery( '<div> </div>' );
-
- if ( isGoingLeft ) {
- jQuery( block ).before( $landing );
- } else {
- jQuery( block ).after( $landing );
- }
-
- range.startContainer = range.endContainer = $landing[0];
- range.startOffset = range.endOffset = 0;
-
- // Clear out any old placeholder first ...
- cleanupPlaceholders( range );
-
- window.$_alohaPlaceholder = $landing;
- } else {
- range.startContainer = range.endContainer = sibling;
- range.startOffset = range.endOffset = isGoingLeft ?
- nodeLength( sibling ) : ( isOldIE ? 1 : 0 );
-
- cleanupPlaceholders( range );
- }
-
- range.select();
-
- Aloha.trigger( 'aloha-block-selected', block );
- Aloha.Selection.preventSelectionChanged();
-}
-
-function nodeContains( node1, node2 ) {
- return isOldIE ? ( shims.compareDocumentPosition( node1, node2 ) & 16 )
- : 0 < jQuery( node1 ).find( node2 ).length;
-}
-
-function isInsidePlaceholder( range ) {
- var start = range.startContainer;
- var end = range.endContainer;
- var $placeholder = window.$_alohaPlaceholder;
-
- return $placeholder.is( start ) ||
- $placeholder.is( end ) ||
- nodeContains( $placeholder[0], start ) ||
- nodeContains( $placeholder[0], end );
-}
-
-function cleanupPlaceholders( range ) {
- if ( window.$_alohaPlaceholder && !isInsidePlaceholder( range ) ) {
- if ( 0 === window.$_alohaPlaceholder.html()
- .replace( /^( )*$/, '' ).length ) {
- window.$_alohaPlaceholder.remove();
- }
-
- window.$_alohaPlaceholder = null;
- }
-}
-
-/**
- * Markup object
- */
-Aloha.Markup = Class.extend( {
-
- /**
- * Key handlers for special key codes
- */
- keyHandlers: {},
-
- /**
- * Add a key handler for the given key code
- * @param keyCode key code
- * @param handler handler function
- */
- addKeyHandler: function( keyCode, handler ) {
- if ( !this.keyHandlers[ keyCode ] ) {
- this.keyHandlers[ keyCode ] = [];
- }
-
- this.keyHandlers[ keyCode ].push( handler );
- },
-
- insertBreak: function() {
- var range = Aloha.Selection.rangeObject,
- onWSIndex,
- nextTextNode,
- newBreak;
-
- if ( !range.isCollapsed() ) {
- this.removeSelectedMarkup();
- }
-
- newBreak = jQuery( '<br/>' );
- GENTICS.Utils.Dom.insertIntoDOM( newBreak, range, Aloha.activeEditable.obj );
-
- nextTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
- newBreak.parent().get( 0 ),
- GENTICS.Utils.Dom.getIndexInParent( newBreak.get( 0 ) ) + 1,
- false
- );
-
- if ( nextTextNode ) {
- // trim leading whitespace
- nonWSIndex = nextTextNode.data.search( /\S/ );
- if ( nonWSIndex > 0 ) {
- nextTextNode.data = nextTextNode.data.substring( nonWSIndex );
- }
- }
-
- range.startContainer = range.endContainer = newBreak.get( 0 ).parentNode;
- range.startOffset = range.endOffset = GENTICS.Utils.Dom.getIndexInParent( newBreak.get( 0 ) ) + 1;
- range.correctRange();
- range.clearCaches();
- range.select();
- },
-
- /**
- * first method to handle key strokes
- * @param event DOM event
- * @param rangeObject as provided by Aloha.Selection.getRangeObject();
- * @return "Aloha.Selection"
- */
- preProcessKeyStrokes: function( event ) {
- if ( event.type !== 'keydown' ) {
- return false;
- }
-
- var rangeObject = Aloha.Selection.rangeObject,
- handlers,
- i;
-
- if ( this.keyHandlers[ event.keyCode ] ) {
- handlers = this.keyHandlers[ event.keyCode ];
- for ( i = 0; i < handlers.length; ++i ) {
- if ( !handlers[i]( event ) ) {
- return false;
- }
- }
- }
-
- // LEFT (37), RIGHT (39) keys for block detection
- if ( event.keyCode === 37 || event.keyCode === 39 ) {
- if ( this.processCursor( rangeObject, event.keyCode ) ) {
- cleanupPlaceholders( Aloha.Selection.rangeObject );
- return true;
- }
-
- return false;
- }
-
- // BACKSPACE
- if ( event.keyCode === 8 ) {
- event.preventDefault(); // prevent history.back() even on exception
- Aloha.execCommand( 'delete', false );
- return false;
- }
-
- // DELETE
- if ( event.keyCode === 46 ) {
- Aloha.execCommand( 'forwarddelete', false );
- return false;
- }
-
- // ENTER
- if ( event.keyCode === 13 ) {
- if ( event.shiftKey ) {
- Aloha.execCommand( 'insertlinebreak', false );
- return false;
- } else {
- Aloha.execCommand( 'insertparagraph', false );
- return false;
- }
- }
-
- return true;
- },
-
- /**
- * Processing of cursor keys.
- * Detect blocks (elements with contenteditable=false) and will select them
- * (normally the cursor would simply jump right past them).
- *
- * For each block that is selected, an 'aloha-block-selected' event will be
- * triggered.
- *
- * @param {RangyRange} range A range object for the current selection.
- * @param {number} keyCode Code of the currently pressed key.
- * @return {boolean} False if a block was found, to prevent further events,
- * true otherwise.
- */
- processCursor: function( range, keyCode ) {
- if ( !range.isCollapsed() ) {
- return true;
- }
-
- var node = range.startContainer;
-
- if ( !node ) {
- return true;
- }
-
- var sibling;
-
- // Versions of Internet Explorer that are older that 9, will
- // erroneously allow you to enter and edit inside elements which have
- // their contenteditable attribute set to false...
- if ( isOldIE ) {
- var $parentBlock = jQuery( node ).parents(
- '[contenteditable=false]' );
- var isInsideBlock = $parentBlock.length > 0;
-
- if ( isInsideBlock ) {
- if ( isBlockInsideEditable( $parentBlock ) ) {
- sibling = $parentBlock[0];
- } else {
- return true;
- }
- }
- }
-
- if ( !sibling ) {
- // True if keyCode denotes LEFT or UP arrow key, otherwise they
- // keyCode is for RIGHT or DOWN in which this value will be false.
- var isLeft = (37 === keyCode || 38 === keyCode);
- var offset = range.startOffset;
-
- if ( isTextNode( node ) ) {
- if ( isLeft ) {
- // FIXME(Petro): Please consider if you have a better idea
- // of how we can work around this.
- //
- // Here is the problem... with Internet Explorer:
- // ----------------------------------------------
- //
- // Versions of Internet Explorer older than 9, are buggy in
- // how they `select()', or position a selection from cursor
- // movements, when the following conditions are true:
- //
- // * The range is collapsed.
- // * startContainer is a contenteditable text node.
- // * startOffset is 1.
- // * There is a non-conenteditable element left of the
- // startContainer.
- // * You attempt to move left to offset 0 (we consider a
- // range to be at "frontposition" if it is at offset 0
- // within its startContainer).
- //
- // What happens in IE 7, and IE 8, is that the selection
- // will jump to the adjacent non-contenteditable
- // element(s), instead moving to the front of the
- // container, and the offset will be stuck at 1--even as
- // the cursor is jumping around the screen!
- //
- // Our imperfect work-around is to reckon ourselves to be
- // at the front of the next node (ie: offset 0 in other
- // browsers), as soon as we detect that we are at offset 1
- // in IEv<9.
- //
- // Considering the bug, I think this is acceptable because
- // the user can still position themselve right between the
- // block (non-contenteditable element) and the first
- // characater of the text node by clicking there with the
- // mouse, since this seems to work fine in all IE versions.
- var isFrontPositionInIE = isOldIE && 1 === offset;
-
- if ( !isFrontPositionInIE &&
- !isFrontPosition( node, offset ) ) {
- return true;
- }
-
- } else if ( !isEndPosition( node, offset ) ) {
- return true;
- }
-
- } else {
- node = node.childNodes[
- offset === nodeLength( node ) ? offset - 1 : offset ];
- }
-
- sibling = isLeft ? prevVisibleNode( node )
- : nextVisibleNode( node );
- }
-
- if ( isBlock( sibling ) ) {
- jumpBlock( sibling, isLeft );
- return false;
- }
-
- return true;
- },
-
- /**
- * method handling shiftEnter
- * @param Aloha.Selection.SelectionRange of the current selection
- * @return void
- */
- processShiftEnter: function( rangeObject ) {
- this.insertHTMLBreak( rangeObject.getSelectionTree(), rangeObject );
- },
-
- /**
- * method handling Enter
- * @param Aloha.Selection.SelectionRange of the current selection
- * @return void
- */
- processEnter: function( rangeObject ) {
- if ( rangeObject.splitObject ) {
- // now comes a very evil hack for ie, when the enter is pressed in a text node in an li element, we just append an empty text node
- // if ( jQuery.browser.msie
- // && GENTICS.Utils.Dom
- // .isListElement( rangeObject.splitObject ) ) {
- // jQuery( rangeObject.splitObject ).append(
- // jQuery( document.createTextNode( '' ) ) );
- // }
- this.splitRangeObject( rangeObject );
- } else { // if there is no split object, the Editable is the paragraph type itself (e.g. a p or h2)
- this.insertHTMLBreak( rangeObject.getSelectionTree(), rangeObject );
- }
- },
-
- /**
- * Insert the given html markup at the current selection
- * @param html html markup to be inserted
- */
- insertHTMLCode: function( html ) {
- var rangeObject = Aloha.Selection.rangeObject;
- this.insertHTMLBreak( rangeObject.getSelectionTree(), rangeObject, jQuery( html ) );
- },
-
- /**
- * insert an HTML Break <br /> into current selection
- * @param Aloha.Selection.SelectionRange of the current selection
- * @return void
- */
- insertHTMLBreak: function( selectionTree, rangeObject, inBetweenMarkup ) {
- var i,
- treeLength,
- el,
- jqEl,
- jqElBefore,
- jqElAfter,
- tmpObject,
- offset,
- checkObj;
-
- inBetweenMarkup = inBetweenMarkup ? inBetweenMarkup: jQuery( '<br/>' );
-
- for ( i = 0, treeLength = selectionTree.length; i < treeLength; ++i ) {
- el = selectionTree[ i ];
- jqEl = el.domobj ? jQuery( el.domobj ) : undefined;
-
- if ( el.selection !== 'none' ) { // before cursor, leave this part inside the splitObject
- if ( el.selection == 'collapsed' ) {
- // collapsed selection found (between nodes)
- if ( i > 0 ) {
- // not at the start, so get the element to the left
- jqElBefore = jQuery( selectionTree[ i - 1 ].domobj );
-
- // and insert the break after it
- jqElBefore.after( inBetweenMarkup );
-
- } else {
- // at the start, so get the element to the right
- jqElAfter = jQuery( selectionTree[1].domobj );
-
- // and insert the break before it
- jqElAfter.before( inBetweenMarkup );
- }
-
- // now set the range
- rangeObject.startContainer = rangeObject.endContainer = inBetweenMarkup[0].parentNode;
- rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent( inBetweenMarkup[0] ) + 1;
- rangeObject.correctRange();
-
- } else if ( el.domobj && el.domobj.nodeType === 3 ) { // textNode
- // when the textnode is immediately followed by a blocklevel element (like p, h1, ...) we need to add an additional br in between
- if ( el.domobj.nextSibling
- && el.domobj.nextSibling.nodeType == 1
- && Aloha.Selection.replacingElements[
- el.domobj.nextSibling.nodeName.toLowerCase()
- ] ) {
- // TODO check whether this depends on the browser
- jqEl.after( '<br/>' );
- }
-
- if ( this.needEndingBreak() ) {
- // when the textnode is the last inside a blocklevel element
- // (like p, h1, ...) we need to add an additional br as very
- // last object in the blocklevel element
- checkObj = el.domobj;
-
- while ( checkObj ) {
- if ( checkObj.nextSibling ) {
- checkObj = false;
- } else {
- // go to the parent
- checkObj = checkObj.parentNode;
-
- // found a blocklevel or list element, we are done
- if ( GENTICS.Utils.Dom.isBlockLevelElement( checkObj )
- || GENTICS.Utils.Dom.isListElement( checkObj ) ) {
- break;
- }
-
- // reached the limit object, we are done
- if ( checkObj === rangeObject.limitObject ) {
- checkObj = false;
- }
- }
- }
-
- // when we found a blocklevel element, insert a break at the
- // end. Mark the break so that it is cleaned when the
- // content is fetched.
- if ( checkObj ) {
- jQuery( checkObj ).append( '<br class="aloha-cleanme" />' );
- }
- }
-
- // insert the break
- jqEl.between( inBetweenMarkup, el.startOffset );
-
- // correct the range
- // count the number of previous siblings
- offset = 0;
- tmpObject = inBetweenMarkup[0];
- while ( tmpObject ) {
- tmpObject = tmpObject.previousSibling;
- ++offset;
- }
-
- rangeObject.startContainer = inBetweenMarkup[0].parentNode;
- rangeObject.endContainer = inBetweenMarkup[0].parentNode;
- rangeObject.startOffset = offset;
- rangeObject.endOffset = offset;
- rangeObject.correctRange();
-
- } else if ( el.domobj && el.domobj.nodeType === 1 ) { // other node, normally a break
- if ( jqEl.parent().find( 'br.aloha-ephemera' ).length === 0 ) {
- // but before putting it, remove all:
- jQuery( rangeObject.limitObject ).find( 'br.aloha-ephemera' ).remove();
-
- // now put it:
- jQuery( rangeObject.commonAncestorContainer )
- .append( this.getFillUpElement( rangeObject.splitObject ) );
- }
-
- jqEl.after( inBetweenMarkup );
-
- // now set the selection. Since we just added one break do the currect el
- // the new position must be el's position + 1. el's position is the index
- // of the el in the selection tree, which is i. then we must add
- // another +1 because we want to be AFTER the object, not before. therefor +2
- rangeObject.startContainer = rangeObject.commonAncestorContainer;
- rangeObject.endContainer = rangeObject.startContainer;
- rangeObject.startOffset = i + 2;
- rangeObject.endOffset = i + 2;
- rangeObject.update();
- }
- }
- }
- rangeObject.select();
- },
-
- /**
- * Check whether blocklevel elements need breaks at the end to visibly render a newline
- * @return true if an ending break is necessary, false if not
- */
- needEndingBreak: function() {
- // currently, all browser except IE need ending breaks
- return !jQuery.browser.msie;
- },
-
- /**
- * Get the currently selected text or false if nothing is selected (or the selection is collapsed)
- * @return selected text
- */
- getSelectedText: function() {
- var rangeObject = Aloha.Selection.rangeObject;
-
- if ( rangeObject.isCollapsed() ) {
- return false;
- }
-
- return this.getFromSelectionTree( rangeObject.getSelectionTree(), true );
- },
-
- /**
- * Recursive function to get the selected text from the selection tree starting at the given level
- * @param selectionTree array of selectiontree elements
- * @param astext true when the contents shall be fetched as text, false for getting as html markup
- * @return selected text from that level (incluiding all sublevels)
- */
- getFromSelectionTree: function( selectionTree, astext ) {
- var text = '', i, treeLength, el, clone;
- for ( i = 0, treeLength = selectionTree.length; i < treeLength; i++ ) {
- el = selectionTree[i];
- if ( el.selection == 'partial' ) {
- if ( el.domobj.nodeType === 3 ) {
- // partial text node selected, get the selected part
- text += el.domobj.data.substring( el.startOffset, el.endOffset );
- } else if ( el.domobj.nodeType === 1 && el.children ) {
- // partial element node selected, do the recursion into the children
- if ( astext ) {
- text += this.getFromSelectionTree( el.children, astext );
- } else {
- // when the html shall be fetched, we create a clone of the element and remove all the children
- clone = jQuery( el.domobj ).clone( false ).empty();
- // then we do the recursion and add the selection into the clone
- clone.html( this.getFromSelectionTree( el.children, astext ) );
- // finally we get the html of the clone
- text += clone.outerHTML();
- }
- }
- } else if ( el.selection == 'full' ) {
- if ( el.domobj.nodeType === 3 ) {
- // full text node selected, get the text
- text += jQuery( el.domobj ).text();
- } else if ( el.domobj.nodeType === 1 && el.children ) {
- // full element node selected, get the html of the node and all children
- text += astext ? jQuery( el.domobj ).text() : jQuery( el.domobj ).outerHTML();
- }
- }
- }
-
- return text;
- },
-
- /**
- * Get the currently selected markup or false if nothing is selected (or the selection is collapsed)
- * @return {?String}
- */
- getSelectedMarkup: function() {
- var rangeObject = Aloha.Selection.rangeObject;
- return rangeObject.isCollapsed() ? null
- : this.getFromSelectionTree( rangeObject.getSelectionTree(), false );
- },
-
- /**
- * Remove the currently selected markup
- */
- removeSelectedMarkup: function() {
- var rangeObject = Aloha.Selection.rangeObject, newRange;
-
- if ( rangeObject.isCollapsed() ) {
- return;
- }
-
- newRange = new Aloha.Selection.SelectionRange();
- // remove the selection
- this.removeFromSelectionTree( rangeObject.getSelectionTree(), newRange );
-
- // do a cleanup now (starting with the commonancestorcontainer)
- newRange.update();
- GENTICS.Utils.Dom.doCleanup( { 'merge' : true, 'removeempty' : true }, Aloha.Selection.rangeObject );
- Aloha.Selection.rangeObject = newRange;
-
- // need to set the collapsed selection now
- newRange.correctRange();
- newRange.update();
- newRange.select();
- Aloha.Selection.updateSelection();
- },
-
- /**
- * Recursively remove the selected items, starting with the given level in the selectiontree
- * @param selectionTree current level of the selectiontree
- * @param newRange new collapsed range to be set after the removal
- */
- removeFromSelectionTree: function( selectionTree, newRange ) {
- // remember the first found partially selected element node (in case we need
- // to merge it with the last found partially selected element node)
- var firstPartialElement,
- newdata,
- i,
- el,
- adjacentTextNode,
- treeLength;
-
- // iterate through the selection tree
- for ( i = 0, treeLength = selectionTree.length; i < treeLength; i++ ) {
- el = selectionTree[ i ];
-
- // check the type of selection
- if ( el.selection == 'partial' ) {
- if ( el.domobj.nodeType === 3 ) {
- // partial text node selected, so remove the selected portion
- newdata = '';
- if ( el.startOffset > 0 ) {
- newdata += el.domobj.data.substring( 0, el.startOffset );
- }
- if ( el.endOffset < el.domobj.data.length ) {
- newdata += el.domobj.data.substring( el.endOffset, el.domobj.data.length );
- }
- el.domobj.data = newdata;
-
- // eventually set the new range (if not done before)
- if ( !newRange.startContainer ) {
- newRange.startContainer = newRange.endContainer = el.domobj;
- newRange.startOffset = newRange.endOffset = el.startOffset;
- }
- } else if ( el.domobj.nodeType === 1 && el.children ) {
- // partial element node selected, so do the recursion into the children
- this.removeFromSelectionTree( el.children, newRange );
-
- if ( firstPartialElement ) {
- // when the first parially selected element is the same type
- // of element, we need to merge them
- if ( firstPartialElement.nodeName == el.domobj.nodeName ) {
- // merge the nodes
- jQuery( firstPartialElement ).append( jQuery( el.domobj ).contents() );
-
- // and remove the latter one
- jQuery( el.domobj ).remove();
- }
-
- } else {
- // remember this element as first partially selected element
- firstPartialElement = el.domobj;
- }
- }
-
- } else if ( el.selection == 'full' ) {
- // eventually set the new range (if not done before)
- if ( !newRange.startContainer ) {
- adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
- el.domobj.parentNode,
- GENTICS.Utils.Dom.getIndexInParent( el.domobj ) + 1,
- false,
- { 'blocklevel' : false }
- );
-
- if ( adjacentTextNode ) {
- newRange.startContainer = newRange.endContainer = adjacentTextNode;
- newRange.startOffset = newRange.endOffset = 0;
- } else {
- newRange.startContainer = newRange.endContainer = el.domobj.parentNode;
- newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent( el.domobj ) + 1;
- }
- }
-
- // full node selected, so just remove it (will also remove all children)
- jQuery( el.domobj ).remove();
- }
- }
- },
-
- /**
- * split passed rangeObject without or with optional markup
- * @param Aloha.Selection.SelectionRange of the current selection
- * @param markup object (jQuery) to insert in between the split elements
- * @return void
- */
- splitRangeObject: function( rangeObject, markup ) {
- // UAAAA: first check where the markup can be inserted... *grrrrr*, then decide where to split
- // object which is split up
- var
- splitObject = jQuery( rangeObject.splitObject ),
- selectionTree, insertAfterObject, followUpContainer;
-
- // update the commonAncestor with the splitObject (so that the selectionTree is correct)
- rangeObject.update( rangeObject.splitObject ); // set the splitObject as new commonAncestorContainer and update the selectionTree
-
- // calculate the selection tree. NOTE: it is necessary to do this before
- // getting the followupcontainer, since getting the selection tree might
- // possibly merge text nodes, which would lead to differences in the followupcontainer
- selectionTree = rangeObject.getSelectionTree();
-
- // object to be inserted after the splitObject
- followUpContainer = this.getSplitFollowUpContainer( rangeObject );
-
- // now split up the splitObject into itself AND the followUpContainer
- this.splitRangeObjectHelper( selectionTree, rangeObject, followUpContainer ); // split the current object into itself and the followUpContainer
-
- // check whether the followupcontainer is still marked for removal
- if ( followUpContainer.hasClass( 'preparedForRemoval' ) ) {
- // TODO shall we just remove the class or shall we not use the followupcontainer?
- followUpContainer.removeClass( 'preparedForRemoval' );
- }
-
- // now let's find the place, where the followUp is inserted afterwards. normally that's the splitObject itself, but in
- // some cases it might be their parent (e.g. inside a list, a <p> followUp must be inserted outside the list)
- insertAfterObject = this.getInsertAfterObject( rangeObject, followUpContainer );
-
- // now insert the followUpContainer
- jQuery( followUpContainer ).insertAfter( insertAfterObject ); // attach the followUpContainer right after the insertAfterObject
-
- // in some cases, we want to remove the "empty" splitObject (e.g. LIs, if enter was hit twice)
- if ( rangeObject.splitObject.nodeName.toLowerCase() === 'li' && !Aloha.Selection.standardTextLevelSemanticsComparator( rangeObject.splitObject, followUpContainer ) ) {
- jQuery( rangeObject.splitObject ).remove();
- }
-
- rangeObject.startContainer = null;
- // first check whether the followUpContainer starts with a <br/>
- // if so, place the cursor right before the <br/>
- var followContents = followUpContainer.contents();
- if ( followContents.length > 0
- && followContents.get( 0 ).nodeType == 1
- && followContents.get( 0 ).nodeName.toLowerCase() === 'br' ) {
- rangeObject.startContainer = followUpContainer.get( 0 );
- }
-
- if ( !rangeObject.startContainer ) {
- // find a possible text node in the followUpContainer and set the selection to it
- // if no textnode is available, set the selection to the followup container itself
- rangeObject.startContainer = followUpContainer.textNodes( true, true ).first().get( 0 );
- }
- if ( !rangeObject.startContainer ) { // if no text node was found, select the parent object of <br class="aloha-ephemera" />
- rangeObject.startContainer = followUpContainer.textNodes( false ).first().parent().get( 0 );
- }
- if ( rangeObject.startContainer ) {
- // the cursor is always at the beginning of the followUp
- rangeObject.endContainer = rangeObject.startContainer;
- rangeObject.startOffset = 0;
- rangeObject.endOffset = 0;
- } else {
- rangeObject.startContainer = rangeObject.endContainer = followUpContainer.parent().get( 0 );
- rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent( followUpContainer.get( 0 ) );
- }
-
- // finally update the range object again
- rangeObject.update();
-
- // now set the selection
- rangeObject.select();
- },
-
- /**
- * method to get the object after which the followUpContainer can be inserted during splitup
- * this is a helper method, not needed anywhere else
- * @param rangeObject Aloha.Selection.SelectionRange of the current selection
- * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
- * @return object after which the followUpContainer can be inserted
- */
- getInsertAfterObject: function( rangeObject, followUpContainer ) {
- var passedSplitObject, i, el;
-
- for ( i = 0; i < rangeObject.markupEffectiveAtStart.length; i++ ) {
- el = rangeObject.markupEffectiveAtStart[ i ];
-
- // check if we have already passed the splitObject (some other markup might come before)
- if ( el === rangeObject.splitObject ) {
- passedSplitObject = true;
- }
-
- // if not passed splitObject, skip this markup
- if ( !passedSplitObject ) {
- continue;
- }
-
- // once we are passed, check if the followUpContainer is allowed to be inserted into the currents el's parent
- if ( Aloha.Selection.canTag1WrapTag2( jQuery( el ).parent()[0].nodeName, followUpContainer[0].nodeName ) ) {
- return el;
- }
- }
-
- return false;
- },
-
- /**
- * @fixme: Someone who knows what this function does, please refactor it.
- * 1. splitObject arg is not used at all
- * 2. Would be better to use ternary operation would be better than if else statement
- *
- * method to get the html code for a fillUpElement. this is needed for empty paragraphs etc., so that they take up their expected height
- * @param splitObject split object (dom object)
- * @return fillUpElement HTML Code
- */
- getFillUpElement: function( splitObject ) {
- if ( jQuery.browser.msie ) {
- return false;
- } else {
- return jQuery( '<br class="aloha-cleanme"/>' );
- }
- },
-
- /**
- * removes textNodes from passed array, which only contain contentWhiteSpace (e.g. a \n between two tags)
- * @param domArray array of domObjects
- * @return void
- */
- removeElementContentWhitespaceObj: function( domArray ) {
- var correction = 0,
- removeLater = [],
- i,
- el, removeIndex;
-
- for ( i = 0; i < domArray.length; ++i ) {
- el = domArray[ i ];
- if ( el.isElementContentWhitespace ) {
- removeLater[ removeLater.length ] = i;
- }
- }
-
- for ( i = 0; i < removeLater.length; ++i ) {
- removeIndex = removeLater[ i ];
- domArray.splice( removeIndex - correction, 1 );
- ++correction;
- }
- },
-
- /**
- * recursive method to parallelly walk through two dom subtrees, leave elements before startContainer in first subtree and move rest to other
- * @param selectionTree tree to iterate over as contained in rangeObject. must be passed separately to allow recursion in the selection tree, but not in the rangeObject
- * @param rangeObject Aloha.Selection.SelectionRange of the current selection
- * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
- * @param inBetweenMarkup jQuery object to be inserted between the two split parts. will be either a <br> (if no followUpContainer is passed) OR e.g. a table, which must be inserted between the splitobject AND the follow up
- * @return void
- */
- splitRangeObjectHelper: function( selectionTree, rangeObject,
- followUpContainer, inBetweenMarkup ) {
- if ( !followUpContainer ) {
- Aloha.Log.warn( this, 'no followUpContainer, no inBetweenMarkup, nothing to do...' );
- }
-
- var fillUpElement = this.getFillUpElement( rangeObject.splitObject ),
- splitObject = jQuery( rangeObject.splitObject ),
- startMoving = false,
- el,
- i,
- completeText,
- jqObj,
- mirrorLevel,
- parent,
- treeLength;
-
- if ( selectionTree.length > 0 ) {
- mirrorLevel = followUpContainer.contents();
-
- // if length of mirrorLevel and selectionTree are not equal, the mirrorLevel must be corrected. this happens, when the mirrorLevel contains whitespace textNodes
- if ( mirrorLevel.length !== selectionTree.length ) {
- this.removeElementContentWhitespaceObj( mirrorLevel );
- }
-
- for ( i = 0, treeLength = selectionTree.length; i < treeLength; ++i ) {
- el = selectionTree[ i ];
-
- // remove all objects in the mirrorLevel, which are BEFORE the cursor
- // OR if the cursor is at the last position of the last Textnode (causing an empty followUpContainer to be appended)
- if ( ( el.selection === 'none' && startMoving === false ) ||
- ( el.domobj && el.domobj.nodeType === 3
- && el === selectionTree[ ( selectionTree.length - 1 ) ]
- && el.startOffset === el.domobj.data.length ) ) {
- // iteration is before cursor, leave this part inside the splitObject, remove from followUpContainer
- // however if the object to remove is the last existing textNode within the followUpContainer, insert a BR instead
- // otherwise the followUpContainer is invalid and takes up no vertical space
-
- if ( followUpContainer.textNodes().length > 1
- || ( el.domobj.nodeType === 1 && el.children.length === 0 ) ) {
- // note: the second part of the if (el.domobj.nodeType === 1 && el.children.length === 0) covers a very special condition,
- // where an empty tag is located right before the cursor when pressing enter. In this case the empty tag would not be
- // removed correctly otherwise
- mirrorLevel.eq( i ).remove();
-
- } else if ( GENTICS.Utils.Dom.isSplitObject( followUpContainer[0] ) ) {
- if ( fillUpElement ) {
- followUpContainer.html( fillUpElement ); // for your zoological german knowhow: ephemera = Eintagsfliege
- } else {
- followUpContainer.empty();
- }
-
- } else {
- followUpContainer.empty();
- followUpContainer.addClass( 'preparedForRemoval' );
- }
-
- continue;
-
- } else {
- // split objects, which are AT the cursor Position or directly above
- if ( el.selection !== 'none' ) { // before cursor, leave this part inside the splitObject
- // TODO better check for selection == 'partial' here?
- if ( el.domobj && el.domobj.nodeType === 3 && el.startOffset !== undefined ) {
- completeText = el.domobj.data;
- if ( el.startOffset > 0 ) {// first check, if there will be some text left in the splitObject
- el.domobj.data = completeText.substr( 0, el.startOffset );
- } else if ( selectionTree.length > 1 ) { // if not, check if the splitObject contains more than one node, because then it can be removed. this happens, when ENTER is pressed inside of a textnode, but not at the borders
- jQuery( el.domobj ).remove();
- } else { // if the "empty" textnode is the last node left in the splitObject, replace it with a ephemera break
- // if the parent is a blocklevel element, we insert the fillup element
- parent = jQuery( el.domobj ).parent();
- if ( GENTICS.Utils.Dom.isSplitObject( parent[0] ) ) {
- if ( fillUpElement ) {
- parent.html( fillUpElement );
- } else {
- parent.empty();
- }
-
- } else {
- // if the parent is no blocklevel element and would be empty now, we completely remove it
- parent.remove();
- }
- }
- if ( completeText.length - el.startOffset > 0 ) {
- // first check if there is text left to put in the followUpContainer's textnode. this happens, when ENTER is pressed inside of a textnode, but not at the borders
- mirrorLevel[i].data = completeText.substr( el.startOffset, completeText.length );
- } else if ( mirrorLevel.length > 1 ) {
- // if not, check if the followUpContainer contains more than one node, because if yes, the "empty" textnode can be removed
- mirrorLevel.eq( ( i ) ).remove();
- } else if ( GENTICS.Utils.Dom.isBlockLevelElement( followUpContainer[0] ) ) {
- // if the "empty" textnode is the last node left in the followUpContainer (which is a blocklevel element), replace it with a ephemera break
- if ( fillUpElement ) {
- followUpContainer.html( fillUpElement );
- } else {
- followUpContainer.empty();
- }
-
- } else {
- // if the "empty" textnode is the last node left in a non-blocklevel element, mark it for removal
- followUpContainer.empty();
- followUpContainer.addClass( 'preparedForRemoval' );
- }
- }
-
- startMoving = true;
-
- if ( el.children.length > 0 ) {
- this.splitRangeObjectHelper( el.children, rangeObject, mirrorLevel.eq( i ), inBetweenMarkup );
- }
-
- } else {
- // remove all objects in the origin, which are AFTER the cursor
- if ( el.selection === 'none' && startMoving === true ) {
- // iteration is after cursor, remove from splitObject and leave this part inside the followUpContainer
- jqObj = jQuery( el.domobj ).remove();
- }
- }
- }
- }
- } else {
- Aloha.Log.error( this, 'can not split splitObject due to an empty selection tree' );
- }
-
- // and finally cleanup: remove all fillUps > 1
- splitObject.find( 'br.aloha-ephemera:gt(0)' ).remove(); // remove all elements greater than (gt) 0, that also means: leave one
- followUpContainer.find( 'br.aloha-ephemera:gt(0)' ).remove(); // remove all elements greater than (gt) 0, that also means: leave one
-
- // remove objects prepared for removal
- splitObject.find( '.preparedForRemoval' ).remove();
- followUpContainer.find( '.preparedForRemoval' ).remove();
-
- // if splitObject / followUp are empty, place a fillUp inside
- if ( splitObject.contents().length === 0
- && GENTICS.Utils.Dom.isSplitObject( splitObject[0] )
- && fillUpElement ) {
- splitObject.html( fillUpElement );
- }
-
- if ( followUpContainer.contents().length === 0
- && GENTICS.Utils.Dom.isSplitObject( followUpContainer[0] )
- && fillUpElement ) {
- followUpContainer.html( fillUpElement );
- }
- },
-
- /**
- * returns a jQuery object fitting the passed splitObject as follow up object
- * examples,
- * - when passed a p it will return an empty p (clone of the passed p)
- * - when passed an h1, it will return either an h1 (clone of the passed one) or a new p (if the collapsed selection was at the end)
- * @param rangeObject Aloha.RangeObject
- * @return void
- */
- getSplitFollowUpContainer: function( rangeObject ) {
- var tagName = rangeObject.splitObject.nodeName.toLowerCase(),
- returnObj,
- inside,
- lastObj;
-
- switch ( tagName ) {
- case 'h1':
- case 'h2':
- case 'h3':
- case 'h4':
- case 'h5':
- case 'h6':
- // get the last textnode in the splitobject, but don't consider aloha-cleanme elements
- lastObj = jQuery( rangeObject.splitObject ).textNodes( ':not(.aloha-cleanme)' ).last()[0];
- // special case: when enter is hit at the end of a heading, the followUp should be a <p>
- if ( lastObj && rangeObject.startContainer === lastObj
- && rangeObject.startOffset === lastObj.length ) {
- returnObj = jQuery( '<p></p>' );
- inside = jQuery( rangeObject.splitObject ).clone().contents();
- returnObj.append( inside );
- return returnObj;
- }
- break;
-
- case 'li':
- // TODO check whether the li is the last one
- // special case: if enter is hit twice inside a list, the next item should be a <p> (and inserted outside the list)
- if ( rangeObject.startContainer.nodeName.toLowerCase() === 'br'
- && jQuery( rangeObject.startContainer ).hasClass( 'aloha-ephemera' ) ) {
- returnObj = jQuery( '<p></p>' );
- inside = jQuery( rangeObject.splitObject ).clone().contents();
- returnObj.append( inside );
- return returnObj;
- }
- // when the li is the last one and empty, we also just return a <p>
- if ( !rangeObject.splitObject.nextSibling
- && jQuery.trim( jQuery( rangeObject.splitObject ).text() ).length === 0 ) {
- returnObj = jQuery( '<p></p>' );
- return returnObj;
- }
- }
-
- return jQuery( rangeObject.splitObject ).clone();
- },
-
- /**
- * Transform the given domobj into an object with the given new nodeName.
- * Preserves the content and all attributes. If a range object is given, also the range will be preserved
- * @param domobj dom object to transform
- * @param nodeName new node name
- * @param range range object
- * @api
- * @return new object as jQuery object
- */
- transformDomObject: function( domobj, nodeName, range ) {
- // first create the new element
- var jqOldObj = jQuery( domobj ),
- jqNewObj = jQuery( '<' + nodeName + '></' + nodeName + '>' ),
- i;
-
- // TODO what about events? css properties?
-
- // copy attributes
- if ( jqOldObj[0].attributes ) {
- for ( i = 0; i < jqOldObj[0].attributes.length; ++i ) {
- jqNewObj.attr(
- jqOldObj[0].attributes[ i ].nodeName,
- jqOldObj[0].attributes[ i ].nodeValue
- );
- }
- }
-
- // copy inline CSS
- if ( jqOldObj[0].style && jqOldObj[0].style.cssText ) {
- jqNewObj[0].style.cssText = jqOldObj[0].style.cssText;
- }
-
- // now move the contents of the old dom object into the new dom object
- jqOldObj.contents().appendTo( jqNewObj );
-
- // finally replace the old object with the new one
- jqOldObj.replaceWith( jqNewObj );
-
- // preserve the range
- if ( range ) {
- if ( range.startContainer == domobj ) {
- range.startContainer = jqNewObj.get( 0 );
- }
-
- if ( range.endContainer == domobj ) {
- range.endContainer = jqNewObj.get( 0 );
- }
- }
-
- return jqNewObj;
- },
-
- /**
- * String representation
- * @return {String}
- */
- toString: function() {
- return 'Aloha.Markup';
- }
-
-} );
-
-Aloha.Markup = new Aloha.Markup();
-
-return Aloha.Markup;
-
-} );
+/* markup.js is part of Aloha Editor project http://aloha-editor.org
+ *
+ * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor.
+ * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria.
+ * Contributors http://aloha-editor.org/contribution.php
+ *
+ * Aloha Editor is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * As an additional permission to the GNU GPL version 2, you may distribute
+ * non-source (e.g., minimized or compacted) forms of the Aloha-Editor
+ * source code without the copy of the GNU GPL normally required,
+ * provided you include this license notice and a URL through which
+ * recipients can access the Corresponding Source.
+ */
+define([
+ 'aloha/core',
+ 'util/class',
+ 'jquery',
+ 'aloha/ecma5shims',
+ 'aloha/console',
+ 'aloha/block-jump'
+], function (
+ Aloha,
+ Class,
+ jQuery,
+ shims,
+ console,
+ BlockJump
+) {
+ "use strict";
+
+ var GENTICS = window.GENTICS;
+
+ var isOldIE = !!(jQuery.browser.msie && 9 > parseInt(jQuery.browser.version, 10));
+
+ function isBR(node) {
+ return 'BR' === node.nodeName;
+ }
+
+ function isBlock(node) {
+ return 'false' === jQuery(node).attr('contenteditable');
+ }
+
+ function isTextNode(node) {
+ return node && 3 === node.nodeType; // Node.TEXT_NODE
+ }
+
+ function nodeLength(node) {
+ return !node ? 0 : (isTextNode(node) ? node.length : node.childNodes.length);
+ }
+
+ /**
+ * Determines whether the given text node is visible to the the user,
+ * based on our understanding that browsers will not display
+ * superfluous white spaces.
+ *
+ * @param {HTMLEmenent} node The text node to be checked.
+ */
+ function isVisibleTextNode(node) {
+ return 0 < node.data.replace(/\s+/g, '').length;
+ }
+
+ function nextVisibleNode(node) {
+ if (!node) {
+ return null;
+ }
+
+ if (node.nextSibling) {
+ // Skip over nodes that the user cannot see ...
+ if (isTextNode(node.nextSibling) && !isVisibleTextNode(node.nextSibling)) {
+ return nextVisibleNode(node.nextSibling);
+ }
+
+ // Skip over propping <br>s ...
+ if (isBR(node.nextSibling) && node.nextSibling === node.parentNode.lastChild) {
+ return nextVisibleNode(node.nextSibling);
+ }
+
+ // Skip over empty editable elements ...
+ if ('' === node.nextSibling.innerHTML && !isBlock(node.nextSibling)) {
+ return nextVisibleNode(node.nextSibling);
+ }
+
+ return node.nextSibling;
+ }
+
+ if (node.parentNode) {
+ return nextVisibleNode(node.parentNode);
+ }
+
+ return null;
+ }
+
+ function prevVisibleNode(node) {
+ if (!node) {
+ return null;
+ }
+
+ if (node.previousSibling) {
+ // Skip over nodes that the user cannot see...
+ if (isTextNode(node.previousSibling) && !isVisibleTextNode(node.previousSibling)) {
+ return prevVisibleNode(node.previousSibling);
+ }
+
+ // Skip over empty editable elements ...
+ if ('' === node.previousSibling.innerHTML && !isBlock(node.previousSibling)) {
+ return prevVisibleNode(node.previouSibling);
+ }
+
+ return node.previousSibling;
+ }
+
+ if (node.parentNode) {
+ return prevVisibleNode(node.parentNode);
+ }
+
+ return null;
+ }
+
+ function isFrontPosition(node, offset) {
+ return (0 === offset) || (offset <= node.data.length - node.data.replace(/^\s+/, '').length);
+ }
+
+ function isBlockInsideEditable($block) {
+ return $block.parent().hasClass('aloha-editable');
+ }
+
+ function isEndPosition(node, offset) {
+ var length = nodeLength(node);
+
+ if (length === offset) {
+ return true;
+ }
+
+ var isText = isTextNode(node);
+
+ // If within a text node, then ignore superfluous white-spaces,
+ // since they are invisible to the user.
+ if (isText && node.data.replace(/\s+$/, '').length === offset) {
+ return true;
+ }
+
+ if (1 === length && !isText) {
+ return isBR(node.childNodes[0]);
+ }
+
+ return false;
+ }
+
+ function blink(node) {
+ jQuery(node).stop(true).css({
+ opacity: 0
+ }).fadeIn(0).delay(100).fadeIn(function () {
+ jQuery(node).css({
+ opacity: 1
+ });
+ });
+
+ return node;
+ }
+
+ function nodeContains(node1, node2) {
+ return isOldIE ? (shims.compareDocumentPosition(node1, node2) & 16) : 0 < jQuery(node1).find(node2).length;
+ }
+
+ function isInsidePlaceholder(range) {
+ var start = range.startContainer;
+ var end = range.endContainer;
+ var $placeholder = window.$_alohaPlaceholder;
+
+ return $placeholder.is(start) || $placeholder.is(end) || nodeContains($placeholder[0], start) || nodeContains($placeholder[0], end);
+ }
+
+ function cleanupPlaceholders(range) {
+ if (window.$_alohaPlaceholder && !isInsidePlaceholder(range)) {
+ if (0 === window.$_alohaPlaceholder.html().replace(/^( )*$/, '').length) {
+ window.$_alohaPlaceholder.remove();
+ }
+
+ window.$_alohaPlaceholder = null;
+ }
+ }
+
+ /**
+ * @TODO(petro): We need to be more intelligent about whether we insert a
+ * block-level placeholder or a phrasing level element.
+ * @TODO(petro): test with <pre>
+ * @TODO: move to block-jump.js
+ */
+ function jumpBlock(block, isGoingLeft, currentRange) {
+ var range = new GENTICS.Utils.RangeObject();
+ var sibling = isGoingLeft ? prevVisibleNode(block) : nextVisibleNode(block);
+
+ if (!sibling || isBlock(sibling)) {
+ var $landing = jQuery('<div> </div>');
+
+ if (isGoingLeft) {
+ jQuery(block).before($landing);
+ } else {
+ jQuery(block).after($landing);
+ }
+
+ range.startContainer = range.endContainer = $landing[0];
+ range.startOffset = range.endOffset = 0;
+
+ // Clear out any old placeholder first ...
+ cleanupPlaceholders(range);
+
+ window.$_alohaPlaceholder = $landing;
+ } else {
+
+ // Don't jump the block yet if the cursor is moving to the
+ // beginning or end of a text node, or if it is about to leave
+ // an element node. Both these cases require a hack in some
+ // browsers.
+ var moveToBoundaryPositionInIE = ( // To the beginning or end of a text node?
+ (currentRange.startContainer.nodeType === 3
+ && currentRange.startContainer === currentRange.endContainer
+ && currentRange.startContainer.nodeValue !== ""
+ && (isGoingLeft ? currentRange.startOffset === 1 : currentRange.endOffset + 1 === currentRange.endContainer.length))
+ // Leaving an element node?
+ || (currentRange.startContainer.nodeType === 1
+ && (!currentRange.startOffset
+ || (currentRange.startContainer.childNodes[currentRange.startOffset] && currentRange.startContainer.childNodes[currentRange.startOffset].nodeType === 1)))
+ );
+
+ if (moveToBoundaryPositionInIE) {
+ // The cursor is moving to the beginning or end of a text
+ // node, or is leaving an element node, which requires a
+ // hack in some browsers.
+ var zeroWidthNode = BlockJump.insertZeroWidthTextNodeFix(block, isGoingLeft);
+ range.startContainer = range.endContainer = zeroWidthNode;
+ range.startOffset = range.endOffset = isGoingLeft ? 1 : 0;
+ } else {
+ // The selection is already at the boundary position - jump
+ // the block.
+ range.startContainer = range.endContainer = sibling;
+ range.startOffset = range.endOffset = isGoingLeft ? nodeLength(sibling) : 0;
+ if (!isGoingLeft) {
+ // Just as above, jumping to the first position right of
+ // a block requires a hack in some browsers. Jumping
+ // left seems to be fine.
+ BlockJump.insertZeroWidthTextNodeFix(block, true);
+ }
+ }
+ cleanupPlaceholders(range);
+ }
+
+ range.select();
+
+ Aloha.trigger('aloha-block-selected', block);
+ Aloha.Selection.preventSelectionChanged();
+ }
+
+ /**
+ * Markup object
+ */
+ Aloha.Markup = Class.extend({
+
+ /**
+ * Key handlers for special key codes
+ */
+ keyHandlers: {},
+
+ /**
+ * Add a key handler for the given key code
+ * @param keyCode key code
+ * @param handler handler function
+ */
+ addKeyHandler: function (keyCode, handler) {
+ if (!this.keyHandlers[keyCode]) {
+ this.keyHandlers[keyCode] = [];
+ }
+
+ this.keyHandlers[keyCode].push(handler);
+ },
+
+ /**
+ * Removes a key handler for the given key code
+ * @param keyCode key code
+ */
+ removeKeyHandler: function (keyCode) {
+ if (this.keyHandlers[keyCode]) {
+ this.keyHandlers[keyCode] = null;
+ }
+ },
+
+ insertBreak: function () {
+ var range = Aloha.Selection.rangeObject,
+ nonWSIndex,
+ nextTextNode,
+ newBreak;
+
+ if (!range.isCollapsed()) {
+ this.removeSelectedMarkup();
+ }
+
+ newBreak = jQuery('<br/>');
+ GENTICS.Utils.Dom.insertIntoDOM(newBreak, range, Aloha.activeEditable.obj);
+
+ nextTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
+ newBreak.parent().get(0),
+ GENTICS.Utils.Dom.getIndexInParent(newBreak.get(0)) + 1,
+ false
+ );
+
+ if (nextTextNode) {
+ // trim leading whitespace
+ nonWSIndex = nextTextNode.data.search(/\S/);
+ if (nonWSIndex > 0) {
+ nextTextNode.data = nextTextNode.data.substring(nonWSIndex);
+ }
+ }
+
+ range.startContainer = range.endContainer = newBreak.get(0).parentNode;
+ range.startOffset = range.endOffset = GENTICS.Utils.Dom.getIndexInParent(newBreak.get(0)) + 1;
+ range.correctRange();
+ range.clearCaches();
+ range.select();
+ },
+
+ /**
+ * first method to handle key strokes
+ * @param event DOM event
+ * @param rangeObject as provided by Aloha.Selection.getRangeObject();
+ * @return "Aloha.Selection"
+ */
+ preProcessKeyStrokes: function (event) {
+ if (event.type !== 'keydown') {
+ return false;
+ }
+
+ var rangeObject,
+ handlers,
+ i;
+
+ if (this.keyHandlers[event.keyCode]) {
+ handlers = this.keyHandlers[event.keyCode];
+ for (i = 0; i < handlers.length; ++i) {
+ if (!handlers[i](event)) {
+ return false;
+ }
+ }
+ }
+
+ // LEFT (37), RIGHT (39) keys for block detection
+ if (event.keyCode === 37 || event.keyCode === 39) {
+ if (Aloha.getSelection().getRangeCount()) {
+ rangeObject = Aloha.getSelection().getRangeAt(0);
+
+ if (this.processCursor(rangeObject, event.keyCode)) {
+ cleanupPlaceholders(Aloha.Selection.rangeObject);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // BACKSPACE
+ if (event.keyCode === 8) {
+ event.preventDefault(); // prevent history.back() even on exception
+ Aloha.execCommand('delete', false);
+ return false;
+ }
+
+ // DELETE
+ if (event.keyCode === 46) {
+ Aloha.execCommand('forwarddelete', false);
+ return false;
+ }
+
+ // ENTER
+ if (event.keyCode === 13) {
+ if (event.shiftKey) {
+ Aloha.execCommand('insertlinebreak', false);
+ return false;
+ }
+ Aloha.execCommand('insertparagraph', false);
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Processing of cursor keys.
+ * Detect blocks (elements with contenteditable=false) and will select them
+ * (normally the cursor would simply jump right past them).
+ *
+ * For each block that is selected, an 'aloha-block-selected' event will be
+ * triggered.
+ *
+ * TODO: the above is what should happen. Currently we just skip past blocks.
+ *
+ * @param {RangyRange} range A range object for the current selection.
+ * @param {number} keyCode Code of the currently pressed key.
+ * @return {boolean} False if a block was found, to prevent further events,
+ * true otherwise.
+ * @TODO move to block-jump.js
+ */
+ processCursor: function (range, keyCode) {
+ if (!range.collapsed) {
+ return true;
+ }
+
+ BlockJump.removeZeroWidthTextNodeFix();
+
+ var node = range.startContainer,
+ selection = Aloha.getSelection();
+
+ if (!node) {
+ return true;
+ }
+
+ var sibling, offset;
+
+ // special handling for moving Cursor around zero-width whitespace in IE7
+ if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) <= 7 && isTextNode(node)) {
+ if (keyCode == 37) {
+ // moving left -> skip zwsp to the left
+ offset = range.startOffset;
+ while (offset > 0 && node.data.charAt(offset - 1) === '\u200b') {
+ offset--;
+ }
+ if (offset != range.startOffset) {
+ range.setStart(range.startContainer, offset);
+ range.setEnd(range.startContainer, offset);
+ selection = Aloha.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ } else if (keyCode == 39) {
+ // moving right -> skip zwsp to the right
+ offset = range.startOffset;
+ while (offset < node.data.length && node.data.charAt(offset) === '\u200b') {
+ offset++;
+ }
+ if (offset != range.startOffset) {
+ range.setStart(range.startContainer, offset);
+ range.setEnd(range.startContainer, offset);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ }
+ }
+
+ // Versions of Internet Explorer that are older that 9, will
+ // erroneously allow you to enter and edit inside elements which have
+ // their contenteditable attribute set to false...
+ if (isOldIE && !jQuery(node).contentEditable()) {
+ var $parentBlock = jQuery(node).parents('[contenteditable=false]');
+ var isInsideBlock = $parentBlock.length > 0;
+
+ if (isInsideBlock) {
+ if (isBlockInsideEditable($parentBlock)) {
+ sibling = $parentBlock[0];
+ } else {
+ return true;
+ }
+ }
+ }
+
+ var isLeft;
+ if (!sibling) {
+ // True if keyCode denotes LEFT or UP arrow key, otherwise they
+ // keyCode is for RIGHT or DOWN in which this value will be false.
+ isLeft = (37 === keyCode || 38 === keyCode);
+ offset = range.startOffset;
+
+ if (isTextNode(node)) {
+ if (isLeft) {
+ var isApproachingFrontPosition = (1 === offset);
+ if (!isApproachingFrontPosition && !isFrontPosition(node, offset)) {
+ return true;
+ }
+ } else if (!isEndPosition(node, offset)) {
+ return true;
+ }
+
+ } else {
+ node = node.childNodes[offset === nodeLength(node) ? offset - 1 : offset];
+ }
+
+ sibling = isLeft ? prevVisibleNode(node) : nextVisibleNode(node);
+ }
+
+ if (isBlock(sibling)) {
+ jumpBlock(sibling, isLeft, range);
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * method handling shiftEnter
+ * @param Aloha.Selection.SelectionRange of the current selection
+ * @return void
+ */
+ processShiftEnter: function (rangeObject) {
+ this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject);
+ },
+
+ /**
+ * method handling Enter
+ * @param Aloha.Selection.SelectionRange of the current selection
+ * @return void
+ */
+ processEnter: function (rangeObject) {
+ if (rangeObject.splitObject) {
+ // now comes a very evil hack for ie, when the enter is pressed in a text node in an li element, we just append an empty text node
+ // if ( jQuery.browser.msie
+ // && GENTICS.Utils.Dom
+ // .isListElement( rangeObject.splitObject ) ) {
+ // jQuery( rangeObject.splitObject ).append(
+ // jQuery( document.createTextNode( '' ) ) );
+ // }
+ this.splitRangeObject(rangeObject);
+ } else { // if there is no split object, the Editable is the paragraph type itself (e.g. a p or h2)
+ this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject);
+ }
+ },
+
+ /**
+ * Insert the given html markup at the current selection
+ * @param html html markup to be inserted
+ */
+ insertHTMLCode: function (html) {
+ var rangeObject = Aloha.Selection.rangeObject;
+ this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject, jQuery(html));
+ },
+
+ /**
+ * insert an HTML Break <br /> into current selection
+ * @param Aloha.Selection.SelectionRange of the current selection
+ * @return void
+ */
+ insertHTMLBreak: function (selectionTree, rangeObject, inBetweenMarkup) {
+ var i,
+ treeLength,
+ el,
+ jqEl,
+ jqElBefore,
+ jqElAfter,
+ tmpObject,
+ offset,
+ checkObj;
+
+ inBetweenMarkup = inBetweenMarkup || jQuery('<br/>');
+
+ for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) {
+ el = selectionTree[i];
+ jqEl = el.domobj ? jQuery(el.domobj) : undefined;
+
+ if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject
+ if (el.selection == 'collapsed') {
+ // collapsed selection found (between nodes)
+ if (i > 0) {
+ // not at the start, so get the element to the left
+ jqElBefore = jQuery(selectionTree[i - 1].domobj);
+
+ // and insert the break after it
+ jqElBefore.after(inBetweenMarkup);
+
+ } else {
+ // at the start, so get the element to the right
+ jqElAfter = jQuery(selectionTree[1].domobj);
+
+ // and insert the break before it
+ jqElAfter.before(inBetweenMarkup);
+ }
+
+ // now set the range
+ rangeObject.startContainer = rangeObject.endContainer = inBetweenMarkup[0].parentNode;
+ rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(inBetweenMarkup[0]) + 1;
+ rangeObject.correctRange();
+
+ } else if (el.domobj && el.domobj.nodeType === 3) { // textNode
+ // when the textnode is immediately followed by a blocklevel element (like p, h1, ...) we need to add an additional br in between
+ if (el.domobj.nextSibling && el.domobj.nextSibling.nodeType == 1 && Aloha.Selection.replacingElements[el.domobj.nextSibling.nodeName.toLowerCase()]) {
+ // TODO check whether this depends on the browser
+ jqEl.after('<br/>');
+ }
+
+ if (this.needEndingBreak()) {
+ // when the textnode is the last inside a blocklevel element
+ // (like p, h1, ...) we need to add an additional br as very
+ // last object in the blocklevel element
+ checkObj = el.domobj;
+
+ while (checkObj) {
+ if (checkObj.nextSibling) {
+ checkObj = false;
+ } else {
+ // go to the parent
+ checkObj = checkObj.parentNode;
+
+ // found a blocklevel or list element, we are done
+ if (GENTICS.Utils.Dom.isBlockLevelElement(checkObj) || GENTICS.Utils.Dom.isListElement(checkObj)) {
+ break;
+ }
+
+ // reached the limit object, we are done
+ if (checkObj === rangeObject.limitObject) {
+ checkObj = false;
+ }
+ }
+ }
+
+ // when we found a blocklevel element, insert a break at the
+ // end. Mark the break so that it is cleaned when the
+ // content is fetched.
+ if (checkObj) {
+ jQuery(checkObj).append('<br class="aloha-cleanme" />');
+ }
+ }
+
+ // insert the break
+ jqEl.between(inBetweenMarkup, el.startOffset);
+
+ // correct the range
+ // count the number of previous siblings
+ offset = 0;
+ tmpObject = inBetweenMarkup[0];
+ while (tmpObject) {
+ tmpObject = tmpObject.previousSibling;
+ ++offset;
+ }
+
+ rangeObject.startContainer = inBetweenMarkup[0].parentNode;
+ rangeObject.endContainer = inBetweenMarkup[0].parentNode;
+ rangeObject.startOffset = offset;
+ rangeObject.endOffset = offset;
+ rangeObject.correctRange();
+
+ } else if (el.domobj && el.domobj.nodeType === 1) { // other node, normally a break
+ if (jqEl.parent().find('br.aloha-ephemera').length === 0) {
+ // but before putting it, remove all:
+ jQuery(rangeObject.limitObject).find('br.aloha-ephemera').remove();
+
+ // now put it:
+ jQuery(rangeObject.commonAncestorContainer).append(this.getFillUpElement(rangeObject.splitObject));
+ }
+
+ jqEl.after(inBetweenMarkup);
+
+ // now set the selection. Since we just added one break do the currect el
+ // the new position must be el's position + 1. el's position is the index
+ // of the el in the selection tree, which is i. then we must add
+ // another +1 because we want to be AFTER the object, not before. therefor +2
+ rangeObject.startContainer = rangeObject.commonAncestorContainer;
+ rangeObject.endContainer = rangeObject.startContainer;
+ rangeObject.startOffset = i + 2;
+ rangeObject.endOffset = i + 2;
+ rangeObject.update();
+ }
+ }
+ }
+ rangeObject.select();
+ },
+
+ /**
+ * Check whether blocklevel elements need breaks at the end to visibly render a newline
+ * @return true if an ending break is necessary, false if not
+ */
+ needEndingBreak: function () {
+ // currently, all browser except IE need ending breaks
+ return !jQuery.browser.msie;
+ },
+
+ /**
+ * Get the currently selected text or false if nothing is selected (or the selection is collapsed)
+ * @return selected text
+ */
+ getSelectedText: function () {
+ var rangeObject = Aloha.Selection.rangeObject;
+
+ if (rangeObject.isCollapsed()) {
+ return false;
+ }
+
+ return this.getFromSelectionTree(rangeObject.getSelectionTree(), true);
+ },
+
+ /**
+ * Recursive function to get the selected text from the selection tree starting at the given level
+ * @param selectionTree array of selectiontree elements
+ * @param astext true when the contents shall be fetched as text, false for getting as html markup
+ * @return selected text from that level (incluiding all sublevels)
+ */
+ getFromSelectionTree: function (selectionTree, astext) {
+ var text = '', i, treeLength, el, clone;
+ for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) {
+ el = selectionTree[i];
+ if (el.selection == 'partial') {
+ if (el.domobj.nodeType === 3) {
+ // partial text node selected, get the selected part
+ text += el.domobj.data.substring(el.startOffset, el.endOffset);
+ } else if (el.domobj.nodeType === 1 && el.children) {
+ // partial element node selected, do the recursion into the children
+ if (astext) {
+ text += this.getFromSelectionTree(el.children, astext);
+ } else {
+ // when the html shall be fetched, we create a clone of
+ // the element and remove all the children
+ clone = jQuery(el.domobj.outerHTML).empty();
+ // then we do the recursion and add the selection into the clone
+ clone.html(this.getFromSelectionTree(el.children, astext));
+ // finally we get the html of the clone
+ text += clone.outerHTML();
+ }
+ }
+ } else if (el.selection == 'full') {
+ if (el.domobj.nodeType === 3) {
+ // full text node selected, get the text
+ text += jQuery(el.domobj).text();
+ } else if (el.domobj.nodeType === 1 && el.children) {
+ // full element node selected, get the html of the node and all children
+ text += astext ? jQuery(el.domobj).text() : jQuery(el.domobj).outerHTML();
+ }
+ }
+ }
+
+ return text;
+ },
+
+ /**
+ * Get the currently selected markup or false if nothing is selected (or the selection is collapsed)
+ * @return {?String}
+ */
+ getSelectedMarkup: function () {
+ var rangeObject = Aloha.Selection.rangeObject;
+ return rangeObject.isCollapsed() ? null : this.getFromSelectionTree(rangeObject.getSelectionTree(), false);
+ },
+
+ /**
+ * Remove the currently selected markup
+ */
+ removeSelectedMarkup: function () {
+ var rangeObject = Aloha.Selection.rangeObject,
+ newRange;
+
+ if (rangeObject.isCollapsed()) {
+ return;
+ }
+
+ newRange = new Aloha.Selection.SelectionRange();
+ // remove the selection
+ this.removeFromSelectionTree(rangeObject.getSelectionTree(), newRange);
+
+ // do a cleanup now (starting with the commonancestorcontainer)
+ newRange.update();
+ GENTICS.Utils.Dom.doCleanup({
+ 'merge': true,
+ 'removeempty': true
+ }, Aloha.Selection.rangeObject);
+ Aloha.Selection.rangeObject = newRange;
+
+ // need to set the collapsed selection now
+ newRange.correctRange();
+ newRange.update();
+ newRange.select();
+ Aloha.Selection.updateSelection();
+ },
+
+ /**
+ * Recursively remove the selected items, starting with the given level in the selectiontree
+ * @param selectionTree current level of the selectiontree
+ * @param newRange new collapsed range to be set after the removal
+ */
+ removeFromSelectionTree: function (selectionTree, newRange) {
+ // remember the first found partially selected element node (in case we need
+ // to merge it with the last found partially selected element node)
+ var firstPartialElement, newdata, i, el, adjacentTextNode, treeLength;
+
+ // iterate through the selection tree
+ for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) {
+ el = selectionTree[i];
+
+ // check the type of selection
+ if (el.selection == 'partial') {
+ if (el.domobj.nodeType === 3) {
+ // partial text node selected, so remove the selected portion
+ newdata = '';
+ if (el.startOffset > 0) {
+ newdata += el.domobj.data.substring(0, el.startOffset);
+ }
+ if (el.endOffset < el.domobj.data.length) {
+ newdata += el.domobj.data.substring(el.endOffset, el.domobj.data.length);
+ }
+ el.domobj.data = newdata;
+
+ // eventually set the new range (if not done before)
+ if (!newRange.startContainer) {
+ newRange.startContainer = newRange.endContainer = el.domobj;
+ newRange.startOffset = newRange.endOffset = el.startOffset;
+ }
+ } else if (el.domobj.nodeType === 1 && el.children) {
+ // partial element node selected, so do the recursion into the children
+ this.removeFromSelectionTree(el.children, newRange);
+
+ if (firstPartialElement) {
+ // when the first parially selected element is the same type
+ // of element, we need to merge them
+ if (firstPartialElement.nodeName == el.domobj.nodeName) {
+ // merge the nodes
+ jQuery(firstPartialElement).append(jQuery(el.domobj).contents());
+
+ // and remove the latter one
+ jQuery(el.domobj).remove();
+ }
+
+ } else {
+ // remember this element as first partially selected element
+ firstPartialElement = el.domobj;
+ }
+ }
+
+ } else if (el.selection == 'full') {
+ // eventually set the new range (if not done before)
+ if (!newRange.startContainer) {
+ adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
+ el.domobj.parentNode,
+ GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1,
+ false,
+ {
+ 'blocklevel': false
+ }
+ );
+
+ if (adjacentTextNode) {
+ newRange.startContainer = newRange.endContainer = adjacentTextNode;
+ newRange.startOffset = newRange.endOffset = 0;
+ } else {
+ newRange.startContainer = newRange.endContainer = el.domobj.parentNode;
+ newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1;
+ }
+ }
+
+ // full node selected, so just remove it (will also remove all children)
+ jQuery(el.domobj).remove();
+ }
+ }
+ },
+
+ /**
+ * split passed rangeObject without or with optional markup
+ * @param Aloha.Selection.SelectionRange of the current selection
+ * @param markup object (jQuery) to insert in between the split elements
+ * @return void
+ */
+ splitRangeObject: function (rangeObject, markup) {
+ // UAAAA: first check where the markup can be inserted... *grrrrr*, then decide where to split
+ // object which is split up
+ var splitObject = jQuery(rangeObject.splitObject),
+ selectionTree,
+ insertAfterObject,
+ followUpContainer;
+
+ // update the commonAncestor with the splitObject (so that the selectionTree is correct)
+ rangeObject.update(rangeObject.splitObject); // set the splitObject as new commonAncestorContainer and update the selectionTree
+
+ // calculate the selection tree. NOTE: it is necessary to do this before
+ // getting the followupcontainer, since getting the selection tree might
+ // possibly merge text nodes, which would lead to differences in the followupcontainer
+ selectionTree = rangeObject.getSelectionTree();
+
+ // object to be inserted after the splitObject
+ followUpContainer = this.getSplitFollowUpContainer(rangeObject);
+
+ // now split up the splitObject into itself AND the followUpContainer
+ this.splitRangeObjectHelper(selectionTree, rangeObject, followUpContainer); // split the current object into itself and the followUpContainer
+
+ // check whether the followupcontainer is still marked for removal
+ if (followUpContainer.hasClass('preparedForRemoval')) {
+ // TODO shall we just remove the class or shall we not use the followupcontainer?
+ followUpContainer.removeClass('preparedForRemoval');
+ }
+
+ // now let's find the place, where the followUp is inserted afterwards. normally that's the splitObject itself, but in
+ // some cases it might be their parent (e.g. inside a list, a <p> followUp must be inserted outside the list)
+ insertAfterObject = this.getInsertAfterObject(rangeObject, followUpContainer);
+
+ // now insert the followUpContainer
+ jQuery(followUpContainer).insertAfter(insertAfterObject); // attach the followUpContainer right after the insertAfterObject
+
+ // in some cases, we want to remove the "empty" splitObject (e.g. LIs, if enter was hit twice)
+ if (rangeObject.splitObject.nodeName.toLowerCase() === 'li' && !Aloha.Selection.standardTextLevelSemanticsComparator(rangeObject.splitObject, followUpContainer)) {
+ jQuery(rangeObject.splitObject).remove();
+ }
+
+ rangeObject.startContainer = null;
+ // first check whether the followUpContainer starts with a <br/>
+ // if so, place the cursor right before the <br/>
+ var followContents = followUpContainer.contents();
+ if (followContents.length > 0 && followContents.get(0).nodeType == 1 && followContents.get(0).nodeName.toLowerCase() === 'br') {
+ rangeObject.startContainer = followUpContainer.get(0);
+ }
+
+ if (!rangeObject.startContainer) {
+ // find a possible text node in the followUpContainer and set the selection to it
+ // if no textnode is available, set the selection to the followup container itself
+ rangeObject.startContainer = followUpContainer.textNodes(true, true).first().get(0);
+ }
+ if (!rangeObject.startContainer) { // if no text node was found, select the parent object of <br class="aloha-ephemera" />
+ rangeObject.startContainer = followUpContainer.textNodes(false).first().parent().get(0);
+ }
+ if (rangeObject.startContainer) {
+ // the cursor is always at the beginning of the followUp
+ rangeObject.endContainer = rangeObject.startContainer;
+ rangeObject.startOffset = 0;
+ rangeObject.endOffset = 0;
+ } else {
+ rangeObject.startContainer = rangeObject.endContainer = followUpContainer.parent().get(0);
+ rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(followUpContainer.get(0));
+ }
+
+ // finally update the range object again
+ rangeObject.update();
+
+ // now set the selection
+ rangeObject.select();
+ },
+
+ /**
+ * method to get the object after which the followUpContainer can be inserted during splitup
+ * this is a helper method, not needed anywhere else
+ * @param rangeObject Aloha.Selection.SelectionRange of the current selection
+ * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
+ * @return object after which the followUpContainer can be inserted
+ */
+ getInsertAfterObject: function (rangeObject, followUpContainer) {
+ var passedSplitObject, i, el;
+
+ for (i = 0; i < rangeObject.markupEffectiveAtStart.length; i++) {
+ el = rangeObject.markupEffectiveAtStart[i];
+
+ // check if we have already passed the splitObject (some other markup might come before)
+ if (el === rangeObject.splitObject) {
+ passedSplitObject = true;
+ }
+
+ // if not passed splitObject, skip this markup
+ if (!passedSplitObject) {
+ continue;
+ }
+
+ // once we are passed, check if the followUpContainer is allowed to be inserted into the currents el's parent
+ if (Aloha.Selection.canTag1WrapTag2(jQuery(el).parent()[0].nodeName, followUpContainer[0].nodeName)) {
+ return el;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * @fixme: Someone who knows what this function does, please refactor it.
+ * 1. splitObject arg is not used at all
+ * 2. Would be better to use ternary operation would be better than if else statement
+ *
+ * method to get the html code for a fillUpElement. this is needed for empty paragraphs etc., so that they take up their expected height
+ * @param splitObject split object (dom object)
+ * @return fillUpElement HTML Code
+ */
+ getFillUpElement: function (splitObject) {
+ if (jQuery.browser.msie) {
+ return false;
+ }
+ return jQuery('<br class="aloha-cleanme"/>');
+ },
+
+ /**
+ * removes textNodes from passed array, which only contain contentWhiteSpace (e.g. a \n between two tags)
+ * @param domArray array of domObjects
+ * @return void
+ */
+ removeElementContentWhitespaceObj: function (domArray) {
+ var correction = 0,
+ removeLater = [],
+ i,
+ el,
+ removeIndex;
+
+ for (i = 0; i < domArray.length; ++i) {
+ el = domArray[i];
+ if (el.isElementContentWhitespace) {
+ removeLater[removeLater.length] = i;
+ }
+ }
+
+ for (i = 0; i < removeLater.length; ++i) {
+ removeIndex = removeLater[i];
+ domArray.splice(removeIndex - correction, 1);
+ ++correction;
+ }
+ },
+
+ /**
+ * recursive method to parallelly walk through two dom subtrees, leave elements before startContainer in first subtree and move rest to other
+ * @param selectionTree tree to iterate over as contained in rangeObject. must be passed separately to allow recursion in the selection tree, but not in the rangeObject
+ * @param rangeObject Aloha.Selection.SelectionRange of the current selection
+ * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
+ * @param inBetweenMarkup jQuery object to be inserted between the two split parts. will be either a <br> (if no followUpContainer is passed) OR e.g. a table, which must be inserted between the splitobject AND the follow up
+ * @return void
+ */
+ splitRangeObjectHelper: function (selectionTree, rangeObject, followUpContainer, inBetweenMarkup) {
+ if (!followUpContainer) {
+ Aloha.Log.warn(this, 'no followUpContainer, no inBetweenMarkup, nothing to do...');
+ }
+
+ var fillUpElement = this.getFillUpElement(rangeObject.splitObject),
+ splitObject = jQuery(rangeObject.splitObject),
+ startMoving = false,
+ el,
+ i,
+ completeText,
+ jqObj,
+ mirrorLevel,
+ parent,
+ treeLength;
+
+ if (selectionTree.length > 0) {
+ mirrorLevel = followUpContainer.contents();
+
+ // if length of mirrorLevel and selectionTree are not equal, the mirrorLevel must be corrected. this happens, when the mirrorLevel contains whitespace textNodes
+ if (mirrorLevel.length !== selectionTree.length) {
+ this.removeElementContentWhitespaceObj(mirrorLevel);
+ }
+
+ for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) {
+ el = selectionTree[i];
+
+ // remove all objects in the mirrorLevel, which are BEFORE the cursor
+ // OR if the cursor is at the last position of the last Textnode (causing an empty followUpContainer to be appended)
+ if ((el.selection === 'none' && startMoving === false) || (el.domobj && el.domobj.nodeType === 3 && el === selectionTree[(selectionTree.length - 1)] && el.startOffset === el.domobj.data.length)) {
+ // iteration is before cursor, leave this part inside the splitObject, remove from followUpContainer
+ // however if the object to remove is the last existing textNode within the followUpContainer, insert a BR instead
+ // otherwise the followUpContainer is invalid and takes up no vertical space
+
+ if (followUpContainer.textNodes().length > 1 || (el.domobj.nodeType === 1 && el.children.length === 0)) {
+ // note: the second part of the if (el.domobj.nodeType === 1 && el.children.length === 0) covers a very special condition,
+ // where an empty tag is located right before the cursor when pressing enter. In this case the empty tag would not be
+ // removed correctly otherwise
+ mirrorLevel.eq(i).remove();
+
+ } else if (GENTICS.Utils.Dom.isSplitObject(followUpContainer[0])) {
+ if (fillUpElement) {
+ followUpContainer.html(fillUpElement); // for your zoological german knowhow: ephemera = Eintagsfliege
+ } else {
+ followUpContainer.empty();
+ }
+
+ } else {
+ followUpContainer.empty();
+ followUpContainer.addClass('preparedForRemoval');
+ }
+
+ continue;
+
+ } else {
+ // split objects, which are AT the cursor Position or directly above
+ if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject
+ // TODO better check for selection == 'partial' here?
+ if (el.domobj && el.domobj.nodeType === 3 && el.startOffset !== undefined) {
+ completeText = el.domobj.data;
+ if (el.startOffset > 0) { // first check, if there will be some text left in the splitObject
+ el.domobj.data = completeText.substr(0, el.startOffset);
+ } else if (selectionTree.length > 1) { // if not, check if the splitObject contains more than one node, because then it can be removed. this happens, when ENTER is pressed inside of a textnode, but not at the borders
+ jQuery(el.domobj).remove();
+ } else { // if the "empty" textnode is the last node left in the splitObject, replace it with a ephemera break
+ // if the parent is a blocklevel element, we insert the fillup element
+ parent = jQuery(el.domobj).parent();
+ if (GENTICS.Utils.Dom.isSplitObject(parent[0])) {
+ if (fillUpElement) {
+ parent.html(fillUpElement);
+ } else {
+ parent.empty();
+ }
+
+ } else {
+ // if the parent is no blocklevel element and would be empty now, we completely remove it
+ parent.remove();
+ }
+ }
+ if (completeText.length - el.startOffset > 0) {
+ // first check if there is text left to put in the followUpContainer's textnode. this happens, when ENTER is pressed inside of a textnode, but not at the borders
+ mirrorLevel[i].data = completeText.substr(el.startOffset, completeText.length);
+ } else if (mirrorLevel.length > 1) {
+ // if not, check if the followUpContainer contains more than one node, because if yes, the "empty" textnode can be removed
+ mirrorLevel.eq((i)).remove();
+ } else if (GENTICS.Utils.Dom.isBlockLevelElement(followUpContainer[0])) {
+ // if the "empty" textnode is the last node left in the followUpContainer (which is a blocklevel element), replace it with a ephemera break
+ if (fillUpElement) {
+ followUpContainer.html(fillUpElement);
+ } else {
+ followUpContainer.empty();
+ }
+
+ } else {
+ // if the "empty" textnode is the last node left in a non-blocklevel element, mark it for removal
+ followUpContainer.empty();
+ followUpContainer.addClass('preparedForRemoval');
+ }
+ }
+
+ startMoving = true;
+
+ if (el.children.length > 0) {
+ this.splitRangeObjectHelper(el.children, rangeObject, mirrorLevel.eq(i), inBetweenMarkup);
+ }
+
+ } else {
+ // remove all objects in the origin, which are AFTER the cursor
+ if (el.selection === 'none' && startMoving === true) {
+ // iteration is after cursor, remove from splitObject and leave this part inside the followUpContainer
+ jqObj = jQuery(el.domobj).remove();
+ }
+ }
+ }
+ }
+ } else {
+ Aloha.Log.error(this, 'can not split splitObject due to an empty selection tree');
+ }
+
+ // and finally cleanup: remove all fillUps > 1
+ splitObject.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one
+ followUpContainer.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one
+
+ // remove objects prepared for removal
+ splitObject.find('.preparedForRemoval').remove();
+ followUpContainer.find('.preparedForRemoval').remove();
+
+ // if splitObject / followUp are empty, place a fillUp inside
+ if (splitObject.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(splitObject[0]) && fillUpElement) {
+ splitObject.html(fillUpElement);
+ }
+
+ if (followUpContainer.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(followUpContainer[0]) && fillUpElement) {
+ followUpContainer.html(fillUpElement);
+ }
+ },
+
+ /**
+ * returns a jQuery object fitting the passed splitObject as follow up object
+ * examples,
+ * - when passed a p it will return an empty p (clone of the passed p)
+ * - when passed an h1, it will return either an h1 (clone of the passed one) or a new p (if the collapsed selection was at the end)
+ * @param rangeObject Aloha.RangeObject
+ * @return void
+ */
+ getSplitFollowUpContainer: function (rangeObject) {
+ var tagName = rangeObject.splitObject.nodeName.toLowerCase(),
+ returnObj,
+ inside,
+ lastObj;
+
+ switch (tagName) {
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ // get the last textnode in the splitobject, but don't consider aloha-cleanme elements
+ lastObj = jQuery(rangeObject.splitObject).textNodes(':not(.aloha-cleanme)').last()[0];
+ // special case: when enter is hit at the end of a heading, the followUp should be a <p>
+ if (lastObj && rangeObject.startContainer === lastObj && rangeObject.startOffset === lastObj.length) {
+ returnObj = jQuery('<p></p>');
+ inside = jQuery(rangeObject.splitObject.outerHTML).contents();
+ returnObj.append(inside);
+ return returnObj;
+ }
+ break;
+
+ case 'li':
+ // TODO check whether the li is the last one
+ // special case: if enter is hit twice inside a list, the next item should be a <p> (and inserted outside the list)
+ if (rangeObject.startContainer.nodeName.toLowerCase() === 'br' && jQuery(rangeObject.startContainer).hasClass('aloha-ephemera')) {
+ returnObj = jQuery('<p></p>');
+ inside = jQuery(rangeObject.splitObject.outerHTML).contents();
+ returnObj.append(inside);
+ return returnObj;
+ }
+ // when the li is the last one and empty, we also just return a <p>
+ if (!rangeObject.splitObject.nextSibling && jQuery.trim(jQuery(rangeObject.splitObject).text()).length === 0) {
+ returnObj = jQuery('<p></p>');
+ return returnObj;
+ }
+ break;
+ }
+
+ return jQuery(rangeObject.splitObject.outerHTML);
+ },
+
+ /**
+ * Transform the given domobj into an object with the given new nodeName.
+ * Preserves the content and all attributes. If a range object is given, also the range will be preserved
+ * @param domobj dom object to transform
+ * @param nodeName new node name
+ * @param range range object
+ * @api
+ * @return new object as jQuery object
+ */
+ transformDomObject: function (domobj, nodeName, range) {
+ // first create the new element
+ var jqOldObj = jQuery(domobj),
+ jqNewObj = jQuery('<' + nodeName + '>'),
+ i,
+ attributes = jqOldObj[0].cloneNode(false).attributes;
+
+ // TODO what about events?
+ // copy attributes
+ if (attributes) {
+ for (i = 0; i < attributes.length; ++i) {
+ if (typeof attributes[i].specified === 'undefined' || attributes[i].specified) {
+ jqNewObj.attr(attributes[i].nodeName, attributes[i].nodeValue);
+ }
+ }
+ }
+
+ // copy inline CSS
+ if (jqOldObj[0].style && jqOldObj[0].style.cssText) {
+ jqNewObj[0].style.cssText = jqOldObj[0].style.cssText;
+ }
+
+ // now move the contents of the old dom object into the new dom object
+ jqOldObj.contents().appendTo(jqNewObj);
+
+ // finally replace the old object with the new one
+ jqOldObj.replaceWith(jqNewObj);
+
+ // preserve the range
+ if (range) {
+ if (range.startContainer == domobj) {
+ range.startContainer = jqNewObj.get(0);
+ }
+
+ if (range.endContainer == domobj) {
+ range.endContainer = jqNewObj.get(0);
+ }
+ }
+
+ return jqNewObj;
+ },
+
+ /**
+ * String representation
+ * @return {String}
+ */
+ toString: function () {
+ return 'Aloha.Markup';
+ }
+
+ });
+
+ Aloha.Markup = new Aloha.Markup();
+ return Aloha.Markup;
+});