初始化模版工程
This commit is contained in:
261
components/html-editor/hooks/useIframeMode.ts
Normal file
261
components/html-editor/hooks/useIframeMode.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
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<void>
|
||||
// 历史记录相关
|
||||
canUndo: boolean
|
||||
canRedo: boolean
|
||||
clearHistory: () => void
|
||||
loadSuccess: boolean
|
||||
}
|
||||
|
||||
function waitForIframeReady(
|
||||
iframe: HTMLIFrameElement,
|
||||
timeout = 5000,
|
||||
): Promise<void> {
|
||||
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<HTMLIFrameElement | HTMLElement | null>,
|
||||
options?: UseInjectModeOptions,
|
||||
scale = 1,
|
||||
): UseInjectModeReturn {
|
||||
const editorRef = useRef<HTMLEditor | null>(null)
|
||||
const [editorIns, setEditorIns] = useState<HTMLEditor | null>(null)
|
||||
const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
|
||||
null,
|
||||
)
|
||||
const [position, setPosition] = useState<Position | null>(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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user