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(); | ||
|  |     }, | ||
|  |   ); | ||
|  | } |