Source: cursors.js

/**
 * cursors.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',
	'boundaries'
], /** @exports Cursors */ function Cursors(
	Dom,
	Boundaries
) {
	'use strict';

	/**
	 * Cursor abstraction of the startContainer/startOffset and
	 * endContainer/endOffset range boundary points.
	 *
	 * @type {Cursor}
	 */
	function Cursor(node, atEnd) {
		this.node = node;
		this.atEnd = atEnd;
	}

	/**
	 * Creates a cursor instance.
	 *
	 * A cursor has the added utility over other iteration methods of iterating
	 * over the end position of an element. The start and end positions of an
	 * element are immediately before the element and immediately after the last
	 * child respectively. All node positions except end positions can be
	 * identified just by a node. To distinguish between element start and end
	 * positions, the additional atEnd boolean is necessary.
	 *
	 * @param {Node} node
	 *        The container in which the cursor is in.
	 * @param {boolean} atEnd
	 *        Whether or not the cursor is at the end of the container.
	 * @return {Cursor}
	 */
	function create(node, atEnd) {
		return new Cursor(node, atEnd);
	}

	/**
	 * Creates a new cursor from the given container and offset.
	 *
	 * @param {Node} container
	 *        If a text node, should have a parent node.
	 * @param {Number} offset
	 *        If container is a text node, the offset will be ignored.
	 * @return {Cursor}
	 */
	function createFromBoundary(container, offset) {
		return create(
			Dom.nodeAtOffset(container, offset),
			Boundaries.isAtEnd(Boundaries.raw(container, offset))
		);
	}

	Cursor.prototype.next = function () {
		var node = this.node;
		var next;
		if (this.atEnd || !Dom.isElementNode(node)) {
			next = node.nextSibling;
			if (next) {
				this.atEnd = false;
			} else {
				next = node.parentNode;
				if (!next) {
					return false;
				}
				this.atEnd = true;
			}
			this.node = next;
		} else {
			next = node.firstChild;
			if (next) {
				this.node = next;
			} else {
				this.atEnd = true;
			}
		}
		return true;
	};

	Cursor.prototype.prev = function () {
		var node = this.node;
		var prev;
		if (this.atEnd) {
			prev = node.lastChild;
			if (prev) {
				this.node = prev;
				if (!Dom.isElementNode(prev)) {
					this.atEnd = false;
				}
			} else {
				this.atEnd = false;
			}
		} else {
			prev = node.previousSibling;
			if (prev) {
				if (Dom.isElementNode(prev)) {
					this.atEnd = true;
				}
			} else {
				prev = node.parentNode;
				if (!prev) {
					return false;
				}
			}
			this.node = prev;
		}
		return true;
	};

	Cursor.prototype.skipPrev = function (cursor) {
		var prev = this.prevSibling();
		if (prev) {
			this.node = prev;
			this.atEnd = false;
			return true;
		}
		return this.prev();
	};

	Cursor.prototype.skipNext = function (cursor) {
		if (this.atEnd) {
			return this.next();
		}
		this.atEnd = true;
		return this.next();
	};

	Cursor.prototype.nextWhile = function (cond) {
		while (cond(this)) {
			if (!this.next()) {
				return false;
			}
		}
		return true;
	};

	Cursor.prototype.prevWhile = function (cond) {
		while (cond(this)) {
			if (!this.prev()) {
				return false;
			}
		}
		return true;
	};

	Cursor.prototype.parent = function () {
		return this.atEnd ? this.node : this.node.parentNode;
	};

	Cursor.prototype.prevSibling = function () {
		return this.atEnd ? this.node.lastChild : this.node.previousSibling;
	};

	Cursor.prototype.nextSibling = function () {
		return this.atEnd ? null : this.node.nextSibling;
	};

	Cursor.prototype.equals = function (cursor) {
		return cursor.node === this.node && cursor.atEnd === this.atEnd;
	};

	Cursor.prototype.setFrom = function (cursor) {
		this.node = cursor.node;
		this.atEnd = cursor.atEnd;
	};

	Cursor.prototype.clone = function () {
		return create(this.node, this.atEnd);
	};

	Cursor.prototype.insert = function (node) {
		return Dom.insert(node, this.node, this.atEnd);
	};

	Cursor.prototype['next'] = Cursor.prototype.next;
	Cursor.prototype['prev'] = Cursor.prototype.prev;
	Cursor.prototype['skipPrev'] = Cursor.prototype.skipPrev;
	Cursor.prototype['skipNext'] = Cursor.prototype.skipNext;
	Cursor.prototype['nextWhile'] = Cursor.prototype.nextWhile;
	Cursor.prototype['prevWhile'] = Cursor.prototype.prevWhile;
	Cursor.prototype['parent'] = Cursor.prototype.parent;
	Cursor.prototype['prevSibling'] = Cursor.prototype.prevSibling;
	Cursor.prototype['nextSibling'] = Cursor.prototype.nextSibling;
	Cursor.prototype['equals'] = Cursor.prototype.equals;
	Cursor.prototype['setFrom'] = Cursor.prototype.setFrom;
	Cursor.prototype['clone'] = Cursor.prototype.clone;
	Cursor.prototype['insert'] = Cursor.prototype.insert;

	/**
	 * Sets the start boundary of a given range from the given range position.
	 *
	 * @param {Cursor} pos
	 * @param {Range} range
	 * @return {Range}
	 *         The modified range.
	 */
	function setRangeStart(range, pos) {
		if (pos.atEnd) {
			range.setStart(pos.node, Dom.nodeLength(pos.node));
		} else {
			range.setStart(pos.node.parentNode, Dom.nodeIndex(pos.node));
		}
		return range;
	}

	/**
	 * Sets the end boundary of a given range from the given range position.
	 *
	 * @param {Range} range
	 * @param {Cursor} pos
	 * @return {Range}
	 *         The given range, having been modified.
	 */
	function setRangeEnd(range, pos) {
		if (pos.atEnd) {
			range.setEnd(pos.node, Dom.nodeLength(pos.node));
		} else {
			range.setEnd(pos.node.parentNode, Dom.nodeIndex(pos.node));
		}
		return range;
	}

	/**
	 * Transforms a cursor to a boundary.
	 * @param {Cursor} cursor
	 * @return {Boundary}
	 */
	function toBoundary(cursor) {
		if (cursor.atEnd) {
			return Boundaries.create(cursor.node, Dom.nodeLength(cursor.node));
		}
		return Boundaries.create(cursor.node.parentNode, Dom.nodeIndex(cursor.node));
	}

	/**
	 * Sets the startContainer/startOffset and endContainer/endOffset boundary
	 * points of the given range, based on the given start and end Cursors.
	 *
	 * @param {Range} range
	 * @param {Cursor} start
	 * @param {Cursor} end
	 * @return {Range}
	 *         The given range, having had its boundary points modified.
	 */
	function setToRange(range, start, end) {
		if (start) {
			setRangeStart(range, start);
		}
		if (end) {
			setRangeEnd(range, end);
		}
		return range;
	}

	return {
		cursor                  : create,
		cursorFromBoundaryPoint : createFromBoundary,
		create                  : create,
		createFromBoundary      : createFromBoundary,
		setToRange              : setToRange,
		setRangeStart           : setRangeStart,
		setRangeEnd             : setRangeEnd,
		toBoundary              : toBoundary
	};
});