]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/utils/markSelection.ts
Images: Added testing to cover animated avif handling
[bookstack] / resources / js / wysiwyg / lexical / utils / markSelection.ts
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import {
10   $getSelection,
11   $isRangeSelection,
12   type EditorState,
13   ElementNode,
14   type LexicalEditor,
15   TextNode,
16 } from 'lexical';
17 import invariant from 'lexical/shared/invariant';
18
19 import mergeRegister from './mergeRegister';
20 import positionNodeOnRange from './positionNodeOnRange';
21 import px from './px';
22
23 export default function markSelection(
24   editor: LexicalEditor,
25   onReposition?: (node: Array<HTMLElement>) => void,
26 ): () => 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)) {
36         // TODO
37         previousAnchorNode = null;
38         previousAnchorOffset = null;
39         previousFocusNode = null;
40         previousFocusOffset = null;
41         removeRangeListener();
42         removeRangeListener = () => {};
43         return;
44       }
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(
62               previousAnchorNode,
63               currentAnchorNodeDOM,
64               editor._config,
65             )));
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(
74               previousFocusNode,
75               currentFocusNodeDOM,
76               editor._config,
77             )));
78       if (differentAnchorDOM || differentFocusDOM) {
79         const anchorHTMLElement = editor.getElementByKey(
80           anchor.getNode().getKey(),
81         );
82         const focusHTMLElement = editor.getElementByKey(
83           focus.getNode().getKey(),
84         );
85         // TODO handle selection beyond the common TextNode
86         if (
87           anchorHTMLElement !== null &&
88           focusHTMLElement !== null &&
89           anchorHTMLElement.tagName === 'SPAN' &&
90           focusHTMLElement.tagName === 'SPAN'
91         ) {
92           const range = document.createRange();
93           let firstHTMLElement;
94           let firstOffset;
95           let lastHTMLElement;
96           let lastOffset;
97           if (focus.isBefore(anchor)) {
98             firstHTMLElement = focusHTMLElement;
99             firstOffset = focus.offset;
100             lastHTMLElement = anchorHTMLElement;
101             lastOffset = anchor.offset;
102           } else {
103             firstHTMLElement = anchorHTMLElement;
104             firstOffset = anchor.offset;
105             lastHTMLElement = focusHTMLElement;
106             lastOffset = focus.offset;
107           }
108           const firstTextNode = firstHTMLElement.firstChild;
109           invariant(
110             firstTextNode !== null,
111             'Expected text node to be first child of span',
112           );
113           const lastTextNode = lastHTMLElement.firstChild;
114           invariant(
115             lastTextNode !== null,
116             'Expected text node to be first child of span',
117           );
118           range.setStart(firstTextNode, firstOffset);
119           range.setEnd(lastTextNode, lastOffset);
120           removeRangeListener();
121           removeRangeListener = positionNodeOnRange(
122             editor,
123             range,
124             (domNodes) => {
125               for (const domNode of domNodes) {
126                 const domNodeStyle = domNode.style;
127                 if (domNodeStyle.background !== 'Highlight') {
128                   domNodeStyle.background = 'Highlight';
129                 }
130                 if (domNodeStyle.color !== 'HighlightText') {
131                   domNodeStyle.color = 'HighlightText';
132                 }
133                 if (domNodeStyle.zIndex !== '-1') {
134                   domNodeStyle.zIndex = '-1';
135                 }
136                 if (domNodeStyle.pointerEvents !== 'none') {
137                   domNodeStyle.pointerEvents = 'none';
138                 }
139                 if (domNodeStyle.marginTop !== px(-1.5)) {
140                   domNodeStyle.marginTop = px(-1.5);
141                 }
142                 if (domNodeStyle.paddingTop !== px(4)) {
143                   domNodeStyle.paddingTop = px(4);
144                 }
145                 if (domNodeStyle.paddingBottom !== px(0)) {
146                   domNodeStyle.paddingBottom = px(0);
147                 }
148               }
149               if (onReposition !== undefined) {
150                 onReposition(domNodes);
151               }
152             },
153           );
154         }
155       }
156       previousAnchorNode = currentAnchorNode;
157       previousAnchorOffset = currentAnchorOffset;
158       previousFocusNode = currentFocusNode;
159       previousFocusOffset = currentFocusOffset;
160     });
161   }
162   compute(editor.getEditorState());
163   return mergeRegister(
164     editor.registerUpdateListener(({editorState}) => compute(editorState)),
165     removeRangeListener,
166     () => {
167       removeRangeListener();
168     },
169   );
170 }