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

Source: carets.js

/**
 * carets.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
 * @namespace carets
 */
define([
	'dom',
	'html',
	'maps',
	'browsers',
	'traversing',
	'boundaries'
], function (
	Dom,
	Html,
	Maps,
	Browsers,
	Traversing,
	Boundaries
) {
	'use strict';

	/**
	 * Adds a style tag to the head of the given document if one does not
	 * already exist.
	 *
	 * Each document in which box() is called requires some special br css
	 * styling in order for box() to return calculate correct range bounding
	 * offsets near br elements.
	 *
	 * @see https://github.com/alohaeditor/Aloha-Editor/issues/1138
	 * @private
	 * @param {Document} doc
	 */
	function ensureBrStyleFix(doc) {
		if (doc['!aloha-br-style-fix']) {
			return;
		}
		var style = doc.createElement('style');
		var text = doc.createTextNode(
			'.aloha-editable br,.aloha-editable br:after{content:"\\A";white-space:pre-line;}'
		);
		Dom.append(text, style);
		Dom.append(style, doc.head);
		doc['!aloha-br-style-fix'] = true;
	}

	/**
	 * Removes an unrendered (empty text-) node infront of the given boundary.
	 *
	 * @private
	 * @param  {Boundary} boundary
	 * @return {Boundary}
	 */
	function trimPreceedingNode(boundary) {
		if (Boundaries.isTextBoundary(boundary)) {
			return boundary;
		}
		if (Boundaries.isAtStart(boundary)) {
			return boundary;
		}
		if (Html.isRendered(Boundaries.nodeBefore(boundary))) {
			return boundary;
		}
		var clone = Dom.clone(Boundaries.container(boundary), true);
		var offset = Boundaries.offset(boundary) - 1;
		Dom.remove(clone.childNodes[offset]);
		return Boundaries.create(clone, offset);
	}

	/**
	 * Expands the range one visual step to the left if possible, returns null
	 * otherwise.
	 *
	 * @private
	 * @param  {Range} range
	 * @return {?Range}
	 */
	function expandLeft(range) {
		var clone = range.cloneRange();
		var start = trimPreceedingNode(Boundaries.fromRangeStart(clone));
		var end = Boundaries.fromRangeEnd(clone);
		if (Boundaries.isAtStart(start)) {
			return null;
		}
		if (Html.hasLinebreakingStyle(Boundaries.prevNode(start))) {
			return null;
		}
		return Boundaries.range(stepLeft(start), end);
	}

	/**
	 * Expands the range one visual step to the right if possible, returns null
	 * otherwise.
	 *
	 * @private
	 * @param  {Range} range
	 * @return {?Range}
	 */
	function expandRight(range) {
		var start = Boundaries.fromRangeStart(range);
		var end = Boundaries.fromRangeEnd(range);
		if (Boundaries.isAtEnd(end)) {
			return null;
		}
		if (Html.hasLinebreakingStyle(Boundaries.nextNode(end))) {
			return null;
		}
		// Because this means that we cannot expand any further right inside the
		// container
		if (Html.isAtEnd(start)) {
			return null;
		}
		return Boundaries.range(start, stepRight(end));
	}

	/**
	 * Steps the given boundary one visual step left or until in behind of a
	 * line break position.
	 *
	 * @private
	 * @param  {Boundary} boundary
	 * @return {Boundary}
	 */
	function stepLeft(boundary) {
		var prev = Traversing.prev(boundary, 'char');
		if (prev) {
			return prev;
		}
		if (Html.hasLinebreakingStyle(Boundaries.prevNode(boundary))) {
			return boundary;
		}
		return stepLeft(Traversing.prev(boundary, 'boundary'));
	}

	/**
	 * Steps the given boundary one visual step right or until in front of a
	 * line break position.
	 *
	 * @private
	 * @param  {Boundary} boundary
	 * @return {Boundary}
	 */
	function stepRight(boundary) {
		var next = Traversing.next(boundary, 'char');
		if (next) {
			return next;
		}
		if (Html.hasLinebreakingStyle(Boundaries.nextNode(boundary))) {
			return boundary;
		}
		return stepRight(Traversing.next(boundary, 'boundary'));
	}

	/**
	 * Returns a mutable bounding client rectangle from the reference range or
	 * element.
	 *
	 * @private
	 * @param  {Element|Range} reference
	 * @return {Object.<string, number>}
	 */
	function boundingRect(reference) {
		var rect = reference.getClientRects()[0] || reference.getBoundingClientRect();
		return {
			top    : rect.top,
			left   : rect.left,
			width  : rect.width,
			height : rect.height
		};
	}

	/**
	 * Shows a box element according to the dimensions and orientation of `box`.
	 *
	 * @param  {Object.<string, number>} box
	 * @param  {Document}                doc
	 * @return {Element}
	 * @memberOf carets
	 */
	function showHint(box, doc) {
		var elem = doc.querySelector('.aloha-caret-box-hint');
		if (!elem) {
			elem = doc.createElement('div');
			Dom.addClass(elem, 'aloha-caret-box-hint', 'aloha-ephemera');
		}
		Maps.extend(elem.style, {
			'top'        : box.top + 'px',
			'left'       : box.left + 'px',
			'height'     : box.height + 'px',
			'width'      : box.width + 'px',
			'position'   : 'absolute',
			'background' : 'red',
			'opacity'    : 0.2
		});
		Dom.append(elem, doc.body);
		return elem;
	}

	/**
	 * Removes any ".aloha-caret-box-hint" elements in the body of the given
	 * document and returns it.
	 *
	 * @param  {Document} doc
	 * @return {?Element}
	 * @memberOf carets
	 */
	function hideHint(doc) {
		var box = doc.querySelector('.aloha-caret-box-hint');
		if (box) {
			Dom.remove(box);
		}
		return box || null;
	}

	/**
	 * Checks whether or not we find ourselves in a situation in Chrome where it
	 * reports incorrect values when calling `boundingRect` with a collapsed
	 * range that is at a soft visual break.
	 *
	 * @private
	 * @param  {!Object.<string, int>} rect
	 * @param  {!Range}                range
	 * @return {boolean}
	 */
	function isChromeBug(rect, range) {
		if (!Browsers.chrome || !range.collapsed) {
			return false;
		}
		var element = Dom.upWhile(range.startContainer, Dom.isTextNode);
		var size = parseInt(Dom.getComputedStyle(element, 'font-size'), 10);
		return rect.width > size;
	}

	/**
	 * Attempts to calculates the bounding rectangle offsets for the given
	 * range.
	 *
	 * This function is a hack to work around the problems that user agents have
	 * in determining the bounding client rect for collapsed ranges.
	 *
	 * @private
	 * @param  {Range} range
	 * @return {Object.<string, number>}
	 */
	function bounds(range) {
		var rect;
		var expanded = expandRight(range);
		if (expanded) {
			rect = boundingRect(expanded);
			if (rect.width && !isChromeBug(rect, range)) {
				return rect;
			}
		}
		expanded = expandLeft(range);
		if (expanded) {
			rect = boundingRect(expanded);
			rect.left += rect.width;
			return rect;
		}
		return boundingRect(range);
	}

	/**
	 * Gets the bounding box of offets for the given range.
	 *
	 * This function requires the following css:
	 * .aloha-editable br, .aloha-editable br:after { content: "\A"; white-space: pre-line; }
	 *
	 * @param  {!Boundary} start
	 * @param  {Boundary=} end
	 * @return {Object.<string, number>}
	 * @memberOf carets
	 */
	function box(start, end) {
		if (!end) {
			end = start;
		}

		var range = Boundaries.range(start, end);
		var rect = bounds(range);
		var doc = range.commonAncestorContainer.ownerDocument;

		ensureBrStyleFix(doc);

		// Because `rect` should be the box of an expanded range and must
		// therefore have a non-zero width if valid
		if (rect.width > 0) {
			return {
				top    : rect.top + Dom.scrollTop(doc),
				left   : rect.left + Dom.scrollLeft(doc),
				width  : rect.width,
				height : rect.height
			};
		}

		var node = Boundaries.nodeAfter(start)
		        || Boundaries.nodeBefore(start);

		if (node && !Dom.isTextNode(node)) {
			rect = boundingRect(node);
			if (rect) {
				return {
					top    : rect.top + Dom.scrollTop(doc),
					left   : rect.left + Dom.scrollLeft(doc),
					width  : rect.width,
					height : rect.height
				};
			}
		}

		// <li>{}</li>
		node = Boundaries.container(start);

		return {
			top    : node.offsetTop + Dom.scrollTop(doc),
			left   : node.offsetLeft + Dom.scrollLeft(doc),
			width  : node.offsetWidth,
			height : parseInt(Dom.getComputedStyle(node, 'line-height'), 10)
		};
	}

	return {
		box      : box,
		showHint : showHint,
		hideHint : hideHint
	};
});
comments powered by Disqus