|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- /**
- * 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
- * @flow
- * @emails oncall+draft_js
- */
- 'use strict';
-
- declare var __DEV__: boolean;
- import type SelectionState from "./SelectionState";
-
- const DraftEffects = require("./DraftEffects");
-
- const DraftJsDebugLogging = require("./DraftJsDebugLogging");
-
- const UserAgent = require("fbjs/lib/UserAgent");
-
- const containsNode = require("fbjs/lib/containsNode");
-
- const getActiveElement = require("fbjs/lib/getActiveElement");
-
- const getCorrectDocumentFromNode = require("./getCorrectDocumentFromNode");
-
- const invariant = require("fbjs/lib/invariant");
-
- const isElement = require("./isElement");
-
- const isIE = UserAgent.isBrowser('IE');
-
- function getAnonymizedDOM(node: Node, getNodeLabels?: (n: Node) => Array<string>): string {
- if (!node) {
- return '[empty]';
- }
-
- const anonymized = anonymizeTextWithin(node, getNodeLabels);
-
- if (anonymized.nodeType === Node.TEXT_NODE) {
- return anonymized.textContent;
- }
-
- invariant(isElement(anonymized), 'Node must be an Element if it is not a text node.');
- const castedElement: Element = (anonymized: any);
- return castedElement.outerHTML;
- }
-
- function anonymizeTextWithin(node: Node, getNodeLabels?: (n: Node) => Array<string>): Node {
- const labels = getNodeLabels !== undefined ? getNodeLabels(node) : [];
-
- if (node.nodeType === Node.TEXT_NODE) {
- const length = node.textContent.length;
- return getCorrectDocumentFromNode(node).createTextNode('[text ' + length + (labels.length ? ' | ' + labels.join(', ') : '') + ']');
- }
-
- const clone = node.cloneNode();
-
- if (clone.nodeType === 1 && labels.length) {
- ((clone: any): Element).setAttribute('data-labels', labels.join(', '));
- }
-
- const childNodes = node.childNodes;
-
- for (let ii = 0; ii < childNodes.length; ii++) {
- clone.appendChild(anonymizeTextWithin(childNodes[ii], getNodeLabels));
- }
-
- return clone;
- }
-
- function getAnonymizedEditorDOM(node: Node, getNodeLabels?: (n: Node) => Array<string>): string {
- // grabbing the DOM content of the Draft editor
- let currentNode = node; // this should only be used after checking with isElement
-
- let castedNode: Element = (currentNode: any);
-
- while (currentNode) {
- if (isElement(currentNode) && castedNode.hasAttribute('contenteditable')) {
- // found the Draft editor container
- return getAnonymizedDOM(currentNode, getNodeLabels);
- } else {
- currentNode = currentNode.parentNode;
- castedNode = (currentNode: any);
- }
- }
-
- return 'Could not find contentEditable parent of node';
- }
-
- function getNodeLength(node: Node): number {
- return node.nodeValue === null ? node.childNodes.length : node.nodeValue.length;
- }
- /**
- * In modern non-IE browsers, we can support both forward and backward
- * selections.
- *
- * Note: IE10+ supports the Selection object, but it does not support
- * the `extend` method, which means that even in modern IE, it's not possible
- * to programatically create a backward selection. Thus, for all IE
- * versions, we use the old IE API to create our selections.
- */
-
-
- function setDraftEditorSelection(selectionState: SelectionState, node: Node, blockKey: string, nodeStart: number, nodeEnd: number): void {
- // It's possible that the editor has been removed from the DOM but
- // our selection code doesn't know it yet. Forcing selection in
- // this case may lead to errors, so just bail now.
- const documentObject = getCorrectDocumentFromNode(node);
-
- if (!containsNode(documentObject.documentElement, node)) {
- return;
- }
-
- const selection = documentObject.defaultView.getSelection();
- let anchorKey = selectionState.getAnchorKey();
- let anchorOffset = selectionState.getAnchorOffset();
- let focusKey = selectionState.getFocusKey();
- let focusOffset = selectionState.getFocusOffset();
- let isBackward = selectionState.getIsBackward(); // IE doesn't support backward selection. Swap key/offset pairs.
-
- if (!selection.extend && isBackward) {
- const tempKey = anchorKey;
- const tempOffset = anchorOffset;
- anchorKey = focusKey;
- anchorOffset = focusOffset;
- focusKey = tempKey;
- focusOffset = tempOffset;
- isBackward = false;
- }
-
- const hasAnchor = anchorKey === blockKey && nodeStart <= anchorOffset && nodeEnd >= anchorOffset;
- const hasFocus = focusKey === blockKey && nodeStart <= focusOffset && nodeEnd >= focusOffset; // If the selection is entirely bound within this node, set the selection
- // and be done.
-
- if (hasAnchor && hasFocus) {
- selection.removeAllRanges();
- addPointToSelection(selection, node, anchorOffset - nodeStart, selectionState);
- addFocusToSelection(selection, node, focusOffset - nodeStart, selectionState);
- return;
- }
-
- if (!isBackward) {
- // If the anchor is within this node, set the range start.
- if (hasAnchor) {
- selection.removeAllRanges();
- addPointToSelection(selection, node, anchorOffset - nodeStart, selectionState);
- } // If the focus is within this node, we can assume that we have
- // already set the appropriate start range on the selection, and
- // can simply extend the selection.
-
-
- if (hasFocus) {
- addFocusToSelection(selection, node, focusOffset - nodeStart, selectionState);
- }
- } else {
- // If this node has the focus, set the selection range to be a
- // collapsed range beginning here. Later, when we encounter the anchor,
- // we'll use this information to extend the selection.
- if (hasFocus) {
- selection.removeAllRanges();
- addPointToSelection(selection, node, focusOffset - nodeStart, selectionState);
- } // If this node has the anchor, we may assume that the correct
- // focus information is already stored on the selection object.
- // We keep track of it, reset the selection range, and extend it
- // back to the focus point.
-
-
- if (hasAnchor) {
- const storedFocusNode = selection.focusNode;
- const storedFocusOffset = selection.focusOffset;
- selection.removeAllRanges();
- addPointToSelection(selection, node, anchorOffset - nodeStart, selectionState);
- addFocusToSelection(selection, storedFocusNode, storedFocusOffset, selectionState);
- }
- }
- }
- /**
- * Extend selection towards focus point.
- */
-
-
- function addFocusToSelection(selection: Object, node: Node, offset: number, selectionState: SelectionState): void {
- const activeElement = getActiveElement();
-
- if (selection.extend && containsNode(activeElement, node)) {
- // If `extend` is called while another element has focus, an error is
- // thrown. We therefore disable `extend` if the active element is somewhere
- // other than the node we are selecting. This should only occur in Firefox,
- // since it is the only browser to support multiple selections.
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=921444.
- // logging to catch bug that is being reported in t16250795
- if (offset > getNodeLength(node)) {
- // the call to 'selection.extend' is about to throw
- DraftJsDebugLogging.logSelectionStateFailure({
- anonymizedDom: getAnonymizedEditorDOM(node),
- extraParams: JSON.stringify({
- offset
- }),
- selectionState: JSON.stringify(selectionState.toJS())
- });
- } // logging to catch bug that is being reported in t18110632
-
-
- const nodeWasFocus = node === selection.focusNode;
-
- try {
- selection.extend(node, offset);
- } catch (e) {
- DraftJsDebugLogging.logSelectionStateFailure({
- anonymizedDom: getAnonymizedEditorDOM(node, function (n) {
- const labels = [];
-
- if (n === activeElement) {
- labels.push('active element');
- }
-
- if (n === selection.anchorNode) {
- labels.push('selection anchor node');
- }
-
- if (n === selection.focusNode) {
- labels.push('selection focus node');
- }
-
- return labels;
- }),
- extraParams: JSON.stringify({
- activeElementName: activeElement ? activeElement.nodeName : null,
- nodeIsFocus: node === selection.focusNode,
- nodeWasFocus: nodeWasFocus,
- selectionRangeCount: selection.rangeCount,
- selectionAnchorNodeName: selection.anchorNode ? selection.anchorNode.nodeName : null,
- selectionAnchorOffset: selection.anchorOffset,
- selectionFocusNodeName: selection.focusNode ? selection.focusNode.nodeName : null,
- selectionFocusOffset: selection.focusOffset,
- message: e ? '' + e : null,
- offset
- }, null, 2),
- selectionState: JSON.stringify(selectionState.toJS(), null, 2)
- }); // allow the error to be thrown -
- // better than continuing in a broken state
-
- throw e;
- }
- } else {
- // IE doesn't support extend. This will mean no backward selection.
- // Extract the existing selection range and add focus to it.
- // Additionally, clone the selection range. IE11 throws an
- // InvalidStateError when attempting to access selection properties
- // after the range is detached.
- if (selection.rangeCount > 0) {
- const range = selection.getRangeAt(0);
- range.setEnd(node, offset);
- selection.addRange(range.cloneRange());
- }
- }
- }
-
- function addPointToSelection(selection: Object, node: Node, offset: number, selectionState: SelectionState): void {
- const range = getCorrectDocumentFromNode(node).createRange(); // logging to catch bug that is being reported in t16250795
-
- if (offset > getNodeLength(node)) {
- // in this case we know that the call to 'range.setStart' is about to throw
- DraftJsDebugLogging.logSelectionStateFailure({
- anonymizedDom: getAnonymizedEditorDOM(node),
- extraParams: JSON.stringify({
- offset
- }),
- selectionState: JSON.stringify(selectionState.toJS())
- });
- DraftEffects.handleExtensionCausedError();
- }
-
- range.setStart(node, offset); // IE sometimes throws Unspecified Error when trying to addRange
-
- if (isIE) {
- try {
- selection.addRange(range);
- } catch (e) {
- if (__DEV__) {
- /* eslint-disable-next-line no-console */
- console.warn('Call to selection.addRange() threw exception: ', e);
- }
- }
- } else {
- selection.addRange(range);
- }
- }
-
- module.exports = {
- setDraftEditorSelection,
- addFocusToSelection
- };
|