"use strict"; /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * * @emails oncall+draft_js */ var UnicodeUtils = require("fbjs/lib/UnicodeUtils"); var getCorrectDocumentFromNode = require("./getCorrectDocumentFromNode"); var getRangeClientRects = require("./getRangeClientRects"); var invariant = require("fbjs/lib/invariant"); /** * Return the computed line height, in pixels, for the provided element. */ function getLineHeightPx(element) { var computed = getComputedStyle(element); var correctDocument = getCorrectDocumentFromNode(element); var div = correctDocument.createElement('div'); div.style.fontFamily = computed.fontFamily; div.style.fontSize = computed.fontSize; div.style.fontStyle = computed.fontStyle; div.style.fontWeight = computed.fontWeight; div.style.lineHeight = computed.lineHeight; div.style.position = 'absolute'; div.textContent = 'M'; var documentBody = correctDocument.body; !documentBody ? process.env.NODE_ENV !== "production" ? invariant(false, 'Missing document.body') : invariant(false) : void 0; // forced layout here documentBody.appendChild(div); var rect = div.getBoundingClientRect(); documentBody.removeChild(div); return rect.height; } /** * Return whether every ClientRect in the provided list lies on the same line. * * We assume that the rects on the same line all contain the baseline, so the * lowest top line needs to be above the highest bottom line (i.e., if you were * to project the rects onto the y-axis, their intersection would be nonempty). * * In addition, we require that no two boxes are lineHeight (or more) apart at * either top or bottom, which helps protect against false positives for fonts * with extremely large glyph heights (e.g., with a font size of 17px, Zapfino * produces rects of height 58px!). */ function areRectsOnOneLine(rects, lineHeight) { var minTop = Infinity; var minBottom = Infinity; var maxTop = -Infinity; var maxBottom = -Infinity; for (var ii = 0; ii < rects.length; ii++) { var rect = rects[ii]; if (rect.width === 0 || rect.width === 1) { // When a range starts or ends a soft wrap, many browsers (Chrome, IE, // Safari) include an empty rect on the previous or next line. When the // text lies in a container whose position is not integral (e.g., from // margin: auto), Safari makes these empty rects have width 1 (instead of // 0). Having one-pixel-wide characters seems unlikely (and most browsers // report widths in subpixel precision anyway) so it's relatively safe to // skip over them. continue; } minTop = Math.min(minTop, rect.top); minBottom = Math.min(minBottom, rect.bottom); maxTop = Math.max(maxTop, rect.top); maxBottom = Math.max(maxBottom, rect.bottom); } return maxTop <= minBottom && maxTop - minTop < lineHeight && maxBottom - minBottom < lineHeight; } /** * Return the length of a node, as used by Range offsets. */ function getNodeLength(node) { // http://www.w3.org/TR/dom/#concept-node-length switch (node.nodeType) { case Node.DOCUMENT_TYPE_NODE: return 0; case Node.TEXT_NODE: case Node.PROCESSING_INSTRUCTION_NODE: case Node.COMMENT_NODE: return node.length; default: return node.childNodes.length; } } /** * Given a collapsed range, move the start position backwards as far as * possible while the range still spans only a single line. */ function expandRangeToStartOfLine(range) { !range.collapsed ? process.env.NODE_ENV !== "production" ? invariant(false, 'expandRangeToStartOfLine: Provided range is not collapsed.') : invariant(false) : void 0; range = range.cloneRange(); var containingElement = range.startContainer; if (containingElement.nodeType !== 1) { containingElement = containingElement.parentNode; } var lineHeight = getLineHeightPx(containingElement); // Imagine our text looks like: //
under^ the // stairs in a small closet.
inside)]. // Picking out which child to recurse into here is a special case since we // don't want to check past-- once we find that the final range starts // in , we can look at all of its children (and all of their children) // to find the break point. // At all times, (bestContainer, bestOffset) is the latest single-line start // point that we know of. var currentContainer = bestContainer; var maxIndexToConsider = bestOffset - 1; do { var nodeValue = currentContainer.nodeValue; var ii = maxIndexToConsider; for (; ii >= 0; ii--) { if (nodeValue != null && ii > 0 && UnicodeUtils.isSurrogatePair(nodeValue, ii - 1)) { // We're in the middle of a surrogate pair -- skip over so we never // return a range with an endpoint in the middle of a code point. continue; } range.setStart(currentContainer, ii); if (areRectsOnOneLine(getRangeClientRects(range), lineHeight)) { bestContainer = currentContainer; bestOffset = ii; } else { break; } } if (ii === -1 || currentContainer.childNodes.length === 0) { // If ii === -1, then (bestContainer, bestOffset), which is equal to // (currentContainer, 0), was a single-line start point but a start // point before currentContainer wasn't, so the line break seems to // have occurred immediately after currentContainer's start tag // // If currentContainer.childNodes.length === 0, we're already at a // terminal node (e.g., text node) and should return our current best. break; } currentContainer = currentContainer.childNodes[ii]; maxIndexToConsider = getNodeLength(currentContainer); } while (true); range.setStart(bestContainer, bestOffset); return range; } module.exports = expandRangeToStartOfLine;