142 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
		
		
			
		
	
	
			142 lines
		
	
	
		
			4.1 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 type {LexicalEditor} from 'lexical'; | ||
|  | 
 | ||
|  | import {createRectsFromDOMRange} from '@lexical/selection'; | ||
|  | import invariant from 'lexical/shared/invariant'; | ||
|  | 
 | ||
|  | import px from './px'; | ||
|  | 
 | ||
|  | const mutationObserverConfig = { | ||
|  |   attributes: true, | ||
|  |   characterData: true, | ||
|  |   childList: true, | ||
|  |   subtree: true, | ||
|  | }; | ||
|  | 
 | ||
|  | export default function positionNodeOnRange( | ||
|  |   editor: LexicalEditor, | ||
|  |   range: Range, | ||
|  |   onReposition: (node: Array<HTMLElement>) => void, | ||
|  | ): () => void { | ||
|  |   let rootDOMNode: null | HTMLElement = null; | ||
|  |   let parentDOMNode: null | HTMLElement = null; | ||
|  |   let observer: null | MutationObserver = null; | ||
|  |   let lastNodes: Array<HTMLElement> = []; | ||
|  |   const wrapperNode = document.createElement('div'); | ||
|  | 
 | ||
|  |   function position(): void { | ||
|  |     invariant(rootDOMNode !== null, 'Unexpected null rootDOMNode'); | ||
|  |     invariant(parentDOMNode !== null, 'Unexpected null parentDOMNode'); | ||
|  |     const {left: rootLeft, top: rootTop} = rootDOMNode.getBoundingClientRect(); | ||
|  |     const parentDOMNode_ = parentDOMNode; | ||
|  |     const rects = createRectsFromDOMRange(editor, range); | ||
|  |     if (!wrapperNode.isConnected) { | ||
|  |       parentDOMNode_.append(wrapperNode); | ||
|  |     } | ||
|  |     let hasRepositioned = false; | ||
|  |     for (let i = 0; i < rects.length; i++) { | ||
|  |       const rect = rects[i]; | ||
|  |       // Try to reuse the previously created Node when possible, no need to
 | ||
|  |       // remove/create on the most common case reposition case
 | ||
|  |       const rectNode = lastNodes[i] || document.createElement('div'); | ||
|  |       const rectNodeStyle = rectNode.style; | ||
|  |       if (rectNodeStyle.position !== 'absolute') { | ||
|  |         rectNodeStyle.position = 'absolute'; | ||
|  |         hasRepositioned = true; | ||
|  |       } | ||
|  |       const left = px(rect.left - rootLeft); | ||
|  |       if (rectNodeStyle.left !== left) { | ||
|  |         rectNodeStyle.left = left; | ||
|  |         hasRepositioned = true; | ||
|  |       } | ||
|  |       const top = px(rect.top - rootTop); | ||
|  |       if (rectNodeStyle.top !== top) { | ||
|  |         rectNode.style.top = top; | ||
|  |         hasRepositioned = true; | ||
|  |       } | ||
|  |       const width = px(rect.width); | ||
|  |       if (rectNodeStyle.width !== width) { | ||
|  |         rectNode.style.width = width; | ||
|  |         hasRepositioned = true; | ||
|  |       } | ||
|  |       const height = px(rect.height); | ||
|  |       if (rectNodeStyle.height !== height) { | ||
|  |         rectNode.style.height = height; | ||
|  |         hasRepositioned = true; | ||
|  |       } | ||
|  |       if (rectNode.parentNode !== wrapperNode) { | ||
|  |         wrapperNode.append(rectNode); | ||
|  |         hasRepositioned = true; | ||
|  |       } | ||
|  |       lastNodes[i] = rectNode; | ||
|  |     } | ||
|  |     while (lastNodes.length > rects.length) { | ||
|  |       lastNodes.pop(); | ||
|  |     } | ||
|  |     if (hasRepositioned) { | ||
|  |       onReposition(lastNodes); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function stop(): void { | ||
|  |     parentDOMNode = null; | ||
|  |     rootDOMNode = null; | ||
|  |     if (observer !== null) { | ||
|  |       observer.disconnect(); | ||
|  |     } | ||
|  |     observer = null; | ||
|  |     wrapperNode.remove(); | ||
|  |     for (const node of lastNodes) { | ||
|  |       node.remove(); | ||
|  |     } | ||
|  |     lastNodes = []; | ||
|  |   } | ||
|  | 
 | ||
|  |   function restart(): void { | ||
|  |     const currentRootDOMNode = editor.getRootElement(); | ||
|  |     if (currentRootDOMNode === null) { | ||
|  |       return stop(); | ||
|  |     } | ||
|  |     const currentParentDOMNode = currentRootDOMNode.parentElement; | ||
|  |     if (!(currentParentDOMNode instanceof HTMLElement)) { | ||
|  |       return stop(); | ||
|  |     } | ||
|  |     stop(); | ||
|  |     rootDOMNode = currentRootDOMNode; | ||
|  |     parentDOMNode = currentParentDOMNode; | ||
|  |     observer = new MutationObserver((mutations) => { | ||
|  |       const nextRootDOMNode = editor.getRootElement(); | ||
|  |       const nextParentDOMNode = | ||
|  |         nextRootDOMNode && nextRootDOMNode.parentElement; | ||
|  |       if ( | ||
|  |         nextRootDOMNode !== rootDOMNode || | ||
|  |         nextParentDOMNode !== parentDOMNode | ||
|  |       ) { | ||
|  |         return restart(); | ||
|  |       } | ||
|  |       for (const mutation of mutations) { | ||
|  |         if (!wrapperNode.contains(mutation.target)) { | ||
|  |           // TODO throttle
 | ||
|  |           return position(); | ||
|  |         } | ||
|  |       } | ||
|  |     }); | ||
|  |     observer.observe(currentParentDOMNode, mutationObserverConfig); | ||
|  |     position(); | ||
|  |   } | ||
|  | 
 | ||
|  |   const removeRootListener = editor.registerRootListener(restart); | ||
|  | 
 | ||
|  |   return () => { | ||
|  |     removeRootListener(); | ||
|  |     stop(); | ||
|  |   }; | ||
|  | } |