2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
17 import invariant from 'lexical/shared/invariant';
19 import mergeRegister from './mergeRegister';
20 import positionNodeOnRange from './positionNodeOnRange';
21 import px from './px';
23 export default function markSelection(
24 editor: LexicalEditor,
25 onReposition?: (node: Array<HTMLElement>) => void,
27 let previousAnchorNode: null | TextNode | ElementNode = null;
28 let previousAnchorOffset: null | number = null;
29 let previousFocusNode: null | TextNode | ElementNode = null;
30 let previousFocusOffset: null | number = null;
31 let removeRangeListener: () => void = () => {};
32 function compute(editorState: EditorState) {
33 editorState.read(() => {
34 const selection = $getSelection();
35 if (!$isRangeSelection(selection)) {
37 previousAnchorNode = null;
38 previousAnchorOffset = null;
39 previousFocusNode = null;
40 previousFocusOffset = null;
41 removeRangeListener();
42 removeRangeListener = () => {};
45 const {anchor, focus} = selection;
46 const currentAnchorNode = anchor.getNode();
47 const currentAnchorNodeKey = currentAnchorNode.getKey();
48 const currentAnchorOffset = anchor.offset;
49 const currentFocusNode = focus.getNode();
50 const currentFocusNodeKey = currentFocusNode.getKey();
51 const currentFocusOffset = focus.offset;
52 const currentAnchorNodeDOM = editor.getElementByKey(currentAnchorNodeKey);
53 const currentFocusNodeDOM = editor.getElementByKey(currentFocusNodeKey);
54 const differentAnchorDOM =
55 previousAnchorNode === null ||
56 currentAnchorNodeDOM === null ||
57 currentAnchorOffset !== previousAnchorOffset ||
58 currentAnchorNodeKey !== previousAnchorNode.getKey() ||
59 (currentAnchorNode !== previousAnchorNode &&
60 (!(previousAnchorNode instanceof TextNode) ||
61 currentAnchorNode.updateDOM(
66 const differentFocusDOM =
67 previousFocusNode === null ||
68 currentFocusNodeDOM === null ||
69 currentFocusOffset !== previousFocusOffset ||
70 currentFocusNodeKey !== previousFocusNode.getKey() ||
71 (currentFocusNode !== previousFocusNode &&
72 (!(previousFocusNode instanceof TextNode) ||
73 currentFocusNode.updateDOM(
78 if (differentAnchorDOM || differentFocusDOM) {
79 const anchorHTMLElement = editor.getElementByKey(
80 anchor.getNode().getKey(),
82 const focusHTMLElement = editor.getElementByKey(
83 focus.getNode().getKey(),
85 // TODO handle selection beyond the common TextNode
87 anchorHTMLElement !== null &&
88 focusHTMLElement !== null &&
89 anchorHTMLElement.tagName === 'SPAN' &&
90 focusHTMLElement.tagName === 'SPAN'
92 const range = document.createRange();
97 if (focus.isBefore(anchor)) {
98 firstHTMLElement = focusHTMLElement;
99 firstOffset = focus.offset;
100 lastHTMLElement = anchorHTMLElement;
101 lastOffset = anchor.offset;
103 firstHTMLElement = anchorHTMLElement;
104 firstOffset = anchor.offset;
105 lastHTMLElement = focusHTMLElement;
106 lastOffset = focus.offset;
108 const firstTextNode = firstHTMLElement.firstChild;
110 firstTextNode !== null,
111 'Expected text node to be first child of span',
113 const lastTextNode = lastHTMLElement.firstChild;
115 lastTextNode !== null,
116 'Expected text node to be first child of span',
118 range.setStart(firstTextNode, firstOffset);
119 range.setEnd(lastTextNode, lastOffset);
120 removeRangeListener();
121 removeRangeListener = positionNodeOnRange(
125 for (const domNode of domNodes) {
126 const domNodeStyle = domNode.style;
127 if (domNodeStyle.background !== 'Highlight') {
128 domNodeStyle.background = 'Highlight';
130 if (domNodeStyle.color !== 'HighlightText') {
131 domNodeStyle.color = 'HighlightText';
133 if (domNodeStyle.zIndex !== '-1') {
134 domNodeStyle.zIndex = '-1';
136 if (domNodeStyle.pointerEvents !== 'none') {
137 domNodeStyle.pointerEvents = 'none';
139 if (domNodeStyle.marginTop !== px(-1.5)) {
140 domNodeStyle.marginTop = px(-1.5);
142 if (domNodeStyle.paddingTop !== px(4)) {
143 domNodeStyle.paddingTop = px(4);
145 if (domNodeStyle.paddingBottom !== px(0)) {
146 domNodeStyle.paddingBottom = px(0);
149 if (onReposition !== undefined) {
150 onReposition(domNodes);
156 previousAnchorNode = currentAnchorNode;
157 previousAnchorOffset = currentAnchorOffset;
158 previousFocusNode = currentFocusNode;
159 previousFocusOffset = currentFocusOffset;
162 compute(editor.getEditorState());
163 return mergeRegister(
164 editor.registerUpdateListener(({editorState}) => compute(editorState)),
167 removeRangeListener();