UNPKG

@lexical/utils

Version:

This package contains misc utilities for Lexical.

184 lines (175 loc) • 6.07 kB
/** * 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(); }, ); }