You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

expandRangeToStartOfLine.js.flow 7.1 KiB

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