1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-18 15:08:22 +00:00
seahub/media/aloha-0.22.7/lib/aloha/markup.js

1273 lines
43 KiB
JavaScript
Raw Normal View History

/* 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(/^(&nbsp;)*$/, '').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>&nbsp;</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;
});