171 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			171 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			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 {
 | |
|   $getSelection,
 | |
|   $isRangeSelection,
 | |
|   type EditorState,
 | |
|   ElementNode,
 | |
|   type LexicalEditor,
 | |
|   TextNode,
 | |
| } from 'lexical';
 | |
| import invariant from 'lexical/shared/invariant';
 | |
| 
 | |
| import mergeRegister from './mergeRegister';
 | |
| import positionNodeOnRange from './positionNodeOnRange';
 | |
| import px from './px';
 | |
| 
 | |
| export default function markSelection(
 | |
|   editor: LexicalEditor,
 | |
|   onReposition?: (node: Array<HTMLElement>) => void,
 | |
| ): () => void {
 | |
|   let previousAnchorNode: null | TextNode | ElementNode = null;
 | |
|   let previousAnchorOffset: null | number = null;
 | |
|   let previousFocusNode: null | TextNode | ElementNode = 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 {anchor, focus} = selection;
 | |
|       const currentAnchorNode = anchor.getNode();
 | |
|       const currentAnchorNodeKey = currentAnchorNode.getKey();
 | |
|       const currentAnchorOffset = anchor.offset;
 | |
|       const currentFocusNode = focus.getNode();
 | |
|       const currentFocusNodeKey = currentFocusNode.getKey();
 | |
|       const currentFocusOffset = focus.offset;
 | |
|       const currentAnchorNodeDOM = editor.getElementByKey(currentAnchorNodeKey);
 | |
|       const currentFocusNodeDOM = editor.getElementByKey(currentFocusNodeKey);
 | |
|       const differentAnchorDOM =
 | |
|         previousAnchorNode === null ||
 | |
|         currentAnchorNodeDOM === null ||
 | |
|         currentAnchorOffset !== previousAnchorOffset ||
 | |
|         currentAnchorNodeKey !== previousAnchorNode.getKey() ||
 | |
|         (currentAnchorNode !== previousAnchorNode &&
 | |
|           (!(previousAnchorNode instanceof TextNode) ||
 | |
|             currentAnchorNode.updateDOM(
 | |
|               previousAnchorNode,
 | |
|               currentAnchorNodeDOM,
 | |
|               editor._config,
 | |
|             )));
 | |
|       const differentFocusDOM =
 | |
|         previousFocusNode === null ||
 | |
|         currentFocusNodeDOM === null ||
 | |
|         currentFocusOffset !== previousFocusOffset ||
 | |
|         currentFocusNodeKey !== previousFocusNode.getKey() ||
 | |
|         (currentFocusNode !== previousFocusNode &&
 | |
|           (!(previousFocusNode instanceof TextNode) ||
 | |
|             currentFocusNode.updateDOM(
 | |
|               previousFocusNode,
 | |
|               currentFocusNodeDOM,
 | |
|               editor._config,
 | |
|             )));
 | |
|       if (differentAnchorDOM || differentFocusDOM) {
 | |
|         const anchorHTMLElement = editor.getElementByKey(
 | |
|           anchor.getNode().getKey(),
 | |
|         );
 | |
|         const focusHTMLElement = editor.getElementByKey(
 | |
|           focus.getNode().getKey(),
 | |
|         );
 | |
|         // TODO handle selection beyond the common TextNode
 | |
|         if (
 | |
|           anchorHTMLElement !== null &&
 | |
|           focusHTMLElement !== null &&
 | |
|           anchorHTMLElement.tagName === 'SPAN' &&
 | |
|           focusHTMLElement.tagName === 'SPAN'
 | |
|         ) {
 | |
|           const range = document.createRange();
 | |
|           let firstHTMLElement;
 | |
|           let firstOffset;
 | |
|           let lastHTMLElement;
 | |
|           let lastOffset;
 | |
|           if (focus.isBefore(anchor)) {
 | |
|             firstHTMLElement = focusHTMLElement;
 | |
|             firstOffset = focus.offset;
 | |
|             lastHTMLElement = anchorHTMLElement;
 | |
|             lastOffset = anchor.offset;
 | |
|           } else {
 | |
|             firstHTMLElement = anchorHTMLElement;
 | |
|             firstOffset = anchor.offset;
 | |
|             lastHTMLElement = focusHTMLElement;
 | |
|             lastOffset = focus.offset;
 | |
|           }
 | |
|           const firstTextNode = firstHTMLElement.firstChild;
 | |
|           invariant(
 | |
|             firstTextNode !== null,
 | |
|             'Expected text node to be first child of span',
 | |
|           );
 | |
|           const lastTextNode = lastHTMLElement.firstChild;
 | |
|           invariant(
 | |
|             lastTextNode !== null,
 | |
|             'Expected text node to be first child of span',
 | |
|           );
 | |
|           range.setStart(firstTextNode, firstOffset);
 | |
|           range.setEnd(lastTextNode, lastOffset);
 | |
|           removeRangeListener();
 | |
|           removeRangeListener = positionNodeOnRange(
 | |
|             editor,
 | |
|             range,
 | |
|             (domNodes) => {
 | |
|               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.zIndex !== '-1') {
 | |
|                   domNodeStyle.zIndex = '-1';
 | |
|                 }
 | |
|                 if (domNodeStyle.pointerEvents !== 'none') {
 | |
|                   domNodeStyle.pointerEvents = 'none';
 | |
|                 }
 | |
|                 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);
 | |
|                 }
 | |
|               }
 | |
|               if (onReposition !== undefined) {
 | |
|                 onReposition(domNodes);
 | |
|               }
 | |
|             },
 | |
|           );
 | |
|         }
 | |
|       }
 | |
|       previousAnchorNode = currentAnchorNode;
 | |
|       previousAnchorOffset = currentAnchorOffset;
 | |
|       previousFocusNode = currentFocusNode;
 | |
|       previousFocusOffset = currentFocusOffset;
 | |
|     });
 | |
|   }
 | |
|   compute(editor.getEditorState());
 | |
|   return mergeRegister(
 | |
|     editor.registerUpdateListener(({editorState}) => compute(editorState)),
 | |
|     removeRangeListener,
 | |
|     () => {
 | |
|       removeRangeListener();
 | |
|     },
 | |
|   );
 | |
| }
 |