初始化模版工程

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,35 @@
import { useEffect } from 'react'
import { usePPTEditContext } from '../context'
import { useToolPostion } from './useToolPostion'
import type { UseInjectModeReturn } from '../hooks/useIframeMode'
export const useDiff = (
useIframeReturn: UseInjectModeReturn,
isDoc?: boolean,
) => {
const editorContext = usePPTEditContext()
const { selectedElement, editor, tipPosition } = useIframeReturn
const docModeToolPosition = useToolPostion(editor, !!isDoc)
useEffect(() => {
const hasActive = editor?.EditorRegistry.hasActiveEditor()
const reWriteState = { ...useIframeReturn }
if (isDoc) {
// 重写状态
reWriteState.position = docModeToolPosition
reWriteState.tipPosition = docModeToolPosition
editorContext?.setState(reWriteState)
return
}
if (hasActive && selectedElement) {
// 有激活的实例且为当前的实例
editorContext?.setState(reWriteState)
} else if (!selectedElement && !hasActive) {
// 失焦后清空所有状态关闭tip
reWriteState.position = null
reWriteState.tipPosition = null
editorContext?.setState(reWriteState)
}
}, [selectedElement, editor, tipPosition, isDoc, docModeToolPosition])
}

View File

@@ -0,0 +1,42 @@
import { useRef, useState } from 'react';
import type { HTMLEditor } from '../lib';
export type SaveType = 'manual' | 'auto' | null;
export const useEditState = () => {
const [isSaving, setIsSaving] = useState(false);
const [saveType, setSaveType] = useState<SaveType>(null);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const redo = useRef(() => { });
const undo = useRef(() => { });
const handleHistoryChangeEvent = (instance: HTMLEditor) => {
if (!instance) return null
const canRedo = instance.EditorRegistry.canRedo()
const canUndo = instance.EditorRegistry.canUndo()
setCanRedo(canRedo)
setCanUndo(canUndo)
redo.current = () => {
instance.EditorRegistry.redo()
}
undo.current = () => {
instance.EditorRegistry.undo()
}
}
return {
isSaving,
setIsSaving,
saveType,
setSaveType,
handleHistoryChangeEvent,
canRedo,
setCanRedo,
canUndo,
setCanUndo,
redo,
undo,
}
}

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

View File

@@ -0,0 +1,25 @@
import { useEffect, useState } from 'react';
import { useNovaKit } from '@/components/nova-sdk/context/useNovaKit';
import type { TaskArtifact } from '@/components/nova-sdk/types';
export const useLoadContent = (
taskArtifact: TaskArtifact
) => {
const [content, setContent] = useState('');
const { api } = useNovaKit();
useEffect(() => {
if (taskArtifact.path) {
api
.getArtifactUrl?.(taskArtifact)
.then(async (res) => {
const url = res?.data || '';
if (url) {
const content = await fetch(url).then((res) => res.text());
setContent(content);
}
})
.catch(() => { });
}
}, [taskArtifact, api]);
return content;
};

View File

@@ -0,0 +1,82 @@
import { HTMLEditor } from '../lib'
import { useEffect, useRef, useState } from 'react'
export const useToolPostion = (editor: HTMLEditor | null, isDoc: boolean) => {
const [position, setPosition] = useState<{ left: number; top: number, bottom: number, right: number, width: number, height: number } | null>(null)
// 标识是否从编辑器内开始选择
const isSelectingRef = useRef(false)
const updatePositionFromSelection = () => {
if (!editor) {
return
}
const view = editor.getDoc().view
if (!view) {
return
}
const selection = view.getSelection()
if (!selection) return
// 如果选区为空,不需要更新
const hasSelectAnything = selection.rangeCount === 0 || selection.toString().trim() === ''
if (selection.isCollapsed || selection.rangeCount === 0 || hasSelectAnything) {
setPosition(null)
return
}
// 获取选区的 Range 对象
const range = selection.getRangeAt(0);
// 获取选区末尾的位置
// 创建一个新的 Range只包含选区的末尾点
const endRange = range.cloneRange();
endRange.collapse(false); // false 表示折叠到末尾
const rect = endRange.getBoundingClientRect();
const { top, left, bottom, right, width, height } = rect
setPosition({ top, left, bottom, right, width, height })
}
useEffect(() => {
if (!editor || !isDoc) {
return
}
const dom = editor.getDoc().document
if (!dom) {
return
}
const onMouseDown = () => {
isSelectingRef.current = true
}
const onMouseUp = () => {
// 只有从编辑器内开始选择时才处理
if (!isSelectingRef.current) {
return
}
isSelectingRef.current = false
setTimeout(() => {
updatePositionFromSelection()
}, 50)
}
const onScrollOrResize = () => {
updatePositionFromSelection()
}
dom.addEventListener('mousedown', onMouseDown)
dom.addEventListener('mouseup', onMouseUp)
dom.addEventListener('scroll', onScrollOrResize)
window.addEventListener('scroll', onScrollOrResize, true)
window.addEventListener('resize', onScrollOrResize)
return () => {
dom.removeEventListener('mousedown', onMouseDown)
dom.removeEventListener('mouseup', onMouseUp)
dom.removeEventListener('scroll', onScrollOrResize)
window.removeEventListener('scroll', onScrollOrResize, true)
window.removeEventListener('resize', onScrollOrResize)
}
}, [editor, updatePositionFromSelection, isDoc])
return position
}