/* range-context.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([ 'util/dom2', 'util/arrays', 'util/trees', 'util/functions', 'util/html' ], function ( Dom, Arrays, Trees, Fn, Html ) { 'use strict'; /** * Walks the siblings of the given child, calling before for * siblings before the given child, after for siblings after the * given child, and at for the given child. */ function walkSiblings(parent, beforeAfterChild, before, at, after, arg) { var fn = before; Dom.walk(parent.firstChild, function (child) { if (child !== beforeAfterChild) { return fn(child, arg); } else { fn = after; return at(child, arg); } }); } /** * Walks the siblings of each node in the given array (see * walkSiblings()). * * @param ascendNodes from lowest descendant to topmost parent. The * topmost parent and its siblings will not be walked over. * * @param atEnd indicates that the position to ascend from is not at * ascendNodes[0], but at the end of ascendNodes[0] (meaning that * all of ascendNodes[0]'s children will be walked over as well). * * @param carryDown is invoked on each node in the given array, * allowing the carrying down of a context value. May return null * to return the carryDown value from above. */ function ascendWalkSiblings(ascendNodes, atEnd, carryDown, before, at, after, arg) { var i; var args = []; i = ascendNodes.length; while (i--) { arg = carryDown(ascendNodes[i], arg) || arg; args.push(arg); } args.reverse(); // Because with end positions like // text{ or text} // ascendecending would start at ignoring "text". if (ascendNodes.length && atEnd) { Dom.walk(ascendNodes[0].firstChild, before, args[0]); } for (i = 0; i < ascendNodes.length - 1; i++) { var child = ascendNodes[i]; var parent = ascendNodes[i + 1]; walkSiblings(parent, child, before, at, after, args[i + 1]); } } /** * Walks the boundary of the range. * * The range's boundary starts at startContainer/startOffset, goes * up to to the commonAncestorContainer's child above or equal * startContainer/startOffset, continues to the * commonAnestorContainer's child above or equal to * endContainer/endOffset, and goes down again to * endContainer/endOffset. * * Requires range's boundary points to be between nodes * (Dom.splitTextContainers). */ function walkBoundary(liveRange, carryDown, stepOutside, stepPartial, stepInside, arg) { // Because range may be mutated during traversal, we must only // refer to it before traversal. var cac = liveRange.commonAncestorContainer; var sc = liveRange.startContainer; var ec = liveRange.endContainer; var so = liveRange.startOffset; var eo = liveRange.endOffset; var start = Dom.nodeAtOffset(sc, so); var end = Dom.nodeAtOffset(ec, eo); var startEnd = Dom.isAtEnd(sc, so); var endEnd = Dom.isAtEnd(ec, eo); var uptoCacChildStart = Dom.childAndParentsUntilNode(start, cac); var uptoCacChildEnd = Dom.childAndParentsUntilNode(end, cac); var cacChildStart = uptoCacChildStart.length ? uptoCacChildStart[uptoCacChildStart.length - 1] : null; var cacChildEnd = uptoCacChildEnd.length ? uptoCacChildEnd[uptoCacChildEnd.length - 1] : null; arg = carryDown(cac, arg) || arg; // Because the start node is inside the range, the end node is // outside, and all ancestors of start and end are partially // inside/outside (for startEnd/endEnd positions the nodes are // also ancestors of the position). function stepAtStart(node, arg) { return node === start && !startEnd ? stepInside(node, arg) : stepPartial(node, arg); } function stepAtEnd(node, arg) { return node === end && !endEnd ? stepOutside(node, arg) : stepPartial(node, arg); } ascendWalkSiblings(uptoCacChildStart, startEnd, carryDown, stepOutside, stepAtStart, stepInside, arg); ascendWalkSiblings(uptoCacChildEnd, endEnd, carryDown, stepInside, stepAtEnd, stepOutside, arg); if (cacChildStart && cacChildStart !== cacChildEnd) { var next; Dom.walkUntilNode(cac.firstChild, stepOutside, cacChildStart, arg); next = stepAtStart(cacChildStart, arg); Dom.walkUntilNode(next, stepInside, cacChildEnd, arg); if (cacChildEnd) { next = stepAtEnd(cacChildEnd, arg); Dom.walk(next, stepOutside, arg); } } } /** * Pushes down a context to the given range by clearing all * overrides between pushDownFrom and range.commonAncestorContainer, * and clearing all overrides inside and along the range's boundary * (see walkBoundary()), invoking pushDownOverride on all siblings * of the range boundary that are not contained in it. * * Requires range's boundary points to be between nodes * (Dom.splitTextContainers). */ function pushDownContext(liveRange, pushDownFrom, cacOverride, getOverride, clearOverride, clearOverrideRec, pushDownOverride) { // Because range may be mutated during traversal, we must only // refer to it before traversal. var cac = liveRange.commonAncestorContainer; walkBoundary(liveRange, getOverride, pushDownOverride, clearOverride, clearOverrideRec, cacOverride); var fromCacToTop = Dom.childAndParentsUntilInclNode(cac, pushDownFrom); ascendWalkSiblings(fromCacToTop, false, getOverride, pushDownOverride, clearOverride, pushDownOverride, null); clearOverride(pushDownFrom); } /** * Walks around the boundary of range and invokes the given * functions with the nodes it encounters. * * clearOverride - invoked for partially contained nodes. * clearOverrideRec - invoked for top-level contained nodes. * pushDownOverride - invoked for left siblings of ancestors * of startContainer[startOffset], and for right siblings of * ancestors of endContainer[endOffset]. * setContext - invoked for top-level contained nodes. * * The purpose of the walk is to either push-down or set a context * on all nodes within the range, and push-down any overrides that * exist along the bounderies of the range. * * An override is a context that overrides the context to set. * * Pushing-down a context means that an existing context-giving * ancestor element will be reused, if available, and setContext() * will not be invoked. * * Pushing-down an override means that ancestors of the range's * start or end containers will have their overrides cleared and the * subset of the ancestors' children that is not contained by the * range will have the override applied via pushDownOverride(). * * This algorithm will not by itself mutate anything, or depend on * any mutations by the given functions. * * clearOverride, clearOverideRec, setContext, pushDownContext may * mutate the given node and it's previous siblings, and may insert * nextSiblings, but must not mutate the next sibling of the given * node, and must return the nextSibling of the given node (the * nextSibling before any mutations). * * When setContext is invoked with hasOverrideAncestor, it is for * example when a bold element is at the same time the upper * boundary (for example when the bold element itself is the editing * host) and an attempt is made to set a non-bold context inside the * bold element. To work around this, setContext() could force a * non-bold context by wrapping the node with a . See hasOverrideAncestor below. * * @param liveRange range's boundary points should be between nodes * (Dom.splitTextContainers). * * @param isUpperBoundary args (node). Identifies exclusive upper * boundary element, only elements below which will be modified. * * @param getOverride(node). Returns a node's override, or * null if the node does not provide an override. The topmost node * for which getOverride returns a non-null value is the topmost * override. If there is a topmost override, and it is below the * upper boundary element, it will be cleared and pushed down. * * @param clearOverride(node). Should clear the given node of an * override. The given node may or may not have an override * set. Will be invoked shallowly for all ancestors of start and end * containers (up to isUpperBoundary or hasContext). May perform * mutations as explained above. * * @parma clearOverrideRec(node). Like clearOverride but * should clear the override recursively. * * @param pushDownOverride(node, override). Applies the given * override to node. Should check whether the given node doesn't * already provide its own override, in which case the given * override should not be applied. May perform mutations as * explained above. * * @param hasContext(node). Returns true if the given node * already provides the context to set. * * @param setContext(node, hasOverrideAncestor). Applies the context * to the given node. Should clear overrides recursively. Should * also clear context recursively to avoid unnecessarily nested * contexts. hasOverrideAncestor is true if an override is in effect * above the given node (see explanation above). May perform * mutations as explained above. */ function mutate(liveRange, isUpperBoundary, getOverride, clearOverride, clearOverrideRec, pushDownOverride, hasContext, setContext, rootHasContext) { if (liveRange.collapsed) { return; } // Because range may be mutated during traversal, we must only // refer to it before traversal. var cac = liveRange.commonAncestorContainer; var topmostOverrideNode = null; var bottommostOverrideNode = null; var isNonClearableOverride = false; var upperBoundaryAndBeyond = false; var fromCacToContext = Dom.childAndParentsUntilIncl(cac, hasContext); Arrays.forEach(fromCacToContext, function (node) { upperBoundaryAndBeyond = upperBoundaryAndBeyond || isUpperBoundary(node); if (getOverride(node)) { topmostOverrideNode = node; isNonClearableOverride = upperBoundaryAndBeyond; bottommostOverrideNode = bottommostOverrideNode || node; } }); if ((rootHasContext || hasContext(fromCacToContext[fromCacToContext.length - 1])) && !isNonClearableOverride) { var pushDownFrom = topmostOverrideNode || cac; var cacOverride = getOverride(bottommostOverrideNode || cac); pushDownContext(liveRange, pushDownFrom, cacOverride, getOverride, clearOverride, clearOverrideRec, pushDownOverride); } else { walkBoundary(liveRange, getOverride, pushDownOverride, clearOverride, function (node) { return setContext(node, isNonClearableOverride); }); } } function adjustPointShallowRemove(point, left, node) { if (point.node === node) { point.next(); } } function adjustPointMoveBackWithinRange(point, left, node, ref, atEnd) { if (point.node === node) { // Because Left positions will be moved back with the node, // which is correct, while right positions must stay where // they are. // Because right positions with point.atEnd == true/false // must both stay where they are, we don't need an extra // check for point.atEnd. if (!left) { point.next(); } } // Because trimRangeClosingOpening will ensure that the boundary // points will be next to a node that is moved, we don't need // any special handling for ref. } function adjustPointWrap(point, left, node, wrapper) { // Because we prefer the range to be outside the wrapper (no // particular reason though). if (point.node === node && !point.atEnd) { point.node = wrapper; } } function shallowRemoveAdjust(node, leftPoint, rightPoint) { adjustPointShallowRemove(leftPoint, true, node); adjustPointShallowRemove(rightPoint, false, node); Dom.shallowRemove(node); } function wrapAdjust(node, wrapper, leftPoint, rightPoint) { if (wrapper.parentNode) { shallowRemoveAdjust(wrapper, leftPoint, rightPoint); } adjustPointWrap(leftPoint, true, node, wrapper); adjustPointWrap(rightPoint, false, node, wrapper); Dom.wrap(node, wrapper); } function insertAdjust(node, ref, atEnd, leftPoint, rightPoint) { adjustPointMoveBackWithinRange(leftPoint, true, node, ref, atEnd); adjustPointMoveBackWithinRange(rightPoint, false, node, ref, atEnd); Dom.insert(node, ref, atEnd); } function nextSibling(node) { return node.nextSibling; } // TODO when restacking the that wraps "z" in // xz, join with the that wraps "x". function restackRec(node, hasContext, notIgnoreHorizontal, notIgnoreVertical) { if (1 !== node.nodeType || notIgnoreVertical(node)) { return null; } var maybeContext = Dom.walkUntil(node.firstChild, nextSibling, notIgnoreHorizontal); if (!maybeContext) { return null; } var notIgnorable = Dom.walkUntil(maybeContext.nextSibling, nextSibling, notIgnoreHorizontal); if (notIgnorable) { return null; } if (hasContext(maybeContext)) { return maybeContext; } return restackRec(maybeContext, hasContext, notIgnoreHorizontal, notIgnoreVertical); } function restack(node, hasContext, ignoreHorizontal, ignoreVertical, leftPoint, rightPoint) { var notIgnoreHorizontal = function (node) { return hasContext(node) || !ignoreHorizontal(node); }; var notIgnoreVertical = Fn.complement(ignoreVertical); if (hasContext(node)) { return true; } var context = restackRec(node, hasContext, notIgnoreHorizontal, notIgnoreVertical); if (!context) { return false; } wrapAdjust(node, context, leftPoint, rightPoint); return true; } function format(liveRange, nodeName, unformat) { var leftPoint; var rightPoint; function hasContext(node) { if (unformat) { // Because we pass rootHasContext=true to mutate. return false; } return nodeName === node.nodeName; } function getOverride(node) { if (!unformat) { return false; } return nodeName === node.nodeName; } function hasOverride(node) { return !!getOverride(node); } function clearOverride(node) { var next = node.nextSibling; if (unformat && nodeName === node.nodeName) { shallowRemoveAdjust(node, leftPoint, rightPoint); } return next; } function clearOverrideRec(node) { return Dom.walkRec(node, clearOverride); } function clearContext(node) { var next = node.nextSibling; if (!unformat && nodeName === node.nodeName) { shallowRemoveAdjust(node, leftPoint, rightPoint); } return next; } function clearContextRec(node) { return Dom.walkRec(node, clearContext); } function ensureWrapper(node, hasWrapper) { if (node.previousSibling && !hasWrapper(node.previousSibling)) { // Because restacking here solves two problems: one the // case where the context was unnecessarily pushed down // on the left of the range, and two to join with a // context node that already exists to the left of the // range. restack(node.previousSibling, hasWrapper, Html.isIgnorableWhitespace, Html.isInlineFormattable, leftPoint, rightPoint); } if (node.previousSibling && hasWrapper(node.previousSibling)) { insertAdjust(node, node.previousSibling, true, leftPoint, rightPoint); return true; } else if (!hasWrapper(node)) { var wrapper = document.createElement(nodeName); wrapAdjust(node, wrapper, leftPoint, rightPoint); return true; } return false; } function pushDownOverride(node, override) { if (!override) { return node.nextSibling; } if (!unformat) { throw "not implemented"; } var next = node.nextSibling; ensureWrapper(node, hasOverride); return next; } function setContext(node) { if (unformat) { throw "not implemented"; } var next = node.nextSibling; if (ensureWrapper(node, hasContext)) { // Because the node was wrapped with a context, and if // the node itself has the context, it should be cleared // to avoid nested contexts. clearContextRec(node); } else { // Because the node itself has the context and was not // wrapped, we must only clear its children. Dom.walk(node.firstChild, clearContextRec); } clearOverrideRec(node); return next; } function isUpperBoundary(node) { return 'BODY' === node.nodeName; } // Because we are mutating the range several times and don't // want the caller to see the in-between updates, and because we // are using trimRange() below to adjust the range's boundary // points, which we don't want the browser to re-adjust (which // some browsers do). var range = Dom.stableRange(liveRange); // Because we should avoid splitTextContainers() if this call is a noop. if (range.collapsed) { return; } // Because trimRangeClosingOpening(), mutate() and // adjustPointMoveBackWithinRange() require boundary points to // be between nodes. Dom.splitTextContainers(range); // Because we want unbolding // onetwo{three} // to result in // onetwothree // and not in // onetwothree // and because adjustPointMoveBackWithinRange() requires the // left boundary point to be next to a non-ignorable node. Dom.trimRangeClosingOpening(range, Html.isIgnorableWhitespace); // Because mutation needs to keep track and adjust boundary // points. leftPoint = Dom.cursorFromBoundaryPoint(range.startContainer, range.startOffset); rightPoint = Dom.cursorFromBoundaryPoint(range.endContainer, range.endOffset); mutate(range, isUpperBoundary, getOverride, clearOverride, clearOverrideRec, pushDownOverride, hasContext, setContext, unformat); // Because we must reflect the adjusted boundary points in the // given range. Dom.setRangeStartFromCursor(liveRange, leftPoint); Dom.setRangeEndFromCursor(liveRange, rightPoint); } return { mutate: mutate, format: format }; });