191 lines
7.2 KiB
Plaintext
191 lines
7.2 KiB
Plaintext
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*
|
|
* @providesModule getDraftEditorSelectionWithNodes
|
|
* @format
|
|
* @flow
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import type { DOMDerivedSelection } from './DOMDerivedSelection';
|
|
import type EditorState from './EditorState';
|
|
|
|
var findAncestorOffsetKey = require('./findAncestorOffsetKey');
|
|
var getSelectionOffsetKeyForNode = require('./getSelectionOffsetKeyForNode');
|
|
var getUpdatedSelectionState = require('./getUpdatedSelectionState');
|
|
var invariant = require('fbjs/lib/invariant');
|
|
var nullthrows = require('fbjs/lib/nullthrows');
|
|
|
|
type SelectionPoint = {
|
|
key: string;
|
|
offset: number;
|
|
};
|
|
|
|
/**
|
|
* Convert the current selection range to an anchor/focus pair of offset keys
|
|
* and values that can be interpreted by components.
|
|
*/
|
|
function getDraftEditorSelectionWithNodes(editorState: EditorState, root: ?HTMLElement, anchorNode: Node, anchorOffset: number, focusNode: Node, focusOffset: number): DOMDerivedSelection {
|
|
var anchorIsTextNode = anchorNode.nodeType === Node.TEXT_NODE;
|
|
var focusIsTextNode = focusNode.nodeType === Node.TEXT_NODE;
|
|
|
|
// If the selection range lies only on text nodes, the task is simple.
|
|
// Find the nearest offset-aware elements and use the
|
|
// offset values supplied by the selection range.
|
|
if (anchorIsTextNode && focusIsTextNode) {
|
|
return {
|
|
selectionState: getUpdatedSelectionState(editorState, nullthrows(findAncestorOffsetKey(anchorNode)), anchorOffset, nullthrows(findAncestorOffsetKey(focusNode)), focusOffset),
|
|
needsRecovery: false
|
|
};
|
|
}
|
|
|
|
var anchorPoint = null;
|
|
var focusPoint = null;
|
|
var needsRecovery = true;
|
|
|
|
// An element is selected. Convert this selection range into leaf offset
|
|
// keys and offset values for consumption at the component level. This
|
|
// is common in Firefox, where select-all and triple click behavior leads
|
|
// to entire elements being selected.
|
|
//
|
|
// Note that we use the `needsRecovery` parameter in the callback here. This
|
|
// is because when certain elements are selected, the behavior for subsequent
|
|
// cursor movement (e.g. via arrow keys) is uncertain and may not match
|
|
// expectations at the component level. For example, if an entire <div> is
|
|
// selected and the user presses the right arrow, Firefox keeps the selection
|
|
// on the <div>. If we allow subsequent keypresses to insert characters
|
|
// natively, they will be inserted into a browser-created text node to the
|
|
// right of that <div>. This is obviously undesirable.
|
|
//
|
|
// With the `needsRecovery` flag, we inform the caller that it is responsible
|
|
// for manually setting the selection state on the rendered document to
|
|
// ensure proper selection state maintenance.
|
|
|
|
if (anchorIsTextNode) {
|
|
anchorPoint = {
|
|
key: nullthrows(findAncestorOffsetKey(anchorNode)),
|
|
offset: anchorOffset
|
|
};
|
|
focusPoint = getPointForNonTextNode(root, focusNode, focusOffset);
|
|
} else if (focusIsTextNode) {
|
|
focusPoint = {
|
|
key: nullthrows(findAncestorOffsetKey(focusNode)),
|
|
offset: focusOffset
|
|
};
|
|
anchorPoint = getPointForNonTextNode(root, anchorNode, anchorOffset);
|
|
} else {
|
|
anchorPoint = getPointForNonTextNode(root, anchorNode, anchorOffset);
|
|
focusPoint = getPointForNonTextNode(root, focusNode, focusOffset);
|
|
|
|
// If the selection is collapsed on an empty block, don't force recovery.
|
|
// This way, on arrow key selection changes, the browser can move the
|
|
// cursor from a non-zero offset on one block, through empty blocks,
|
|
// to a matching non-zero offset on other text blocks.
|
|
if (anchorNode === focusNode && anchorOffset === focusOffset) {
|
|
needsRecovery = !!anchorNode.firstChild && anchorNode.firstChild.nodeName !== 'BR';
|
|
}
|
|
}
|
|
|
|
return {
|
|
selectionState: getUpdatedSelectionState(editorState, anchorPoint.key, anchorPoint.offset, focusPoint.key, focusPoint.offset),
|
|
needsRecovery
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Identify the first leaf descendant for the given node.
|
|
*/
|
|
function getFirstLeaf(node: any): Node {
|
|
while (node.firstChild && (
|
|
// data-blocks has no offset
|
|
node.firstChild instanceof Element && node.firstChild.getAttribute('data-blocks') === 'true' || getSelectionOffsetKeyForNode(node.firstChild))) {
|
|
node = node.firstChild;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Identify the last leaf descendant for the given node.
|
|
*/
|
|
function getLastLeaf(node: any): Node {
|
|
while (node.lastChild && (
|
|
// data-blocks has no offset
|
|
node.lastChild instanceof Element && node.lastChild.getAttribute('data-blocks') === 'true' || getSelectionOffsetKeyForNode(node.lastChild))) {
|
|
node = node.lastChild;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function getPointForNonTextNode(editorRoot: ?HTMLElement, startNode: Node, childOffset: number): SelectionPoint {
|
|
let node = startNode;
|
|
var offsetKey: ?string = findAncestorOffsetKey(node);
|
|
|
|
invariant(offsetKey != null || editorRoot && (editorRoot === node || editorRoot.firstChild === node), 'Unknown node in selection range.');
|
|
|
|
// If the editorRoot is the selection, step downward into the content
|
|
// wrapper.
|
|
if (editorRoot === node) {
|
|
node = node.firstChild;
|
|
invariant(node instanceof Element && node.getAttribute('data-contents') === 'true', 'Invalid DraftEditorContents structure.');
|
|
if (childOffset > 0) {
|
|
childOffset = node.childNodes.length;
|
|
}
|
|
}
|
|
|
|
// If the child offset is zero and we have an offset key, we're done.
|
|
// If there's no offset key because the entire editor is selected,
|
|
// find the leftmost ("first") leaf in the tree and use that as the offset
|
|
// key.
|
|
if (childOffset === 0) {
|
|
var key: ?string = null;
|
|
if (offsetKey != null) {
|
|
key = offsetKey;
|
|
} else {
|
|
var firstLeaf = getFirstLeaf(node);
|
|
key = nullthrows(getSelectionOffsetKeyForNode(firstLeaf));
|
|
}
|
|
return { key, offset: 0 };
|
|
}
|
|
|
|
var nodeBeforeCursor = node.childNodes[childOffset - 1];
|
|
var leafKey: ?string = null;
|
|
var textLength: ?number = null;
|
|
|
|
if (!getSelectionOffsetKeyForNode(nodeBeforeCursor)) {
|
|
// Our target node may be a leaf or a text node, in which case we're
|
|
// already where we want to be and can just use the child's length as
|
|
// our offset.
|
|
leafKey = nullthrows(offsetKey);
|
|
textLength = getTextContentLength(nodeBeforeCursor);
|
|
} else {
|
|
// Otherwise, we'll look at the child to the left of the cursor and find
|
|
// the last leaf node in its subtree.
|
|
var lastLeaf = getLastLeaf(nodeBeforeCursor);
|
|
leafKey = nullthrows(getSelectionOffsetKeyForNode(lastLeaf));
|
|
textLength = getTextContentLength(lastLeaf);
|
|
}
|
|
|
|
return {
|
|
key: leafKey,
|
|
offset: textLength
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Return the length of a node's textContent, regarding single newline
|
|
* characters as zero-length. This allows us to avoid problems with identifying
|
|
* the correct selection offset for empty blocks in IE, in which we
|
|
* render newlines instead of break tags.
|
|
*/
|
|
function getTextContentLength(node: Node): number {
|
|
var textContent = node.textContent;
|
|
return textContent === '\n' ? 0 : textContent.length;
|
|
}
|
|
|
|
module.exports = getDraftEditorSelectionWithNodes; |