Revise more architecture around selection (#541)

* Revise more architecture around selection

* Fix tests

* Fix bug

* Fix bug

* Fix codes
This commit is contained in:
Dominic Gannaway
2021-08-04 20:37:07 +01:00
committed by acywatson
parent 7dd75ea0fa
commit e7b98f0850
15 changed files with 325 additions and 222 deletions

View File

@ -19,7 +19,7 @@ import React, {useCallback, useLayoutEffect, useMemo, useRef} from 'react';
import {startTransition, useEffect, useState} from 'react';
// $FlowFixMe
import {createPortal} from 'react-dom';
import {TextNode, isTextNode} from 'outline';
import {TextNode} from 'outline';
import useEvent from './useEvent';
type MentionMatch = {
@ -391,14 +391,18 @@ function getPossibleMentionMatch(text): MentionMatch | null {
}
function getTextUpToAnchor(selection: Selection): string | null {
const anchorNode = selection.anchor.getNode();
const anchor = selection.anchor;
if (anchor.type !== 'character') {
return null;
}
const anchorNode = anchor.getNode();
// We should not be attempting to extract mentions out of nodes
// that are already being used for other core things. This is
// especially true for immutable nodes, which can't be mutated at all.
if (!isTextNode(anchorNode) || !anchorNode.isSimpleText()) {
if (!anchorNode.isSimpleText()) {
return null;
}
const anchorOffset = selection.anchor.offset;
const anchorOffset = anchor.offset;
return anchorNode.getTextContent().slice(0, anchorOffset);
}
@ -469,14 +473,18 @@ function createMentionNodeFromSearchResult(
if (selection == null || !selection.isCollapsed()) {
return;
}
const anchorNode = selection.anchor.getNode();
const anchor = selection.anchor;
if (anchor.type !== 'character') {
return;
}
const anchorNode = anchor.getNode();
// We should not be attempting to extract mentions out of nodes
// that are already being used for other core things. This is
// especially true for immutable nodes, which can't be mutated at all.
if (!isTextNode(anchorNode) || !anchorNode.isSimpleText()) {
if (!anchorNode.isSimpleText()) {
return;
}
const selectionOffset = selection.anchor.offset;
const selectionOffset = anchor.offset;
const textContent = anchorNode.getTextContent().slice(0, selectionOffset);
const characterOffset = match.replaceableString.length;

View File

@ -50,29 +50,33 @@ export default function useTypeahead(editor: OutlineEditor): void {
if (currentTypeaheadNode !== null) {
const selection = view.getSelection();
if (selection !== null) {
let anchorNode = selection.anchor.getNode();
let anchorNodeOffset = selection.anchor.offset;
if (anchorNode.getKey() === currentTypeaheadNode.getKey()) {
anchorNode = anchorNode.getPreviousSibling();
if (isTextNode(anchorNode)) {
anchorNodeOffset = anchorNode.getTextContent().length;
const anchor = selection.anchor;
const focus = selection.focus;
if (anchor.type === 'character' && focus.type === 'character') {
let anchorNode = anchor.getNode();
let anchorNodeOffset = anchor.offset;
if (anchorNode.getKey() === currentTypeaheadNode.getKey()) {
anchorNode = anchorNode.getPreviousSibling();
if (isTextNode(anchorNode)) {
anchorNodeOffset = anchorNode.getTextContent().length;
}
}
}
let focusNode = selection.focus.getNode();
let focusNodeOffset = selection.focus.offset;
if (focusNode.getKey() === currentTypeaheadNode.getKey()) {
focusNode = focusNode.getPreviousSibling();
if (isTextNode(focusNode)) {
focusNodeOffset = focusNode.getTextContent().length;
let focusNode = focus.getNode();
let focusNodeOffset = focus.offset;
if (focusNode.getKey() === currentTypeaheadNode.getKey()) {
focusNode = focusNode.getPreviousSibling();
if (isTextNode(focusNode)) {
focusNodeOffset = focusNode.getTextContent().length;
}
}
if (isTextNode(focusNode) && isTextNode(anchorNode)) {
selection.setTextNodeRange(
anchorNode,
anchorNodeOffset,
focusNode,
focusNodeOffset,
);
}
}
if (isTextNode(focusNode) && isTextNode(anchorNode)) {
selection.setBaseAndExtent(
anchorNode,
anchorNodeOffset,
focusNode,
focusNodeOffset,
);
}
}
currentTypeaheadNode.remove();

View File

@ -7,7 +7,13 @@
* @flow strict-local
*/
import type {BlockNode, ViewModel, View, OutlineEditor} from 'outline';
import type {
BlockNode,
ViewModel,
View,
OutlineEditor,
Selection,
} from 'outline';
import {isBlockNode, isTextNode} from 'outline';
@ -69,10 +75,26 @@ export default function TreeView({
return <pre {...styles}>{content}</pre>;
}
function printSelection(selection: Selection): string {
let res = '';
const anchor = selection.anchor;
const focus = selection.focus;
const anchorOffset = anchor.offset;
const focusOffset = focus.offset;
res = `\n ├ anchor { key: ${anchor.key}, offset: ${
anchorOffset === null ? 'null' : anchorOffset
}, type: ${anchor.type} }`;
res += `\n └ focus { key: ${focus.key}, offset: ${
focusOffset === null ? 'null' : focusOffset
}, type: ${focus.type} }`;
return res;
}
function generateContent(viewModel: ViewModel): string {
let res = ' root\n';
viewModel.read((view: View) => {
const selectionString = viewModel.read((view: View) => {
const selection = view.getSelection();
let selectedNodes = null;
if (selection !== null) {
@ -100,9 +122,11 @@ function generateContent(viewModel: ViewModel): string {
typeDisplay,
});
});
return selection === null ? 'null' : printSelection(selection);
});
return res;
return res + '\n selection' + selectionString;
}
function visitTree(view: View, currentNode: BlockNode, visitor, indent = []) {
@ -238,34 +262,36 @@ function printSelectedCharsLine({
function getSelectionStartEnd(node, selection): [number, number] {
const anchor = selection.anchor;
const focus = selection.focus;
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
const textContent = node.getTextContent(true);
const textLength = textContent.length;
let start = -1;
let end = -1;
// Only one node is being selected.
if (
anchorNode === focusNode &&
node === anchorNode &&
anchor.offset !== focus.offset
) {
[start, end] =
anchor.offset < focus.offset
? [anchor.offset, focus.offset]
: [focus.offset, anchor.offset];
} else if (node === anchorNode) {
[start, end] = anchorNode.isBefore(focusNode)
? [anchor.offset, textLength]
: [0, anchor.offset];
} else if (node === focusNode) {
[start, end] = focusNode.isBefore(anchorNode)
? [focus.offset, textLength]
: [0, focus.offset];
} else {
// Node is within selection but not the anchor nor focus.
[start, end] = [0, textLength];
if (anchor.type === 'character' && focus.type === 'character') {
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
if (
anchorNode === focusNode &&
node === anchorNode &&
anchor.offset !== focus.offset
) {
[start, end] =
anchor.offset < focus.offset
? [anchor.offset, focus.offset]
: [focus.offset, anchor.offset];
} else if (node === anchorNode) {
[start, end] = anchorNode.isBefore(focusNode)
? [anchor.offset, textLength]
: [0, anchor.offset];
} else if (node === focusNode) {
[start, end] = focusNode.isBefore(anchorNode)
? [focus.offset, textLength]
: [0, focus.offset];
} else {
// Node is within selection but not the anchor nor focus.
[start, end] = [0, textLength];
}
}
// Account for non-single width characters.

View File

@ -162,6 +162,9 @@ function shouldOverrideBrowserDefault(
): boolean {
const anchor = selection.anchor;
const focus = selection.focus;
if (anchor.type !== 'character' || focus.type !== 'character') {
return true;
}
const anchorOffset = anchor.offset;
const focusOffset = focus.offset;
const anchorTextContentSize = anchor.getNode().getTextContentSize();
@ -336,19 +339,21 @@ export function onKeyDownForRichText(
} else if (isTab(event)) {
// Handle code blocks
const anchor = selection.anchor;
const anchorNode = anchor.getNode();
const parentBlock = anchorNode.getParentBlockOrThrow();
if (parentBlock.canInsertTab()) {
if (event.shiftKey) {
const textContent = anchorNode.getTextContent();
const character = textContent[anchor.offset - 1];
if (character === '\t') {
deleteBackward(selection);
if (anchor.type === 'character') {
const anchorNode = anchor.getNode();
const parentBlock = anchorNode.getParentBlockOrThrow();
if (parentBlock.canInsertTab()) {
if (event.shiftKey) {
const textContent = anchorNode.getTextContent();
const character = textContent[anchor.offset - 1];
if (character === '\t') {
deleteBackward(selection);
}
} else {
insertText(selection, '\t');
}
} else {
insertText(selection, '\t');
event.preventDefault();
}
event.preventDefault();
}
} else if (isSelectAll(event)) {
event.preventDefault();

View File

@ -62,7 +62,7 @@ export default function useOutlineDragonSupport(editor: OutlineEditor) {
setSelStart = blockStart;
setSelEnd = blockStart + blockLength;
// If the offset is more than the end, make it the end
selection.setBaseAndExtent(
selection.setTextNodeRange(
anchorNode,
setSelStart,
anchorNode,
@ -89,7 +89,7 @@ export default function useOutlineDragonSupport(editor: OutlineEditor) {
setSelEnd > anchorNodeTextLength
? anchorNodeTextLength
: setSelEnd;
selection.setBaseAndExtent(
selection.setTextNodeRange(
anchorNode,
setSelStart,
anchorNode,

View File

@ -70,7 +70,7 @@ function getMergeAction(
const anchor = selection.anchor;
const anchorKey = anchor.key;
const prevAnchorKey = prevSelection.anchor.key;
if (anchorKey !== prevAnchorKey) {
if (anchorKey !== prevAnchorKey || anchor.type !== 'character') {
return NO_MERGE;
}
const anchorOffset = anchor.offset;

View File

@ -8,6 +8,7 @@
*/
import type {NodeKey, ParsedNode} from './OutlineNode';
import type {Selection} from './OutlineSelection';
import {isTextNode, TextNode, isLineBreakNode} from '.';
import {
@ -16,7 +17,7 @@ import {
wrapInTextNodes,
updateDirectionIfNeeded,
} from './OutlineNode';
import {Selection} from './OutlineSelection';
import {makeSelection, getSelection, setPointValues} from './OutlineSelection';
import {errorOnReadOnly} from './OutlineView';
import {
IS_DIRECTIONLESS,
@ -138,6 +139,19 @@ export class BlockNode extends OutlineNode {
// Mutators
selectEnd(): Selection {
errorOnReadOnly();
const selection = getSelection();
const key = this.__key;
if (selection === null) {
return makeSelection(key, null, key, null, 'end', 'end');
} else {
setPointValues(selection.anchor, key, null, 'end');
setPointValues(selection.focus, key, null, 'end');
selection.isDirty = true;
}
return selection;
}
clear(): BlockNode {
errorOnReadOnly();
const writableSelf = this.getWritable();

View File

@ -53,13 +53,13 @@ export type ParsedNodeMap = Map<NodeKey, ParsedNode>;
type ParsedSelection = {
anchor: {
key: NodeKey,
offset: number,
type: 'character' | 'start' | 'end',
offset: null | number,
type: 'character' | 'before' | 'end',
},
focus: {
key: NodeKey,
offset: number,
type: 'character' | 'start' | 'end',
offset: null | number,
type: 'character' | 'before' | 'end',
},
};
// export type NodeMapType = {root: RootNode, [key: NodeKey]: OutlineNode};

View File

@ -9,7 +9,10 @@
import type {NodeKey, NodeMapType} from './OutlineNode';
import type {OutlineEditor, EditorThemeClasses} from './OutlineEditor';
import type {Selection as OutlineSelection} from './OutlineSelection';
import type {
Selection as OutlineSelection,
PointType,
} from './OutlineSelection';
import type {TextNode} from './OutlineTextNode';
import type {Node as ReactNode} from 'react';
@ -496,36 +499,50 @@ function reconcileSelection(
const focusNode = focus.getNode();
const anchorDOM = getElementByKeyOrThrow(editor, anchorKey);
const focusDOM = getElementByKeyOrThrow(editor, focusKey);
let nextAnchorNode;
let nextFocusNode;
let nextAnchorOffset;
let nextFocusOffset;
// Get the underlying DOM text nodes from the representative
// Outline text nodes (we use elements for text nodes).
const anchorDOMTarget = getDOMTextNode(anchorDOM);
const focusDOMTarget = getDOMTextNode(focusDOM);
if (anchor.type === 'character') {
const nextSelectionAnchorOffset = anchor.offset;
nextAnchorNode = getDOMTextNode(anchorDOM);
nextAnchorOffset =
isImmutableOrInertOrSegmented(anchorNode) ||
anchorNode.getTextContent() !== ''
? nextSelectionAnchorOffset
: nextSelectionAnchorOffset + 1;
} else {
nextAnchorNode = anchorDOM;
// TODO
nextAnchorOffset = 0;
}
if (focus.type === 'character') {
const nextSelectionFocusOffset = focus.offset;
nextFocusNode = getDOMTextNode(focusDOM);
nextFocusOffset =
isImmutableOrInertOrSegmented(focusNode) ||
focusNode.getTextContent() !== ''
? nextSelectionFocusOffset
: nextSelectionFocusOffset + 1;
} else {
nextFocusNode = focusDOM;
// TODO
nextFocusOffset = 0;
}
// If we can't get an underlying text node for selection, then
// we should avoid setting selection to something incorrect.
if (focusDOMTarget === null || anchorDOMTarget === null) {
if (nextAnchorNode === null || nextFocusNode === null) {
return;
}
const nextSelectionAnchorOffset = anchor.offset;
const nextSelectionFocusOffset = focus.offset;
const nextAnchorOffset =
isImmutableOrInertOrSegmented(anchorNode) ||
anchorNode.getTextContent() !== ''
? nextSelectionAnchorOffset
: nextSelectionAnchorOffset + 1;
const nextFocusOffset =
isImmutableOrInertOrSegmented(focusNode) ||
focusNode.getTextContent() !== ''
? nextSelectionFocusOffset
: nextSelectionFocusOffset + 1;
// Diff against the native DOM selection to ensure we don't do
// an unnecessary selection update.
if (
anchorOffset === nextAnchorOffset &&
focusOffset === nextFocusOffset &&
anchorDOMNode === anchorDOMTarget &&
focusDOMNode === focusDOMTarget
anchorDOMNode === nextAnchorNode &&
focusDOMNode === nextFocusNode
) {
return;
}
@ -534,9 +551,9 @@ function reconcileSelection(
// a "selectionchange" event, although it will be asynchronous.
try {
domSelection.setBaseAndExtent(
anchorDOMTarget,
nextAnchorNode,
nextAnchorOffset,
focusDOMTarget,
nextFocusNode,
nextFocusOffset,
);
} catch {
@ -590,26 +607,18 @@ export function getElementByKeyOrThrow(
return element;
}
function mergeAdjacentTextNodes(textNodes: Array<TextNode>): void {
// We're checking `selection !== null` later before we use these
// so initializing to 0 is safe and saves us an extra check below
const compositionKey = getCompositionKey();
let anchorOffset = 0;
let focusOffset = 0;
let anchorKey;
let focusKey;
if (activeSelection !== null) {
anchorOffset = activeSelection.anchor.offset;
focusOffset = activeSelection.focus.offset;
anchorKey = activeSelection.anchor.key;
focusKey = activeSelection.focus.key;
}
function mergeAdjacentTextNodes(
textNodes: Array<TextNode>,
anchor: null | PointType,
focus: null | PointType,
): void {
// Merge all text nodes into the first node
const writableMergeToNode = textNodes[0].getWritable();
const key = writableMergeToNode.__key;
const compositionKey = getCompositionKey();
let textLength = writableMergeToNode.getTextContentSize();
let selectionIsDirty = false;
for (let i = 1; i < textNodes.length; i++) {
const textNode = textNodes[i];
const siblingText = textNode.getTextContent();
@ -617,19 +626,21 @@ function mergeAdjacentTextNodes(textNodes: Array<TextNode>): void {
if (compositionKey === textNodeKey) {
setCompositionKey(key);
}
if (activeSelection !== null && textNodeKey === anchorKey) {
activeSelection.anchor.offset = textLength + anchorOffset;
activeSelection.anchor.key = key;
if (anchor !== null && textNodeKey === anchor.key) {
anchor.offset = textLength + anchor.offset;
anchor.key = key;
selectionIsDirty = true;
}
if (activeSelection !== null && textNodeKey === focusKey) {
activeSelection.focus.offset = textLength + focusOffset;
activeSelection.focus.key = key;
if (focus !== null && textNodeKey === focus.key) {
focus.offset = textLength + focus.offset;
focus.key = key;
selectionIsDirty = true;
}
writableMergeToNode.spliceText(textLength, 0, siblingText);
textLength += siblingText.length;
textNode.remove();
}
if (activeSelection !== null) {
if (selectionIsDirty && activeSelection !== null) {
activeSelection.isDirty = true;
}
}
@ -638,6 +649,14 @@ function normalizeTextNodes(block: BlockNode): void {
const children = block.getChildren();
let toNormalize = [];
let lastTextNodeFlags: number | null = null;
let anchor = null;
let focus = null;
if (activeSelection !== null) {
anchor = activeSelection.anchor;
focus = activeSelection.focus;
}
for (let i = 0; i < children.length; i++) {
const child = children[i];
@ -654,20 +673,20 @@ function normalizeTextNodes(block: BlockNode): void {
lastTextNodeFlags = flags;
} else {
if (toNormalize.length > 1) {
mergeAdjacentTextNodes(toNormalize);
mergeAdjacentTextNodes(toNormalize, anchor, focus);
}
toNormalize = [child];
lastTextNodeFlags = flags;
}
} else {
if (toNormalize.length > 1) {
mergeAdjacentTextNodes(toNormalize);
mergeAdjacentTextNodes(toNormalize, anchor, focus);
}
toNormalize = [];
lastTextNodeFlags = null;
}
}
if (toNormalize.length > 1) {
mergeAdjacentTextNodes(toNormalize);
mergeAdjacentTextNodes(toNormalize, anchor, focus);
}
}

View File

@ -26,7 +26,7 @@ import {getDOMTextNode, isSelectionWithinEditor} from './OutlineUtils';
import invariant from 'shared/invariant';
import {ZERO_WIDTH_JOINER_CHAR} from './OutlineConstants';
declare type CharacterPointType = {
type CharacterPointType = {
key: NodeKey,
offset: number,
type: 'character',
@ -34,33 +34,36 @@ declare type CharacterPointType = {
getNode: () => TextNode,
};
type BlockStartPointType = {
key: NodeKey,
offset: 0,
type: 'start',
is: (PointType) => boolean,
getNode: () => BlockNode,
};
type BlockEndPointType = {
key: NodeKey,
offset: 1,
offset: null,
type: 'end',
is: (PointType) => boolean,
getNode: () => BlockNode,
};
type PointType = CharacterPointType | BlockStartPointType | BlockEndPointType;
type BeforeNodePointType = {
key: NodeKey,
offset: null,
type: 'before',
is: (PointType) => boolean,
getNode: () => BlockNode,
};
export type PointType =
| CharacterPointType
| BeforeNodePointType
| BlockEndPointType;
class Point {
key: NodeKey;
offset: number;
type: 'character' | 'start' | 'end';
type: 'character' | 'before' | 'end';
constructor(
key: NodeKey,
offset: number,
type: 'character' | 'start' | 'end',
type: 'character' | 'before' | 'end',
) {
this.key = key;
this.offset = offset;
@ -81,18 +84,18 @@ class Point {
function createPoint(
key: NodeKey,
offset: number,
type: 'character' | 'start' | 'end',
offset: null | number,
type: 'character' | 'before' | 'end',
): PointType {
// $FlowFixMe: intentionally cast as we use a class for perf reasons
return new Point(key, offset, type);
}
function setPointValues(
export function setPointValues(
point: PointType,
key: NodeKey,
offset: number,
type: 'character' | 'start' | 'end',
offset: null | number,
type: 'character' | 'before' | 'end',
): void {
point.key = key;
// $FlowFixMe: internal utility function
@ -126,7 +129,7 @@ export class Selection {
}
return anchorNode.getNodesBetween(focusNode);
}
setBaseAndExtent(
setTextNodeRange(
anchorNode: TextNode,
anchorOffset: number,
focusNode: TextNode,
@ -144,8 +147,8 @@ export class Selection {
const firstNode = nodes[0];
const lastNode = nodes[nodes.length - 1];
const isBefore = firstNode === this.anchor.getNode();
const anchorOffset = this.anchor.offset;
const focusOffset = this.focus.offset;
const anchorOffset = this.anchor.offset || 0;
const focusOffset = this.focus.offset || 0;
let textContent = '';
nodes.forEach((node) => {
if (isTextNode(node)) {
@ -399,10 +402,20 @@ function resolveSelectionPoints(
editor._compositionKey !== resolvedAnchorPoint.key &&
lastSelection !== null
) {
resolvedAnchorPoint.key = lastSelection.anchor.key;
resolvedAnchorPoint.offset = lastSelection.anchor.offset;
resolvedFocusPoint.key = lastSelection.focus.key;
resolvedFocusPoint.offset = lastSelection.focus.offset;
const lastAnchor = lastSelection.anchor;
const lastFocus = lastSelection.focus;
setPointValues(
resolvedAnchorPoint,
lastAnchor.key,
lastAnchor.offset,
lastAnchor.type,
);
setPointValues(
resolvedFocusPoint,
lastFocus.key,
lastFocus.offset,
lastFocus.type,
);
}
}
@ -414,11 +427,11 @@ function resolveSelectionPoints(
// when it current exists outside the editor.
export function makeSelection(
anchorKey: NodeKey,
anchorOffset: number,
anchorOffset: null | number,
focusKey: NodeKey,
focusOffset: number,
anchorType: 'character' | 'start' | 'end',
focusType: 'character' | 'start' | 'end',
focusOffset: null | number,
anchorType: 'character' | 'before' | 'end',
focusType: 'character' | 'before' | 'end',
): Selection {
const viewModel = getActiveViewModel();
const selection = new Selection(
@ -506,13 +519,13 @@ export function createSelectionFromParse(
parsedSelection: null | {
anchor: {
key: string,
offset: number,
type: 'character' | 'start' | 'end',
offset: null | number,
type: 'character' | 'before' | 'end',
},
focus: {
key: string,
offset: number,
type: 'character' | 'start' | 'end',
offset: null | number,
type: 'character' | 'before' | 'end',
},
},
): null | Selection {

View File

@ -424,17 +424,14 @@ export class TextNode extends OutlineNode {
}
select(_anchorOffset?: number, _focusOffset?: number): Selection {
errorOnReadOnly();
if (this.isImmutable()) {
return this.selectNext(0, 0);
}
let anchorOffset = _anchorOffset;
let focusOffset = _focusOffset;
const selection = getSelection();
const text = this.getTextContent();
const key = this.__key;
if (key === null) {
invariant(
false,
'select: TODO? validate nodes have keys in a more generic way',
);
}
if (typeof text === 'string') {
const lastOffset = text.length;
if (anchorOffset === undefined) {
@ -464,7 +461,7 @@ export class TextNode extends OutlineNode {
) {
setCompositionKey(key);
}
selection.setBaseAndExtent(this, anchorOffset, this, focusOffset);
selection.setTextNodeRange(this, anchorOffset, this, focusOffset);
}
return selection;
}
@ -504,7 +501,7 @@ export class TextNode extends OutlineNode {
invariant(false, 'spliceText: selection not found');
}
const newOffset = offset + handledTextLength;
selection.setBaseAndExtent(
selection.setTextNodeRange(
writableSelf,
newOffset,
writableSelf,
@ -586,27 +583,25 @@ export class TextNode extends OutlineNode {
if (selection !== null) {
const anchor = selection.anchor;
const focus = selection.focus;
const anchorOffset = anchor.offset;
const focusOffset = focus.offset;
if (
anchor.key === key &&
anchor.type === 'character' &&
anchorOffset > textSize &&
anchorOffset <= nextTextSize
anchor.offset > textSize &&
anchor.offset <= nextTextSize
) {
anchor.key = siblingKey;
anchor.offset = anchorOffset - textSize;
anchor.offset -= textSize;
selection.isDirty = true;
}
if (
focus.key === key &&
focus.type === 'character' &&
focusOffset > textSize &&
focusOffset <= nextTextSize
focus.offset > textSize &&
focus.offset <= nextTextSize
) {
focus.key = siblingKey;
focus.offset = focusOffset - textSize;
focus.offset -= textSize;
selection.isDirty = true;
}
}

View File

@ -56,13 +56,13 @@ export type ParsedViewModel = {
_selection: null | {
anchor: {
key: string,
offset: number,
type: 'character' | 'start' | 'end',
offset: null | number,
type: 'character' | 'before' | 'end',
},
focus: {
key: string,
offset: number,
type: 'character' | 'start' | 'end',
offset: null | number,
type: 'character' | 'before' | 'end',
},
},
_nodeMap: Array<[NodeKey, ParsedNode]>,

View File

@ -44,10 +44,15 @@ export function getNodesInRange(selection: Selection): {
range: Array<NodeKey>,
nodeMap: Array<[NodeKey, OutlineNode]>,
} {
const anchorNode = selection.anchor.getNode();
const focusNode = selection.focus.getNode();
const anchorOffset = selection.anchor.offset;
const focusOffset = selection.focus.offset;
const anchor = selection.anchor;
const focus = selection.focus;
if (anchor.type !== 'character' || focus.type !== 'character') {
invariant(false, 'Selection block node not yet implemented.');
}
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
const anchorOffset = anchor.offset;
const focusOffset = focus.offset;
let startOffset;
let endOffset;
@ -163,14 +168,19 @@ export function extractSelection(selection: Selection): Array<OutlineNode> {
const selectedNodes = selection.getNodes();
const selectedNodesLength = selectedNodes.length;
const lastIndex = selectedNodesLength - 1;
const anchor = selection.anchor;
const focus = selection.focus;
let firstNode = selectedNodes[0];
let lastNode = selectedNodes[lastIndex];
if (!isTextNode(firstNode) || !isTextNode(lastNode)) {
invariant(false, 'formatText: firstNode/lastNode not a text node');
}
const anchorOffset = selection.anchor.offset;
const focusOffset = selection.focus.offset;
if (anchor.type !== 'character' || focus.type !== 'character') {
invariant(false, 'Selection block node not yet implemented.');
}
const anchorOffset = anchor.offset;
const focusOffset = focus.offset;
let startOffset;
let endOffset;
@ -212,16 +222,21 @@ export function formatText(
if (!isTextNode(firstNode) || !isTextNode(lastNode)) {
invariant(false, 'formatText: firstNode/lastNode not a text node');
}
const anchor = selection.anchor;
const focus = selection.focus;
if (anchor.type !== 'character' || focus.type !== 'character') {
invariant(false, 'Selection block node not yet implemented.');
}
const firstNodeText = firstNode.getTextContent();
const firstNodeTextLength = firstNodeText.length;
const currentBlock = firstNode.getParentBlockOrThrow();
const focusOffset = selection.focus.offset;
const focusOffset = focus.offset;
let firstNextFlags = firstNode.getTextNodeFormatFlags(
formatType,
null,
forceFormat,
);
let anchorOffset = selection.anchor.offset;
let anchorOffset = anchor.offset;
let startOffset;
let endOffset;
@ -320,7 +335,7 @@ export function formatText(
selectedNode.setFlags(selectedNextFlags);
}
}
selection.setBaseAndExtent(firstNode, startOffset, lastNode, endOffset);
selection.setTextNodeRange(firstNode, startOffset, lastNode, endOffset);
}
}
@ -340,7 +355,7 @@ export function insertParagraph(selection: Selection): void {
const textContentLength = textContent.length;
const nodesToMove = anchorNode.getNextSiblings().reverse();
const currentBlock = anchorNode.getParentBlockOrThrow();
let anchorOffset = selection.anchor.offset;
let anchorOffset = anchor.offset;
if (anchorOffset === 0) {
nodesToMove.push(anchorNode);
@ -827,15 +842,20 @@ export function insertRichText(selection: Selection, text: string): void {
export function insertText(selection: Selection, text: string): void {
const selectedNodes = selection.getNodes();
const selectedNodesLength = selectedNodes.length;
const anchorOffset = selection.anchor.offset;
const focusOffset = selection.focus.offset;
const anchor = selection.anchor;
const focus = selection.focus;
let firstNode = selectedNodes[0];
if (!isTextNode(firstNode)) {
invariant(false, 'insertText: firstNode not a a text node');
}
if (anchor.type !== 'character' || focus.type !== 'character') {
invariant(false, 'Selection block node not yet implemented.');
}
const anchorOffset = anchor.offset;
const focusOffset = focus.offset;
const firstNodeText = firstNode.getTextContent();
const firstNodeTextLength = firstNodeText.length;
const isBefore = firstNode === selection.anchor.getNode();
const isBefore = firstNode === anchor.getNode();
if (firstNode.isSegmented() || !firstNode.canInsertTextAtEnd()) {
if (selection.isCollapsed() && focusOffset === firstNodeTextLength) {
@ -992,7 +1012,7 @@ export function selectAll(selection: Selection): void {
const firstTextNode = root.getFirstTextNode();
const lastTextNode = root.getLastTextNode();
if (firstTextNode !== null && lastTextNode !== null) {
selection.setBaseAndExtent(
selection.setTextNodeRange(
firstTextNode,
0,
lastTextNode,

View File

@ -900,10 +900,10 @@ describe('OutlineSelection tests', () => {
'<span data-outline-text="true"></span>' +
'</p></div>',
expectedSelection: {
anchorPath: [0, 1, 0],
anchorOffset: 3,
focusPath: [0, 1, 0],
focusOffset: 3,
anchorPath: [0, 2, 0],
anchorOffset: 0,
focusPath: [0, 2, 0],
focusOffset: 0,
},
},
{

View File

@ -9,35 +9,34 @@
"7": "updateDOM: prevInnerDOM is null or undefined",
"8": "updateDOM: innerDOM is null or undefined",
"9": "setTextContent: can only be used on non-immutable text nodes",
"10": "select: TODO? validate nodes have keys in a more generic way",
"11": "spliceText: can only be used on non-immutable text nodes",
"12": "spliceText: TODO? validate nodes have keys in a more generic way",
"13": "spliceText: selection not found",
"14": "splitText: can only be used on non-immutable text nodes",
"15": "decorate: base method not extended",
"16": "OutlineNode: Node type %s does not implement deserialize().",
"17": "OutlineNode: Node type %s does not implement .clone().",
"18": "Expected node %s to have a parent.",
"19": "Expected node %s to have a parent block.",
"20": "Expected node %s to have a top parent block.",
"21": "getNodesBetween: ancestor is null",
"22": "getLatest: node not found",
"23": "createDOM: base method not extended",
"24": "updateDOM: base method not extended",
"25": "setFlags: can only be used on non-immutable nodes",
"26": "selectNext: found invalid sibling",
"27": "Expected node with key %s to exist but it's not in the nodeMap.",
"28": "createNodeFromParse: type \"%s\" + not found",
"29": "Point.getNode: node not found",
"30": "resolveNonLineBreakOrInertNode: resolved node not a text node",
"31": "Editor.update() cannot be used within a text node transform.",
"32": "Cannot use method in read-only mode.",
"33": "Unable to find an active view model. View methods or node methods can only be used synchronously during the callback of editor.update() or viewModel.read().",
"34": "Unable to find an active editor. View methods or node methods can only be used synchronously during the callback of editor.update() or viewModel.read().",
"35": "updateEditor: selection has been lost because the previously selected nodes have been removed and selection wasn't moved to another node. Ensure selection changes after removing/replacing a selected node.",
"36": "createNode: node does not exist in nodeMap",
"37": "reconcileNode: prevNode or nextNode does not exist in nodeMap",
"38": "reconcileNode: parentDOM is null",
"39": "storeDOMWithNodeKey: key was null",
"40": "Reconciliation: could not find DOM element for node key \"${key}\""
"10": "spliceText: can only be used on non-immutable text nodes",
"11": "spliceText: TODO? validate nodes have keys in a more generic way",
"12": "spliceText: selection not found",
"13": "splitText: can only be used on non-immutable text nodes",
"14": "decorate: base method not extended",
"15": "OutlineNode: Node type %s does not implement deserialize().",
"16": "OutlineNode: Node type %s does not implement .clone().",
"17": "Expected node %s to have a parent.",
"18": "Expected node %s to have a parent block.",
"19": "Expected node %s to have a top parent block.",
"20": "getNodesBetween: ancestor is null",
"21": "getLatest: node not found",
"22": "createDOM: base method not extended",
"23": "updateDOM: base method not extended",
"24": "setFlags: can only be used on non-immutable nodes",
"25": "selectNext: found invalid sibling",
"26": "Expected node with key %s to exist but it's not in the nodeMap.",
"27": "createNodeFromParse: type \"%s\" + not found",
"28": "Point.getNode: node not found",
"29": "resolveNonLineBreakOrInertNode: resolved node not a text node",
"30": "Editor.update() cannot be used within a text node transform.",
"31": "Cannot use method in read-only mode.",
"32": "Unable to find an active view model. View methods or node methods can only be used synchronously during the callback of editor.update() or viewModel.read().",
"33": "Unable to find an active editor. View methods or node methods can only be used synchronously during the callback of editor.update() or viewModel.read().",
"34": "updateEditor: selection has been lost because the previously selected nodes have been removed and selection wasn't moved to another node. Ensure selection changes after removing/replacing a selected node.",
"35": "createNode: node does not exist in nodeMap",
"36": "reconcileNode: prevNode or nextNode does not exist in nodeMap",
"37": "reconcileNode: parentDOM is null",
"38": "storeDOMWithNodeKey: key was null",
"39": "Reconciliation: could not find DOM element for node key \"${key}\""
}