初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

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