UNPKG

7.36 kBJavaScriptView Raw
1"use strict";
2
3/**
4 * Copyright (c) Facebook, Inc. and its affiliates.
5 *
6 * This source code is licensed under the MIT license found in the
7 * LICENSE file in the root directory of this source tree.
8 *
9 * @format
10 *
11 * @emails oncall+draft_js
12 */
13var UnicodeUtils = require("fbjs/lib/UnicodeUtils");
14
15var getCorrectDocumentFromNode = require("./getCorrectDocumentFromNode");
16
17var getRangeClientRects = require("./getRangeClientRects");
18
19var invariant = require("fbjs/lib/invariant");
20/**
21 * Return the computed line height, in pixels, for the provided element.
22 */
23
24
25function getLineHeightPx(element) {
26 var computed = getComputedStyle(element);
27 var correctDocument = getCorrectDocumentFromNode(element);
28 var div = correctDocument.createElement('div');
29 div.style.fontFamily = computed.fontFamily;
30 div.style.fontSize = computed.fontSize;
31 div.style.fontStyle = computed.fontStyle;
32 div.style.fontWeight = computed.fontWeight;
33 div.style.lineHeight = computed.lineHeight;
34 div.style.position = 'absolute';
35 div.textContent = 'M';
36 var documentBody = correctDocument.body;
37 !documentBody ? process.env.NODE_ENV !== "production" ? invariant(false, 'Missing document.body') : invariant(false) : void 0; // forced layout here
38
39 documentBody.appendChild(div);
40 var rect = div.getBoundingClientRect();
41 documentBody.removeChild(div);
42 return rect.height;
43}
44/**
45 * Return whether every ClientRect in the provided list lies on the same line.
46 *
47 * We assume that the rects on the same line all contain the baseline, so the
48 * lowest top line needs to be above the highest bottom line (i.e., if you were
49 * to project the rects onto the y-axis, their intersection would be nonempty).
50 *
51 * In addition, we require that no two boxes are lineHeight (or more) apart at
52 * either top or bottom, which helps protect against false positives for fonts
53 * with extremely large glyph heights (e.g., with a font size of 17px, Zapfino
54 * produces rects of height 58px!).
55 */
56
57
58function areRectsOnOneLine(rects, lineHeight) {
59 var minTop = Infinity;
60 var minBottom = Infinity;
61 var maxTop = -Infinity;
62 var maxBottom = -Infinity;
63
64 for (var ii = 0; ii < rects.length; ii++) {
65 var rect = rects[ii];
66
67 if (rect.width === 0 || rect.width === 1) {
68 // When a range starts or ends a soft wrap, many browsers (Chrome, IE,
69 // Safari) include an empty rect on the previous or next line. When the
70 // text lies in a container whose position is not integral (e.g., from
71 // margin: auto), Safari makes these empty rects have width 1 (instead of
72 // 0). Having one-pixel-wide characters seems unlikely (and most browsers
73 // report widths in subpixel precision anyway) so it's relatively safe to
74 // skip over them.
75 continue;
76 }
77
78 minTop = Math.min(minTop, rect.top);
79 minBottom = Math.min(minBottom, rect.bottom);
80 maxTop = Math.max(maxTop, rect.top);
81 maxBottom = Math.max(maxBottom, rect.bottom);
82 }
83
84 return maxTop <= minBottom && maxTop - minTop < lineHeight && maxBottom - minBottom < lineHeight;
85}
86/**
87 * Return the length of a node, as used by Range offsets.
88 */
89
90
91function getNodeLength(node) {
92 // http://www.w3.org/TR/dom/#concept-node-length
93 switch (node.nodeType) {
94 case Node.DOCUMENT_TYPE_NODE:
95 return 0;
96
97 case Node.TEXT_NODE:
98 case Node.PROCESSING_INSTRUCTION_NODE:
99 case Node.COMMENT_NODE:
100 return node.length;
101
102 default:
103 return node.childNodes.length;
104 }
105}
106/**
107 * Given a collapsed range, move the start position backwards as far as
108 * possible while the range still spans only a single line.
109 */
110
111
112function expandRangeToStartOfLine(range) {
113 !range.collapsed ? process.env.NODE_ENV !== "production" ? invariant(false, 'expandRangeToStartOfLine: Provided range is not collapsed.') : invariant(false) : void 0;
114 range = range.cloneRange();
115 var containingElement = range.startContainer;
116
117 if (containingElement.nodeType !== 1) {
118 containingElement = containingElement.parentNode;
119 }
120
121 var lineHeight = getLineHeightPx(containingElement); // Imagine our text looks like:
122 // <div><span>once upon a time, there was a <em>boy
123 // who lived</em> </span><q><strong>under^ the
124 // stairs</strong> in a small closet.</q></div>
125 // where the caret represents the cursor. First, we crawl up the tree until
126 // the range spans multiple lines (setting the start point to before
127 // "<strong>", then before "<div>"), then at each level we do a search to
128 // find the latest point which is still on a previous line. We'll find that
129 // the break point is inside the span, then inside the <em>, then in its text
130 // node child, the actual break point before "who".
131
132 var bestContainer = range.endContainer;
133 var bestOffset = range.endOffset;
134 range.setStart(range.startContainer, 0);
135
136 while (areRectsOnOneLine(getRangeClientRects(range), lineHeight)) {
137 bestContainer = range.startContainer;
138 bestOffset = range.startOffset;
139 !bestContainer.parentNode ? process.env.NODE_ENV !== "production" ? invariant(false, 'Found unexpected detached subtree when traversing.') : invariant(false) : void 0;
140 range.setStartBefore(bestContainer);
141
142 if (bestContainer.nodeType === 1 && getComputedStyle(bestContainer).display !== 'inline') {
143 // The start of the line is never in a different block-level container.
144 break;
145 }
146 } // In the above example, range now spans from "<div>" to "under",
147 // bestContainer is <div>, and bestOffset is 1 (index of <q> inside <div>)].
148 // Picking out which child to recurse into here is a special case since we
149 // don't want to check past <q> -- once we find that the final range starts
150 // in <span>, we can look at all of its children (and all of their children)
151 // to find the break point.
152 // At all times, (bestContainer, bestOffset) is the latest single-line start
153 // point that we know of.
154
155
156 var currentContainer = bestContainer;
157 var maxIndexToConsider = bestOffset - 1;
158
159 do {
160 var nodeValue = currentContainer.nodeValue;
161 var ii = maxIndexToConsider;
162
163 for (; ii >= 0; ii--) {
164 if (nodeValue != null && ii > 0 && UnicodeUtils.isSurrogatePair(nodeValue, ii - 1)) {
165 // We're in the middle of a surrogate pair -- skip over so we never
166 // return a range with an endpoint in the middle of a code point.
167 continue;
168 }
169
170 range.setStart(currentContainer, ii);
171
172 if (areRectsOnOneLine(getRangeClientRects(range), lineHeight)) {
173 bestContainer = currentContainer;
174 bestOffset = ii;
175 } else {
176 break;
177 }
178 }
179
180 if (ii === -1 || currentContainer.childNodes.length === 0) {
181 // If ii === -1, then (bestContainer, bestOffset), which is equal to
182 // (currentContainer, 0), was a single-line start point but a start
183 // point before currentContainer wasn't, so the line break seems to
184 // have occurred immediately after currentContainer's start tag
185 //
186 // If currentContainer.childNodes.length === 0, we're already at a
187 // terminal node (e.g., text node) and should return our current best.
188 break;
189 }
190
191 currentContainer = currentContainer.childNodes[ii];
192 maxIndexToConsider = getNodeLength(currentContainer);
193 } while (true);
194
195 range.setStart(bestContainer, bestOffset);
196 return range;
197}
198
199module.exports = expandRangeToStartOfLine;
\No newline at end of file