CaretUtils.js 6.42 KB
/**
 * CaretUtils.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/**
 * Utility functions shared by the caret logic.
 *
 * @private
 * @class tinymce.caret.CaretUtils
 */
define("tinymce/caret/CaretUtils", [
	"tinymce/util/Fun",
	"tinymce/dom/TreeWalker",
	"tinymce/dom/NodeType",
	"tinymce/caret/CaretPosition",
	"tinymce/caret/CaretContainer",
	"tinymce/caret/CaretCandidate"
], function(Fun, TreeWalker, NodeType, CaretPosition, CaretContainer, CaretCandidate) {
	var isContentEditableTrue = NodeType.isContentEditableTrue,
		isContentEditableFalse = NodeType.isContentEditableFalse,
		isBlockLike = NodeType.matchStyleValues('display', 'block table table-cell table-caption'),
		isCaretContainer = CaretContainer.isCaretContainer,
		curry = Fun.curry,
		isElement = NodeType.isElement,
		isCaretCandidate = CaretCandidate.isCaretCandidate;

	function isForwards(direction) {
		return direction > 0;
	}

	function isBackwards(direction) {
		return direction < 0;
	}

	function findNode(node, direction, predicateFn, rootNode, shallow) {
		var walker = new TreeWalker(node, rootNode);

		if (isBackwards(direction)) {
			if (isContentEditableFalse(node)) {
				node = walker.prev(true);
				if (predicateFn(node)) {
					return node;
				}
			}

			while ((node = walker.prev(shallow))) {
				if (predicateFn(node)) {
					return node;
				}
			}
		}

		if (isForwards(direction)) {
			if (isContentEditableFalse(node)) {
				node = walker.next(true);
				if (predicateFn(node)) {
					return node;
				}
			}

			while ((node = walker.next(shallow))) {
				if (predicateFn(node)) {
					return node;
				}
			}
		}

		return null;
	}

	function getEditingHost(node, rootNode) {
		for (node = node.parentNode; node && node != rootNode; node = node.parentNode) {
			if (isContentEditableTrue(node)) {
				return node;
			}
		}

		return rootNode;
	}

	function getParentBlock(node, rootNode) {
		while (node && node != rootNode) {
			if (isBlockLike(node)) {
				return node;
			}

			node = node.parentNode;
		}

		return null;
	}

	function isInSameBlock(caretPosition1, caretPosition2, rootNode) {
		return getParentBlock(caretPosition1.container(), rootNode) == getParentBlock(caretPosition2.container(), rootNode);
	}

	function isInSameEditingHost(caretPosition1, caretPosition2, rootNode) {
		return getEditingHost(caretPosition1.container(), rootNode) == getEditingHost(caretPosition2.container(), rootNode);
	}

	function getChildNodeAtRelativeOffset(relativeOffset, caretPosition) {
		var container, offset;

		if (!caretPosition) {
			return null;
		}

		container = caretPosition.container();
		offset = caretPosition.offset();

		if (!isElement(container)) {
			return null;
		}

		return container.childNodes[offset + relativeOffset];
	}

	function beforeAfter(before, node) {
		var range = node.ownerDocument.createRange();

		if (before) {
			range.setStartBefore(node);
			range.setEndBefore(node);
		} else {
			range.setStartAfter(node);
			range.setEndAfter(node);
		}

		return range;
	}

	function isNodesInSameBlock(rootNode, node1, node2) {
		return getParentBlock(node1, rootNode) == getParentBlock(node2, rootNode);
	}

	function lean(left, rootNode, node) {
		var sibling, siblingName;

		if (left) {
			siblingName = 'previousSibling';
		} else {
			siblingName = 'nextSibling';
		}

		while (node && node != rootNode) {
			sibling = node[siblingName];

			if (isCaretContainer(sibling)) {
				sibling = sibling[siblingName];
			}

			if (isContentEditableFalse(sibling)) {
				if (isNodesInSameBlock(rootNode, sibling, node)) {
					return sibling;
				}

				break;
			}

			if (isCaretCandidate(sibling)) {
				break;
			}

			node = node.parentNode;
		}

		return null;
	}

	var before = curry(beforeAfter, true);
	var after = curry(beforeAfter, false);

	function normalizeRange(direction, rootNode, range) {
		var node, container, offset, location;
		var leanLeft = curry(lean, true, rootNode);
		var leanRight = curry(lean, false, rootNode);

		container = range.startContainer;
		offset = range.startOffset;

		if (CaretContainer.isCaretContainerBlock(container)) {
			if (!isElement(container)) {
				container = container.parentNode;
			}

			location = container.getAttribute('data-mce-caret');

			if (location == 'before') {
				node = container.nextSibling;
				if (isContentEditableFalse(node)) {
					return before(node);
				}
			}

			if (location == 'after') {
				node = container.previousSibling;
				if (isContentEditableFalse(node)) {
					return after(node);
				}
			}
		}

		if (!range.collapsed) {
			return range;
		}

		if (NodeType.isText(container)) {
			if (isCaretContainer(container)) {
				if (direction === 1) {
					node = leanRight(container);
					if (node) {
						return before(node);
					}

					node = leanLeft(container);
					if (node) {
						return after(node);
					}
				}

				if (direction === -1) {
					node = leanLeft(container);
					if (node) {
						return after(node);
					}

					node = leanRight(container);
					if (node) {
						return before(node);
					}
				}

				return range;
			}

			if (CaretContainer.endsWithCaretContainer(container) && offset >= container.data.length - 1) {
				if (direction === 1) {
					node = leanRight(container);
					if (node) {
						return before(node);
					}
				}

				return range;
			}

			if (CaretContainer.startsWithCaretContainer(container) && offset <= 1) {
				if (direction === -1) {
					node = leanLeft(container);
					if (node) {
						return after(node);
					}
				}

				return range;
			}

			if (offset === container.data.length) {
				node = leanRight(container);
				if (node) {
					return before(node);
				}

				return range;
			}

			if (offset === 0) {
				node = leanLeft(container);
				if (node) {
					return after(node);
				}

				return range;
			}
		}

		return range;
	}

	function isNextToContentEditableFalse(relativeOffset, caretPosition) {
		return isContentEditableFalse(getChildNodeAtRelativeOffset(relativeOffset, caretPosition));
	}

	return {
		isForwards: isForwards,
		isBackwards: isBackwards,
		findNode: findNode,
		getEditingHost: getEditingHost,
		getParentBlock: getParentBlock,
		isInSameBlock: isInSameBlock,
		isInSameEditingHost: isInSameEditingHost,
		isBeforeContentEditableFalse: curry(isNextToContentEditableFalse, 0),
		isAfterContentEditableFalse: curry(isNextToContentEditableFalse, -1),
		normalizeRange: normalizeRange
	};
});