@lexical/utils
Version:
This package contains misc utilities for Lexical.
184 lines (175 loc) • 6.07 kB
text/typescript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
$getDOMSlot,
$getDOMTextNode,
$getSelection,
$isElementNode,
$isRangeSelection,
$isTextNode,
type EditorState,
type ElementNode,
getDOMTextNode,
type LexicalEditor,
mergeRegister,
type Point,
type RangeSelection,
type TextNode,
} from 'lexical';
import positionNodeOnRange from './positionNodeOnRange';
import px from './px';
function $getOrderedSelectionPoints(selection: RangeSelection): [Point, Point] {
const points = selection.getStartEndPoints();
return selection.isBackward() ? [points[1], points[0]] : points;
}
function $rangeTargetFromPoint(
editor: LexicalEditor,
point: Point,
node: ElementNode | TextNode,
dom: HTMLElement,
): [HTMLElement | Text, number] {
if (point.type === 'text' || !$isElementNode(node)) {
const textDOM =
($isTextNode(node)
? $getDOMTextNode(node, dom, editor)
: getDOMTextNode(dom)) || dom;
return [textDOM, point.offset];
} else {
const slot = $getDOMSlot(node, dom, editor);
return [slot.element, slot.getFirstChildOffset() + point.offset];
}
}
function $rangeFromPoints(
editor: LexicalEditor,
start: Point,
startNode: ElementNode | TextNode,
startDOM: HTMLElement,
end: Point,
endNode: ElementNode | TextNode,
endDOM: HTMLElement,
): Range {
const editorDocument = editor._window ? editor._window.document : document;
const range = editorDocument.createRange();
range.setStart(...$rangeTargetFromPoint(editor, start, startNode, startDOM));
range.setEnd(...$rangeTargetFromPoint(editor, end, endNode, endDOM));
return range;
}
function defaultOnReposition(domNodes: readonly HTMLElement[]): void {
for (const domNode of domNodes) {
const domNodeStyle = domNode.style;
if (domNodeStyle.background !== 'Highlight') {
domNodeStyle.background = 'Highlight';
}
if (domNodeStyle.color !== 'HighlightText') {
domNodeStyle.color = 'HighlightText';
}
if (domNodeStyle.marginTop !== px(-1.5)) {
domNodeStyle.marginTop = px(-1.5);
}
if (domNodeStyle.paddingTop !== px(4)) {
domNodeStyle.paddingTop = px(4);
}
if (domNodeStyle.paddingBottom !== px(0)) {
domNodeStyle.paddingBottom = px(0);
}
}
}
/**
* Place one or multiple newly created Nodes at the current selection. Multiple
* nodes will only be created when the selection spans multiple lines (aka
* client rects).
*
* This function can come useful when you want to show the selection but the
* editor has been focused away.
*/
export default function markSelection(
editor: LexicalEditor,
onReposition: (node: readonly HTMLElement[]) => void = defaultOnReposition,
): () => void {
let previousAnchorNode: null | TextNode | ElementNode = null;
let previousAnchorNodeDOM: null | HTMLElement = null;
let previousAnchorOffset: null | number = null;
let previousFocusNode: null | TextNode | ElementNode = null;
let previousFocusNodeDOM: null | HTMLElement = null;
let previousFocusOffset: null | number = null;
let removeRangeListener: () => void = () => {};
function compute(editorState: EditorState) {
editorState.read(
() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
// TODO
previousAnchorNode = null;
previousAnchorOffset = null;
previousFocusNode = null;
previousFocusOffset = null;
removeRangeListener();
removeRangeListener = () => {};
return;
}
const [start, end] = $getOrderedSelectionPoints(selection);
const currentStartNode = start.getNode() as TextNode | ElementNode;
const currentStartNodeKey = currentStartNode.getKey();
const currentStartOffset = start.offset;
const currentEndNode = end.getNode() as TextNode | ElementNode;
const currentEndNodeKey = currentEndNode.getKey();
const currentEndOffset = end.offset;
const currentStartNodeDOM = editor.getElementByKey(currentStartNodeKey);
const currentEndNodeDOM = editor.getElementByKey(currentEndNodeKey);
const differentStartDOM =
previousAnchorNode === null ||
currentStartNodeDOM !== previousAnchorNodeDOM ||
currentStartOffset !== previousAnchorOffset ||
currentStartNodeKey !== previousAnchorNode.getKey();
const differentEndDOM =
previousFocusNode === null ||
currentEndNodeDOM !== previousFocusNodeDOM ||
currentEndOffset !== previousFocusOffset ||
currentEndNodeKey !== previousFocusNode.getKey();
if (
(differentStartDOM || differentEndDOM) &&
currentStartNodeDOM !== null &&
currentEndNodeDOM !== null
) {
const range = $rangeFromPoints(
editor,
start,
currentStartNode,
currentStartNodeDOM,
end,
currentEndNode,
currentEndNodeDOM,
);
removeRangeListener();
removeRangeListener = positionNodeOnRange(
editor,
range,
onReposition,
);
}
previousAnchorNode = currentStartNode;
previousAnchorNodeDOM = currentStartNodeDOM;
previousAnchorOffset = currentStartOffset;
previousFocusNode = currentEndNode;
previousFocusNodeDOM = currentEndNodeDOM;
previousFocusOffset = currentEndOffset;
// Pass {editor} so the active editor is set: $rangeTargetFromPoint reads
// the slot (getFirstChildOffset), which consults the active editor to
// skip the block cursor.
},
{editor},
);
}
compute(editor.getEditorState());
return mergeRegister(
editor.registerUpdateListener(({editorState}) => compute(editorState)),
() => {
removeRangeListener();
},
);
}