/**
* boundaries.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-2014 Gentics Software GmbH, Vienna, Austria.
* Contributors http://aloha-editor.org/contribution.php
*/
define([
'dom',
'misc',
'arrays',
'assert',
'strings'
], /** @exports Boundaries */ function Boundaries(
Dom,
Misc,
Arrays,
Assert,
Strings
) {
'use strict';
/**
* Creates a "raw" (un-normalized) boundary from the given node and offset.
*
* @param {Node} node
* @param {number} offset
* @return {Boundary}
*/
function raw(node, offset) {
return [node, offset];
}
/**
* Returns a boundary's container node.
*
* @param {Boundary} boundary
* @return {Node}
*/
function container(boundary) {
return boundary[0];
}
/**
* Returns a boundary's offset.
*
* @param {Boundary} boundary
* @return {number}
*/
function offset(boundary) {
return boundary[1];
}
/**
* Returns a boundary that is right in front of the given node.
*
* @param {Node} node
* @return {Boundary}
*/
function fromNode(node) {
return raw(node.parentNode, Dom.nodeIndex(node));
}
/**
* Returns a boundary that is at the end position of the given node.
*
* @param {Node} node
* @return {Boundary}
*/
function fromEndOfNode(node) {
return raw(node, Dom.nodeLength(node));
}
/**
* Normalizes the boundary point (represented by a container and an offset
* tuple) such that it will not point to the start or end of a text node.
*
* This normalization reduces the number of states the a boundary can be
* in, and thereby slightly increases the robusteness of the code written
* against it.
*
* It should be noted that native ranges controlled by the browser's DOM
* implementation have the habit of changing by themselves, so even if a
* range is set using a boundary that has been normalized this way, the
* range could revert to an un-normalized state. See StableRange().
*
* The returned value will either be a normalized copy of the given
* boundary, or the given boundary itself if no normalization was done.
*
* @param {Boundary} boundary
* @return {Boundary}
*/
function normalize(boundary) {
var node = container(boundary);
if (Dom.isTextNode(node)) {
Assert.assertNotNou(node.parentNode);
var boundaryOffset = offset(boundary);
if (0 === boundaryOffset) {
return fromNode(node);
}
if (boundaryOffset >= Dom.nodeLength(node)) {
return raw(node.parentNode, Dom.nodeIndex(node) + 1);
}
}
return boundary;
}
/**
* Creates a node boundary representing an offset position inside of a
* container node.
*
* The resulting boundary will be a normalized boundary, such that the
* boundary will never describe a terminal position in a text node.
*
* @param {Node} node
* @param {number} offset Positive integer
* @return {Boundary}
*/
function create(node, offset) {
return normalize(raw(node, offset));
}
/**
* Compares two boundaries for equality. Boundaries are equal if their
* corresponding containers and offsets are strictly equal.
*
* @param {Boundary} a
* @param {Boundary} b
* @retufn {boolean}
*/
function equals(a, b) {
return (container(a) === container(b)) && (offset(a) === offset(b));
}
/**
* Sets the given range's start boundary.
*
* @param {Range} range Range to modify.
* @param {Boundary} boundary
*/
function setRangeStart(range, boundary) {
boundary = normalize(boundary);
range.setStart(container(boundary), offset(boundary));
}
/**
* Sets the given range's end boundary.
*
* @param {Range} range Range to modify
* @param {Boundary}
*/
function setRangeEnd(range, boundary) {
boundary = normalize(boundary);
range.setEnd(container(boundary), offset(boundary));
}
/**
* Sets the given range's start and end position from two respective
* boundaries.
*
* @param {Range} range Range to modify.
* @param {Boundary} start Boundary to set the start position to
* @param {Boundary} end Boundary to set the end position to
*/
function setRange(range, start, end) {
setRangeStart(range, start);
setRangeEnd(range, end);
}
/**
* Sets the start and end position of a list of ranges from the given list
* of boundaries.
*
* Because the range at index i in `ranges` will be modified using the
* boundaries at index 2i and 2i + 1 in `boundaries`, the size of `ranges`
* must be no less than half the size of `boundaries`.
*
* Because the list of boundaries will need to be partitioned into pairs of
* start/end tuples, it is required that the length of `boundaries` be
* even. See Arrays.partition().
*
* @param {Array.<Range>} ranges List of ranges to modify
* @param {Array.<Boundary>} boundaries Even list of boundaries
*/
function setRanges(ranges, boundaries) {
Arrays.partition(boundaries, 2).forEach(function (boundaries, i) {
setRange(ranges[i], boundaries[0], boundaries[1]);
});
}
/**
* Creates a boundary from the given range's start position.
*
* @param {Range} range
* @return {Boundary}
*/
function fromRangeStart(range) {
return create(range.startContainer, range.startOffset);
}
/**
* Creates a boundary from the given range's end position.
*
* @param {Range} range
* @return {Boundary}
*/
function fromRangeEnd(range) {
return create(range.endContainer, range.endOffset);
}
/**
* Returns a start/end boundary tuple representing the start and end
* positions of the given range.
*
* @param {Range} range
* @return {Array.<Boundary>}
*/
function fromRange(range) {
return [fromRangeStart(range), fromRangeEnd(range)];
}
/**
* Returns an even-sized contiguous sequence of start/end boundaries
* aligned in their pairs.
*
* @param {Array.<Range>} ranges
* @return {Array.<Boundary>}
*/
function fromRanges(ranges) {
// TODO: after refactoring range-preserving functions to use
// boundaries we can remove this.
ranges = ranges || [];
return Arrays.mapcat(ranges, fromRange);
}
/**
* Checks if a boundary (when normalized) represents a position at the
* start of its container's content.
*
* The start boundary of the given ranges is at the start position:
* <b><i>f</i>[oo]</b> and <b><i>{f</i>oo}</b>
* The first is at the start of the text node "oo" and the other at start
* of the <i> element.
*
* @param {Boundary} boundary
* @return {boolean}
*/
function isAtStart(boundary) {
return 0 === offset(normalize(boundary));
}
/**
* Checks if a boundary represents a position at the end of its container's
* content.
*
* The end boundary of the given ranges is at the end position:
* <b><i>f</i>{oo]</b> and <b><i>f</i>{oo}</b>
* The first is at end of the text node "oo"and the other at end of the <b>
* element.
*
* @param {Boundary} boundary
* @return {boolean}
*/
function isAtEnd(boundary) {
boundary = normalize(boundary);
return offset(boundary) === Dom.nodeLength(container(boundary));
}
/**
* Checks if the unnormalized boundary is at the start position of it's
* container.
*
* @param {Boundary} boundary
* @return {boolean}
*/
function isAtRawStart(boundary) {
return 0 === offset(boundary);
}
/**
* Checks if the unnormalized boundary is at the end position of it's
* container.
*
* @param {Boundary} boundary
* @return {boolean}
*/
function isAtRawEnd(boundary) {
return offset(boundary) === Dom.nodeLength(container(boundary));
}
/**
* Checks whether the given boundary is a position inside of a text nodes.
*
* @param {Boundary} boundary
* @return {boolean}
*/
function isTextBoundary(boundary) {
return Dom.isTextNode(container(boundary));
}
/**
* Checks whether the given boundary is a position between nodes (as
* opposed to a position inside of a text node).
*
* @param {Boundary} boundary
* @return {boolean}
*/
function isNodeBoundary(boundary) {
return !isTextBoundary(boundary);
}
/**
* Returns the node that is after the given boundary position.
* Will return null if the given boundary is at the end position.
*
* Note that the given boundary will be normalized.
*
* @param {Boundary} boundary
* @return {Node}
*/
function nodeAfter(boundary) {
boundary = normalize(boundary);
return isAtEnd(boundary) ? null : Dom.nthChild(container(boundary), offset(boundary));
}
/**
* Returns the node that is before the given boundary position.
* Will returns null if the given boundary is at the start position.
*
* Note that the given boundary will be normalized.
*
* @param {Boundary} boundary
* @return {Node}
*/
function nodeBefore(boundary) {
boundary = normalize(boundary);
return isAtStart(boundary) ? null : Dom.nthChild(container(boundary), offset(boundary) - 1);
}
/**
* Returns the node after the given boundary, or the boundary's container
* if the boundary is at the end position.
*
* @param {Boundary} boundary
* @return {Node}
*/
function nextNode(boundary) {
boundary = normalize(boundary);
return nodeAfter(boundary) || container(boundary);
}
/**
* Returns the node before the given boundary, or the boundary container if
* the boundary is at the end position.
*
* @param {Boundary} boundary
* @return {Node}
*/
function prevNode(boundary) {
boundary = normalize(boundary);
return nodeBefore(boundary) || container(boundary);
}
/**
* Skips the given boundary over the node that is next to the boundary.
*
* @param {Boundary} boundary
* @return {Boundary}
*/
function jumpOver(boundary) {
var node = nextNode(boundary);
return raw(node.parentNode, Dom.nodeIndex(node) + 1);
}
/**
* Returns a boundary that is at the previous position to the given.
*
* If the given boundary represents a position inside of a text node, the
* returned boundary will be moved behind that text node.
*
* Given the markup below:
*
* <div>
* foo
* <p>
* bar
* <b>
* <u></u>
* baz
* </b>
* </p>
* </div>
*
* the boundary positions which can be traversed with this function are
* those marked with the pipe ("|") below:
*
* |foo|<p>|bar|<b>|<u>|</u>|baz|<b>|</p>|
*
* This function complements Boundaries.next()
*
* @param {Boundary} boundary
* @return {Boundary}
*/
function prev(boundary) {
boundary = normalize(boundary);
var node = container(boundary);
if (Dom.isTextNode(node) || isAtStart(boundary)) {
return fromNode(node);
}
node = Dom.nthChild(node, offset(boundary) - 1);
return Dom.isTextNode(node)
? fromNode(node)
: raw(node, Dom.nodeLength(node));
}
/**
* Like Boundaries.prev(), but returns the boundary position that follows
* from the given.
*
* @param {Boundary} boundary
* @return {Boundary}
*/
function next(boundary) {
boundary = normalize(boundary);
var node = container(boundary);
var boundaryOffset = offset(boundary);
if (Dom.isTextNode(node) || isAtEnd(boundary)) {
return jumpOver(boundary);
}
var nextNode = Dom.nthChild(node, boundaryOffset);
return Dom.isTextNode(nextNode)
? raw(nextNode.parentNode, boundaryOffset + 1)
: raw(nextNode, 0);
}
/**
* Like Boundaries.prev() but treats the given boundary as an unnormalized
* boundary.
*
* @param {Boundary} boundary
* @return {Boundary}
*/
function prevRawBoundary(boundary) {
var node = container(boundary);
if (isAtRawStart(boundary)) {
return fromNode(container(boundary));
}
if (isTextBoundary(boundary)) {
return raw(container(boundary), 0);
}
node = Dom.nthChild(node, offset(boundary) - 1);
return fromEndOfNode(node);
}
/**
* Like Boundaries.next() but treats the given boundary as an unnormalized
* boundary.
*
* @param {Boundary} boundary
* @return {Boundary}
*/
function nextRawBoundary(boundary) {
var node = container(boundary);
if (isAtRawEnd(boundary)) {
return jumpOver(boundary);
}
if (isTextBoundary(boundary)) {
return fromEndOfNode(node);
}
return raw(Dom.nthChild(node, offset(boundary)), 0);
}
/**
* Steps through boundaries while the given condition is true.
*
* @param {Boundary} boundary Start position
* @param {function(Boundary):boolean} cond Predicate
* @param {function(Boundary):Boundary} step Gets the next boundary
* @return {Boundary}
*/
function stepWhile(boundary, cond, step) {
var pos = boundary;
while (cond(pos)) {
pos = step(pos);
}
return pos;
}
/**
* Steps forward while the given condition is true.
*
* @param {Boundary} boundary
* @param {function(Boundary):boolean} cond
* @return {Boundary}
*/
function nextWhile(boundary, cond) {
return stepWhile(boundary, cond, next);
}
/**
* Steps backwards while the given condition is true.
*
* @param {Boundary} boundary
* @param {function(Boundary):boolean} cond
* @return {Boundary}
*/
function prevWhile(boundary, cond) {
return stepWhile(boundary, cond, prev);
}
/**
* Walks along boundaries according to step(), applying callback() to each
* boundary along the traversal until cond() returns false.
*
* @param {Boundary} boundary Start position
* @param {function(Boundary):boolean} cond Predicate
* @param {function(Boundary):Boundary} step Gets the next boundary
* @param {function(Boundary)} callback Applied to each boundary
*/
function walkWhile(boundary, cond, step, callback) {
var pos = boundary;
while (pos && cond(pos)) {
callback(pos);
pos = step(pos);
}
}
/**
* Calculates the cumulative length of contiguous text nodes immediately
* preceding the given boundary.
*
* @param {Boundary} boundary
* @return {number}
*/
function precedingTextLength(boundary) {
var node, len;
boundary = normalize(boundary);
if (isNodeBoundary(boundary)) {
len = 0;
node = nodeBefore(boundary);
} else {
len += offset(boundary);
node = container(boundary).previousSibling;
}
while (node && Dom.isTextNode(node)) {
len += Dom.nodeLength(node);
node = node.previousSibling;
}
return len;
}
/**
* Gets the boundaries of the currently selected range from the given
* document element.
*
* If no document element is given, the document element of the calling
* frame's window will be used.
*
* @param {Document=} doc
* @return {?Array<Boundary>}
*/
function get(doc) {
var selection = (doc || document).getSelection();
return (selection.rangeCount > 0)
? fromRange(selection.getRangeAt(0))
: null;
}
/**
* Sets the a range to the browser selection according to the given start
* and end boundaries. This operation will cause the selection to be
* visually rendered by the user agent.
*
* @param {Boundary} start
* @param {Boundary=} end
*/
function select(start, end) {
if (!end) {
end = start;
}
var sc = container(start);
var so = offset(start);
var ec = container(end);
var eo = offset(end);
var doc = sc.ownerDocument;
var selection = doc.getSelection();
var range = doc.createRange();
range.setStart(sc, so);
range.setEnd(ec, eo);
selection.removeAllRanges();
selection.addRange(range);
}
/**
* Return the ancestor container that contains both the given boundaries.
*
* @param {Boundary} start
* @param {Boundary} end
* @return {Node}
*/
function commonContainer(start, end) {
var sc = container(start);
var so = offset(start);
var ec = container(end);
var eo = offset(end);
var doc = Dom.Nodes.DOCUMENT === sc.nodeType ? sc : sc.ownerDocument;
var range = doc.createRange();
range.setStart(sc, so);
range.setEnd(ec, eo);
return range.commonAncestorContainer;
}
return {
get : get,
select : select,
raw : raw,
create : create,
normalize : normalize,
equals : equals,
container : container,
offset : offset,
fromRange : fromRange,
fromRanges : fromRanges,
fromRangeStart : fromRangeStart,
fromRangeEnd : fromRangeEnd,
fromNode : fromNode,
fromEndOfNode : fromEndOfNode,
/* these functions should be in ranges.js */
setRange : setRange,
setRanges : setRanges,
setRangeStart : setRangeStart,
setRangeEnd : setRangeEnd,
isAtStart : isAtStart,
isAtEnd : isAtEnd,
isAtRawStart : isAtRawStart,
isAtRawEnd : isAtRawEnd,
isTextBoundary : isTextBoundary,
isNodeBoundary : isNodeBoundary,
next : next,
prev : prev,
nextRawBoundary : nextRawBoundary,
prevRawBoundary : prevRawBoundary,
jumpOver : jumpOver,
nextWhile : nextWhile,
prevWhile : prevWhile,
stepWhile : stepWhile,
walkWhile : walkWhile,
nextNode : nextNode,
prevNode : prevNode,
nodeAfter : nodeAfter,
nodeBefore : nodeBefore,
precedingTextLength : precedingTextLength,
commonContainer : commonContainer
};
});