Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

3 лет назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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. 'use strict';
  12. declare var __DEV__: boolean;
  13. import type SelectionState from "./SelectionState";
  14. const DraftEffects = require("./DraftEffects");
  15. const DraftJsDebugLogging = require("./DraftJsDebugLogging");
  16. const UserAgent = require("fbjs/lib/UserAgent");
  17. const containsNode = require("fbjs/lib/containsNode");
  18. const getActiveElement = require("fbjs/lib/getActiveElement");
  19. const getCorrectDocumentFromNode = require("./getCorrectDocumentFromNode");
  20. const invariant = require("fbjs/lib/invariant");
  21. const isElement = require("./isElement");
  22. const isIE = UserAgent.isBrowser('IE');
  23. function getAnonymizedDOM(node: Node, getNodeLabels?: (n: Node) => Array<string>): string {
  24. if (!node) {
  25. return '[empty]';
  26. }
  27. const anonymized = anonymizeTextWithin(node, getNodeLabels);
  28. if (anonymized.nodeType === Node.TEXT_NODE) {
  29. return anonymized.textContent;
  30. }
  31. invariant(isElement(anonymized), 'Node must be an Element if it is not a text node.');
  32. const castedElement: Element = (anonymized: any);
  33. return castedElement.outerHTML;
  34. }
  35. function anonymizeTextWithin(node: Node, getNodeLabels?: (n: Node) => Array<string>): Node {
  36. const labels = getNodeLabels !== undefined ? getNodeLabels(node) : [];
  37. if (node.nodeType === Node.TEXT_NODE) {
  38. const length = node.textContent.length;
  39. return getCorrectDocumentFromNode(node).createTextNode('[text ' + length + (labels.length ? ' | ' + labels.join(', ') : '') + ']');
  40. }
  41. const clone = node.cloneNode();
  42. if (clone.nodeType === 1 && labels.length) {
  43. ((clone: any): Element).setAttribute('data-labels', labels.join(', '));
  44. }
  45. const childNodes = node.childNodes;
  46. for (let ii = 0; ii < childNodes.length; ii++) {
  47. clone.appendChild(anonymizeTextWithin(childNodes[ii], getNodeLabels));
  48. }
  49. return clone;
  50. }
  51. function getAnonymizedEditorDOM(node: Node, getNodeLabels?: (n: Node) => Array<string>): string {
  52. // grabbing the DOM content of the Draft editor
  53. let currentNode = node; // this should only be used after checking with isElement
  54. let castedNode: Element = (currentNode: any);
  55. while (currentNode) {
  56. if (isElement(currentNode) && castedNode.hasAttribute('contenteditable')) {
  57. // found the Draft editor container
  58. return getAnonymizedDOM(currentNode, getNodeLabels);
  59. } else {
  60. currentNode = currentNode.parentNode;
  61. castedNode = (currentNode: any);
  62. }
  63. }
  64. return 'Could not find contentEditable parent of node';
  65. }
  66. function getNodeLength(node: Node): number {
  67. return node.nodeValue === null ? node.childNodes.length : node.nodeValue.length;
  68. }
  69. /**
  70. * In modern non-IE browsers, we can support both forward and backward
  71. * selections.
  72. *
  73. * Note: IE10+ supports the Selection object, but it does not support
  74. * the `extend` method, which means that even in modern IE, it's not possible
  75. * to programatically create a backward selection. Thus, for all IE
  76. * versions, we use the old IE API to create our selections.
  77. */
  78. function setDraftEditorSelection(selectionState: SelectionState, node: Node, blockKey: string, nodeStart: number, nodeEnd: number): void {
  79. // It's possible that the editor has been removed from the DOM but
  80. // our selection code doesn't know it yet. Forcing selection in
  81. // this case may lead to errors, so just bail now.
  82. const documentObject = getCorrectDocumentFromNode(node);
  83. if (!containsNode(documentObject.documentElement, node)) {
  84. return;
  85. }
  86. const selection = documentObject.defaultView.getSelection();
  87. let anchorKey = selectionState.getAnchorKey();
  88. let anchorOffset = selectionState.getAnchorOffset();
  89. let focusKey = selectionState.getFocusKey();
  90. let focusOffset = selectionState.getFocusOffset();
  91. let isBackward = selectionState.getIsBackward(); // IE doesn't support backward selection. Swap key/offset pairs.
  92. if (!selection.extend && isBackward) {
  93. const tempKey = anchorKey;
  94. const tempOffset = anchorOffset;
  95. anchorKey = focusKey;
  96. anchorOffset = focusOffset;
  97. focusKey = tempKey;
  98. focusOffset = tempOffset;
  99. isBackward = false;
  100. }
  101. const hasAnchor = anchorKey === blockKey && nodeStart <= anchorOffset && nodeEnd >= anchorOffset;
  102. const hasFocus = focusKey === blockKey && nodeStart <= focusOffset && nodeEnd >= focusOffset; // If the selection is entirely bound within this node, set the selection
  103. // and be done.
  104. if (hasAnchor && hasFocus) {
  105. selection.removeAllRanges();
  106. addPointToSelection(selection, node, anchorOffset - nodeStart, selectionState);
  107. addFocusToSelection(selection, node, focusOffset - nodeStart, selectionState);
  108. return;
  109. }
  110. if (!isBackward) {
  111. // If the anchor is within this node, set the range start.
  112. if (hasAnchor) {
  113. selection.removeAllRanges();
  114. addPointToSelection(selection, node, anchorOffset - nodeStart, selectionState);
  115. } // If the focus is within this node, we can assume that we have
  116. // already set the appropriate start range on the selection, and
  117. // can simply extend the selection.
  118. if (hasFocus) {
  119. addFocusToSelection(selection, node, focusOffset - nodeStart, selectionState);
  120. }
  121. } else {
  122. // If this node has the focus, set the selection range to be a
  123. // collapsed range beginning here. Later, when we encounter the anchor,
  124. // we'll use this information to extend the selection.
  125. if (hasFocus) {
  126. selection.removeAllRanges();
  127. addPointToSelection(selection, node, focusOffset - nodeStart, selectionState);
  128. } // If this node has the anchor, we may assume that the correct
  129. // focus information is already stored on the selection object.
  130. // We keep track of it, reset the selection range, and extend it
  131. // back to the focus point.
  132. if (hasAnchor) {
  133. const storedFocusNode = selection.focusNode;
  134. const storedFocusOffset = selection.focusOffset;
  135. selection.removeAllRanges();
  136. addPointToSelection(selection, node, anchorOffset - nodeStart, selectionState);
  137. addFocusToSelection(selection, storedFocusNode, storedFocusOffset, selectionState);
  138. }
  139. }
  140. }
  141. /**
  142. * Extend selection towards focus point.
  143. */
  144. function addFocusToSelection(selection: Object, node: Node, offset: number, selectionState: SelectionState): void {
  145. const activeElement = getActiveElement();
  146. if (selection.extend && containsNode(activeElement, node)) {
  147. // If `extend` is called while another element has focus, an error is
  148. // thrown. We therefore disable `extend` if the active element is somewhere
  149. // other than the node we are selecting. This should only occur in Firefox,
  150. // since it is the only browser to support multiple selections.
  151. // See https://bugzilla.mozilla.org/show_bug.cgi?id=921444.
  152. // logging to catch bug that is being reported in t16250795
  153. if (offset > getNodeLength(node)) {
  154. // the call to 'selection.extend' is about to throw
  155. DraftJsDebugLogging.logSelectionStateFailure({
  156. anonymizedDom: getAnonymizedEditorDOM(node),
  157. extraParams: JSON.stringify({
  158. offset
  159. }),
  160. selectionState: JSON.stringify(selectionState.toJS())
  161. });
  162. } // logging to catch bug that is being reported in t18110632
  163. const nodeWasFocus = node === selection.focusNode;
  164. try {
  165. selection.extend(node, offset);
  166. } catch (e) {
  167. DraftJsDebugLogging.logSelectionStateFailure({
  168. anonymizedDom: getAnonymizedEditorDOM(node, function (n) {
  169. const labels = [];
  170. if (n === activeElement) {
  171. labels.push('active element');
  172. }
  173. if (n === selection.anchorNode) {
  174. labels.push('selection anchor node');
  175. }
  176. if (n === selection.focusNode) {
  177. labels.push('selection focus node');
  178. }
  179. return labels;
  180. }),
  181. extraParams: JSON.stringify({
  182. activeElementName: activeElement ? activeElement.nodeName : null,
  183. nodeIsFocus: node === selection.focusNode,
  184. nodeWasFocus: nodeWasFocus,
  185. selectionRangeCount: selection.rangeCount,
  186. selectionAnchorNodeName: selection.anchorNode ? selection.anchorNode.nodeName : null,
  187. selectionAnchorOffset: selection.anchorOffset,
  188. selectionFocusNodeName: selection.focusNode ? selection.focusNode.nodeName : null,
  189. selectionFocusOffset: selection.focusOffset,
  190. message: e ? '' + e : null,
  191. offset
  192. }, null, 2),
  193. selectionState: JSON.stringify(selectionState.toJS(), null, 2)
  194. }); // allow the error to be thrown -
  195. // better than continuing in a broken state
  196. throw e;
  197. }
  198. } else {
  199. // IE doesn't support extend. This will mean no backward selection.
  200. // Extract the existing selection range and add focus to it.
  201. // Additionally, clone the selection range. IE11 throws an
  202. // InvalidStateError when attempting to access selection properties
  203. // after the range is detached.
  204. if (selection.rangeCount > 0) {
  205. const range = selection.getRangeAt(0);
  206. range.setEnd(node, offset);
  207. selection.addRange(range.cloneRange());
  208. }
  209. }
  210. }
  211. function addPointToSelection(selection: Object, node: Node, offset: number, selectionState: SelectionState): void {
  212. const range = getCorrectDocumentFromNode(node).createRange(); // logging to catch bug that is being reported in t16250795
  213. if (offset > getNodeLength(node)) {
  214. // in this case we know that the call to 'range.setStart' is about to throw
  215. DraftJsDebugLogging.logSelectionStateFailure({
  216. anonymizedDom: getAnonymizedEditorDOM(node),
  217. extraParams: JSON.stringify({
  218. offset
  219. }),
  220. selectionState: JSON.stringify(selectionState.toJS())
  221. });
  222. DraftEffects.handleExtensionCausedError();
  223. }
  224. range.setStart(node, offset); // IE sometimes throws Unspecified Error when trying to addRange
  225. if (isIE) {
  226. try {
  227. selection.addRange(range);
  228. } catch (e) {
  229. if (__DEV__) {
  230. /* eslint-disable-next-line no-console */
  231. console.warn('Call to selection.addRange() threw exception: ', e);
  232. }
  233. }
  234. } else {
  235. selection.addRange(range);
  236. }
  237. }
  238. module.exports = {
  239. setDraftEditorSelection,
  240. addFocusToSelection
  241. };