We use cookies to make our website more effective. By using our website you agree to our privacy policy.

Source: ranges.js

/**
 * ranges.js is part of Aloha Editor project http://www.alohaeditor.org
 *
 * Aloha Editor ● JavaScript Content Editing Library
 * Copyright (c) 2010-2015 Gentics Software GmbH, Vienna, Austria.
 * Contributors http://www.alohaeditor.org/docs/contributing.html
 *
 * @see
 * https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#deleting-the-selection
 */
define(['dom', 'arrays'], function (Dom, Arrays) {
	'use strict';

	/**
	 * Creates a range object with boundaries defined by containers, and offsets
	 * in those containers.
	 *
	 * @param  {!Element} sc
	 * @param  {number}   so
	 * @param  {!Element} ec
	 * @param  {number}   eo
	 * @return {Range}
	 */
	function create(sc, so, ec, eo) {
		var range = sc.ownerDocument.createRange();
		range.setStart(sc, so || 0);
		if (ec) {
			range.setEnd(ec, eo || 0);
		} else {
			range.setEnd(sc, so || 0);
		}
		return range;
	}

	/**
	 * Creates a range from the horizontal and vertical offset pixel positions
	 * relative to upper-left corner the document body.
	 *
	 * Returns a collapsed range for the position where the text insertion
	 * indicator would be rendered.
	 *
	 * @see
	 * http://dev.w3.org/csswg/cssom-view/#dom-document-caretpositionfrompoint
	 * http://stackoverflow.com/questions/3189812/creating-a-collapsed-range-from-a-pixel-position-in-ff-webkit
	 * http://jsfiddle.net/timdown/ABjQP/8/
	 * http://lists.w3.org/Archives/Public/public-webapps/2009OctDec/0113.html
	 *
	 * @private
	 * @param  {number}    x
	 * @param  {number}    y
	 * @param  {!Document} doc
	 * @return {?Range}
	 */
	function fromPoint(x, y, doc) {
		if (x < 0 || y < 0) {
			return null;
		}
		if (doc['caretRangeFromPoint']) {
			return doc['caretRangeFromPoint'](x, y);
		}
		if (doc.caretPositionFromPoint) {
			var pos = doc.caretPositionFromPoint(x, y);
			return create(pos.offsetNode, pos.offset);
		}
		if (doc.elementFromPoint) {
			return fromPointIE(x, y, doc);
		}

		throw 'fromPoint() unimplemented for this browser';
	}

	/**
	 * Returns whether x and y are inside or above the given
	 * rectangle as created by range.getClientRects()
	 * @see http://jsfiddle.net/timdown/ABjQP/8/
	 *
	 * @param {int} x
	 * @param {int} y
	 * @param {Rectangle} rect
	 * @return {boolean}
	 */
	function pointIsInOrAboveRect(x, y, rect) {
		return y < rect.bottom && x >= rect.left && x <= rect.right;
	}

	/**
	 * Transforms a collapsed range into mockup
	 * client rectange object, by exchanging the
	 * left property with the provided one.
	 *
	 * @see stepTextNode
	 *
	 * @param  {Range} range
	 * @param  {?int}  left
	 * @return {Object|null}
	 */
	function collapsedRangeToRect(range, left) {
		var clientRect = Arrays.last(range.getClientRects());
		if (!clientRect) {
			return null;
		}
		return {
			left   : left || clientRect.left,
			right  : clientRect.right,
			bottom : clientRect.bottom
		};
	}

	/**
	 * Will extend a range inside node until it covers
	 * x and y and then return an offset object containing
	 * the offset node and the actual offset index.
	 * The method will call itself recursively, using the
	 * lastLeft parameter, which holds the left offset from
	 * the last iteration. Don't pass lastLeft when calling
	 * the function yourself.
	 *
	 * Because client rectangle calculation (range.getClientRects)
	 * is broken in Internet Explorer 11, this function will 
	 * use a collapsed range to match the x and y positions 
	 * and create a rectangle using the lastLeft parameter
	 * internally. Not using this approach will lead to bogus
	 * results for range.getClientRects when clicking inside
	 * an text node thats nested inside an li element.
	 *
	 * @param  {!Node} node
	 * @param  {!Range} range
	 * @param  {!integer} offset
	 * @param  {!integer} x
	 * @param  {!integer} y
	 * @param  {?integer} lastLeft
	 */
	function stepTextNode(node, range, offset, x, y, lastLeft) {
		range.setStart(node, offset);
		range.setEnd(node, offset);
		var rect = collapsedRangeToRect(range, lastLeft);
		if (rect && pointIsInOrAboveRect(x, y, rect)) {
			if (rect.right - x > x - rect.left) {
				offset--;
			}
			return {
				node  : node,
				index : offset
			};
		}
		if (offset < node.length) {
			return stepTextNode(node, range, ++offset, x, y, rect ? rect.left : null);
		} 
		return null;
	}

	/**
	 * Will extend range inside a node until it covers 
	 * the x & y position to return an offset object
	 * that contains an offset node and the offset itself
	 *
	 * @param  {!Node}    node
	 * @param  {!Range}   range
	 * @param  {!integer} x
	 * @param  {!integer} y
	 * @return {Object}
	 */
	function findOffset(node, range, x, y) {
		if (Dom.isTextNode(node)) {
			var offset = stepTextNode(node, range, 0, x, y);
			if (offset) {
				return offset;
			}
		} else {
			range.setEndAfter(node);
			var rect = Arrays.last(range.getClientRects());
			if (rect && pointIsInOrAboveRect(x, y, rect)) {
				return {
					node  : node.parentNode,
					index : Dom.nodeIndex(node)
				};
			}
		}

		if (node.nextSibling) {
			return findOffset(node.nextSibling, range, x, y);
		}

		return {
			node  : node.parentNode,
			index : Dom.nodeIndex(node)
		};
	}

	/**
	 * Creates a Range object from click coordinates 
	 * x and y on the document. Meant to be a drop-in
	 * replacement for @see fromPoint which works in
	 * Internet Explorer
	 *
	 * Based on http://jsfiddle.net/timdown/ABjQP/8/
	 *
	 * @param  {!integer}  x
	 * @param  {!integer}  y
	 * @param  {!Document} doc
	 * @return {Range}
	 */
	function fromPointIE(x, y, doc) {
		var el = doc.elementFromPoint(x, y);
		var range = doc.createRange();
		var offset = {
			node  : el.firstChild,
			index : -1
		};

		range.selectNodeContents(el);
		range.collapse(true);

		if (!offset.node) {
			offset = {
				node  : el.parentNode,
				index : Dom.nodeIndex(el)
			};
		} else {
			offset = findOffset(offset.node, range, x, y);
		}
		return create(offset.node, offset.index);
	}


	/**
	 * Gets the given node's nearest non-editable parent.
	 *
	 * @private
	 * @param  {!Node} node
	 * @return {?Element}
	 */
	function parentBlock(node) {
		var block = Dom.isEditable(node) ? Dom.editingHost(node) : node;
		var parent = Dom.upWhile(block, function (node) {
			return node.parentNode && !Dom.isEditable(node.parentNode);
		});
		return (Dom.Nodes.DOCUMENT === parent.nodeType) ? null : parent;
	}

	/**
	 * Derives a range from the horizontal and vertical offset pixel positions
	 * relative to upper-left corner of the document that is visible within the
	 * view port.
	 *
	 * It is important that the x, y coordinates given are not only within the
	 * dimensions of the document, but also viewport (ie: they are visible on
	 * the screen).
	 *
	 * Returns null if no suitable range can be determined from within an
	 * editable.
	 *
	 * @param  {number}    x
	 * @param  {number}    y
	 * @param  {!Document} doc
	 * @return {?Range}
	 */
	function fromPosition(x, y, doc) {
		x -= Dom.scrollLeft(doc);
		y -= Dom.scrollTop(doc);
		var range = fromPoint(x, y, doc);
		if (!range) {
			return null;
		}
		if (Dom.isEditableNode(range.commonAncestorContainer)) {
			return range;
		}
		var block = parentBlock(range.commonAncestorContainer);
		if (!block || !block.parentNode) {
			return null;
		}
		var body = doc.body;
		var offsets = Dom.offset(block);
		var offset = Dom.nodeIndex(block);
		var pointX = x + body.scrollLeft;
		var blockX = offsets.left + body.scrollLeft + block.offsetWidth;
		if (pointX > blockX) {
			offset += 1;
		}
		return create(block.parentNode, offset);
	}

	/**
	 * Checks whether two ranges are equal. Ranges are equal if their
	 * corresponding boundary containers and offsets are strictly equal.
	 *
	 * @param  {Range} a
	 * @param  {Range} b
	 * @return {boolean}
	 */
	function equals(a, b) {
		return a.startContainer === b.startContainer
			&& a.startOffset    === b.startOffset
			&& a.endContainer   === b.endContainer
			&& a.endOffset      === b.endOffset;
	}

	/**
	 * Returns true if the given value is a Range object as created by
	 * document.createRange()
	 *
	 * @param  {*} obj
	 * @return {boolean}
	 * @memberOf selections
	 */
	function is(obj) {
		return obj
		    && obj.hasOwnProperty
		    && obj.hasOwnProperty('commonAncestorContainer')
		    && obj.hasOwnProperty('collapsed')
		    && obj.hasOwnProperty('startContainer')
		    && obj.hasOwnProperty('startOffset');
	}

	return {
		is           : is,
		equals       : equals,
		create       : create,
		fromPosition : fromPosition
	};
});
comments powered by Disqus