d2-ui
Version:
178 lines (158 loc) • 6.84 kB
JavaScript
/**
* 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
* @typechecks
*
*/
;
var findAncestorOffsetKey = require('./findAncestorOffsetKey');
var getSelectionOffsetKeyForNode = require('./getSelectionOffsetKeyForNode');
var getUpdatedSelectionState = require('./getUpdatedSelectionState');
var invariant = require('fbjs/lib/invariant');
var nullthrows = require('fbjs/lib/nullthrows');
/**
* Convert the current selection range to an anchor/focus pair of offset keys
* and values that can be interpreted by components.
*/
function getDraftEditorSelectionWithNodes(editorState, root, anchorNode, anchorOffset, focusNode, focusOffset) {
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.nodeName !== 'BR';
}
}
return {
selectionState: getUpdatedSelectionState(editorState, anchorPoint.key, anchorPoint.offset, focusPoint.key, focusPoint.offset),
needsRecovery: needsRecovery
};
}
/**
* Identify the first leaf descendant for the given node.
*/
function getFirstLeaf(node) {
while (node.firstChild && getSelectionOffsetKeyForNode(node.firstChild)) {
node = node.firstChild;
}
return node;
}
/**
* Identify the last leaf descendant for the given node.
*/
function getLastLeaf(node) {
while (node.lastChild && getSelectionOffsetKeyForNode(node.lastChild)) {
node = node.lastChild;
}
return node;
}
function getPointForNonTextNode(editorRoot, node, childOffset) {
var offsetKey = findAncestorOffsetKey(node);
!(offsetKey != null || editorRoot && (editorRoot === node || editorRoot.firstChild === node)) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Unknown node in selection range.') : invariant(false) : undefined;
// If the editorRoot is the selection, step downward into the content
// wrapper.
if (editorRoot === node) {
node = node.firstChild;
!(node instanceof Element && node.getAttribute('data-contents') === 'true') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Invalid DraftEditorContents structure.') : invariant(false) : undefined;
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 = null;
if (offsetKey != null) {
key = offsetKey;
} else {
var firstLeaf = getFirstLeaf(node);
key = nullthrows(getSelectionOffsetKeyForNode(firstLeaf));
}
return { key: key, offset: 0 };
}
var nodeBeforeCursor = node.childNodes[childOffset - 1];
var leafKey = null;
var textLength = 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) {
var textContent = node.textContent;
return textContent === '\n' ? 0 : textContent.length;
}
module.exports = getDraftEditorSelectionWithNodes;