import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { Position, EditorStyleConfig, HistoryState } from '../lib' import { cleanDom, HTMLEditor } from '../lib' interface UseInjectModeOptions { styleConfig?: EditorStyleConfig enableGlobalContentEditable?: boolean onContentChange?: (srcDoc: string) => void onHistoryChange?: (state: HistoryState, editor: HTMLEditor) => void enabled?: boolean } const isValidDom = (innerText: string) => { try { const data = JSON.parse(innerText) if (data && !data.success) { return false } } catch { return true } } const isIfrmae = (target: HTMLElement) => { return target.tagName === 'IFRAME' || target instanceof HTMLIFrameElement } export interface UseInjectModeReturn { editor: HTMLEditor | null editorIns: HTMLEditor | null selectedElement: HTMLElement | null position: Position | null tipPosition: Position | null injectScript: (targetContainer: HTMLElement) => Promise // 历史记录相关 canUndo: boolean canRedo: boolean clearHistory: () => void loadSuccess: boolean } function waitForIframeReady( iframe: HTMLIFrameElement, timeout = 5000, ): Promise { return new Promise(resolve => { const start = performance.now() const tryAttach = () => { const doc = iframe.contentDocument if (!doc) { if (performance.now() - start > timeout) return resolve() return requestAnimationFrame(tryAttach) } // 如果已经 complete,直接 resolve if (doc.readyState === 'complete') { return resolve() } let stableTimer: any const observer = new MutationObserver(() => { clearTimeout(stableTimer) stableTimer = setTimeout(() => { observer.disconnect() resolve() }, 800) }) observer.observe(doc.documentElement, { childList: true, subtree: true, attributes: true, }) // 超时保护 setTimeout(() => { observer.disconnect() clearTimeout(stableTimer) console.error('iframe ready timeout') resolve() }, timeout) // 同时监听 readyState const checkReady = () => { if (doc.readyState === 'complete') { observer.disconnect() clearTimeout(stableTimer) resolve() } else if (performance.now() - start < timeout) { requestAnimationFrame(checkReady) } } checkReady() } tryAttach() }) } export function useIframeMode( id: string, container: React.RefObject, options?: UseInjectModeOptions, scale = 1, ): UseInjectModeReturn { const editorRef = useRef(null) const [editorIns, setEditorIns] = useState(null) const [selectedElement, setSelectedElement] = useState( null, ) const [position, setPosition] = useState(null) const [canUndo, setCanUndo] = useState(false) const [canRedo, setCanRedo] = useState(false) const [loadSuccess, setLoadSuccess] = useState(false) const injectScript = useCallback(async (targetContainer: HTMLElement) => { if (!targetContainer) { console.error('ifrmae is null') return } if (editorRef.current) { console.error('ifrmae is destroyed') editorRef.current.destroy() } const editor = new HTMLEditor({ id, styleConfig: options?.styleConfig, enableGlobalContentEditable: options?.enableGlobalContentEditable, onElementSelect: (element: HTMLElement | null, pos?: Position) => { setSelectedElement(element) if (element && pos) { setPosition(pos) } else { setPosition(null) } }, onStyleChange: (element: HTMLElement) => { if (element) { const pos = element.getBoundingClientRect() setPosition(pos) } }, onContentChange: () => { // 触发内容变化回调,数据清洗 if (options?.onContentChange) { if (editor.isIframe) { const iframe = container.current as HTMLIFrameElement const iframeDoc = iframe.contentDocument?.documentElement if (iframeDoc) { const srcDoc = cleanDom(iframeDoc) options.onContentChange(srcDoc) } } else { const srcDoc = cleanDom(container.current as HTMLElement) options.onContentChange(srcDoc) } } }, onHistoryChange: (state: HistoryState) => { setCanUndo(state.canUndo) setCanRedo(state.canRedo) options?.onHistoryChange?.(state, editor) }, enableMoveable: true, helperBox: true, enableHistory: true, historyOptions: { maxHistorySize: 100, mergeInterval: 1000, }, }) editor.init(targetContainer) editorRef.current = editor setEditorIns(editor) }, [id, options, container]) useEffect(() => { const element = container.current if (!element || options?.enabled === false) { return } const isIframeEle = isIfrmae(element) const handleKeyDown = (e: KeyboardEvent) => { // macos 快捷键 command + z 撤销 / command + shift + z 重做 if (e.metaKey && e.key === 'z' && e.shiftKey) { e.preventDefault() editorRef.current?.redo() } if (e.metaKey && e.key === 'z' && !e.shiftKey) { e.preventDefault() editorRef.current?.undo() } } if (isIframeEle) { // iframe 加载 const onLoad = async () => { await waitForIframeReady(element as HTMLIFrameElement) const doc = (element as HTMLIFrameElement).contentDocument! // 判断是否是合法的 iframe 内容 const loadSuccess = isValidDom(doc.body.innerText) if (loadSuccess) { injectScript(doc.body) console.log('编辑器加载成功') setLoadSuccess(true) const document = (element as HTMLIFrameElement).contentDocument ?.documentElement document?.addEventListener('keydown', handleKeyDown) return () => { document?.removeEventListener('keydown', handleKeyDown) } } } ; (element as HTMLIFrameElement).onload = onLoad } else { injectScript(element) setLoadSuccess(true) document?.addEventListener('keydown', handleKeyDown) return () => { document?.removeEventListener('keydown', handleKeyDown) } } }, [injectScript, container, options?.enabled]) useEffect(() => { return () => { if (editorRef.current && loadSuccess) { console.log('销毁编辑器') editorRef.current.destroy() } } }, [loadSuccess]) const tipPosition = useMemo(() => { if (!container.current || !position) { return null } const elementRect = container.current.getBoundingClientRect() return { top: position.top * scale + elementRect.top, left: position.left * scale, width: position.width * scale, height: position.height * scale, bottom: position.bottom * scale + elementRect.top, right: position.right * scale + elementRect.left, } }, [position, scale, container]) return { editor: editorRef.current, editorIns, selectedElement, position, tipPosition, injectScript, canUndo, canRedo, loadSuccess, clearHistory: () => editorRef.current?.clearHistory(), } }