211 lines
6.3 KiB
TypeScript
211 lines
6.3 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
import type { Editor } from "@tiptap/react"
|
|
import { apiClient } from "@/lib/api/client"
|
|
import {
|
|
notifyParagraphStyleCatalog,
|
|
setActiveParagraphStyleCatalog,
|
|
} from "@/lib/drive/docs-paragraph-style-bridge"
|
|
import { buildParagraphStylesCss } from "@/lib/drive/docs-paragraph-styles-css"
|
|
import type { DocParagraphStyleDefinition, DocParagraphStylesCatalog } from "@/lib/drive/docs-paragraph-styles"
|
|
import {
|
|
createCustomParagraphStyle,
|
|
defaultDocumentParagraphStyles,
|
|
emptyUserParagraphStyles,
|
|
mergeParagraphStyleCatalogs,
|
|
} from "@/lib/drive/docs-paragraph-styles"
|
|
import type { DocParagraphStylesState } from "@/lib/drive/docs-styles-types"
|
|
|
|
const STYLES_DEBOUNCE_MS = 600
|
|
|
|
function parseCatalog(raw: unknown): DocParagraphStylesCatalog | null {
|
|
if (!raw || typeof raw !== "object") return null
|
|
const definitions = (raw as DocParagraphStylesCatalog).definitions
|
|
if (!definitions || typeof definitions !== "object") return null
|
|
return { definitions }
|
|
}
|
|
|
|
export function useDocsParagraphStyles({
|
|
editor,
|
|
initialDocumentStyles,
|
|
editable,
|
|
canonicalPath,
|
|
saveUrl,
|
|
}: {
|
|
editor: Editor | null
|
|
initialDocumentStyles?: DocParagraphStylesCatalog | null
|
|
editable?: boolean
|
|
canonicalPath?: string
|
|
saveUrl?: string
|
|
}) {
|
|
const [documentStyles, setDocumentStyles] = useState<DocParagraphStylesCatalog>(
|
|
() => initialDocumentStyles ?? defaultDocumentParagraphStyles()
|
|
)
|
|
const [userStyles, setUserStyles] = useState<DocParagraphStylesCatalog>(emptyUserParagraphStyles)
|
|
const documentStylesRef = useRef(documentStyles)
|
|
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
|
|
useEffect(() => {
|
|
documentStylesRef.current = documentStyles
|
|
}, [documentStyles])
|
|
|
|
useEffect(() => {
|
|
if (initialDocumentStyles) setDocumentStyles(initialDocumentStyles)
|
|
}, [initialDocumentStyles])
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
void apiClient
|
|
.get<DocParagraphStylesCatalog>("/richtext/user-paragraph-styles")
|
|
.then((catalog) => {
|
|
if (cancelled) return
|
|
setUserStyles(parseCatalog(catalog) ?? emptyUserParagraphStyles())
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setUserStyles(emptyUserParagraphStyles())
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [])
|
|
|
|
const mergedCatalog = useMemo(
|
|
() => mergeParagraphStyleCatalogs(documentStyles, userStyles),
|
|
[documentStyles, userStyles]
|
|
)
|
|
|
|
useEffect(() => {
|
|
setActiveParagraphStyleCatalog(mergedCatalog)
|
|
notifyParagraphStyleCatalog(mergedCatalog)
|
|
}, [mergedCatalog])
|
|
|
|
useEffect(() => {
|
|
const css = buildParagraphStylesCss(mergedCatalog)
|
|
const id = "docs-paragraph-styles-dynamic"
|
|
let el = document.getElementById(id) as HTMLStyleElement | null
|
|
if (!el) {
|
|
el = document.createElement("style")
|
|
el.id = id
|
|
document.head.appendChild(el)
|
|
}
|
|
el.textContent = css
|
|
}, [mergedCatalog])
|
|
|
|
const flushDocumentStylesSave = useCallback(async () => {
|
|
if (!editable || !canonicalPath) return
|
|
const payload = { path: canonicalPath, paragraphStyles: documentStylesRef.current }
|
|
if (saveUrl) {
|
|
const res = await fetch(saveUrl, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
})
|
|
if (!res.ok) throw new Error("paragraph styles save failed")
|
|
return
|
|
}
|
|
await apiClient.put("/richtext/save", payload)
|
|
}, [canonicalPath, editable, saveUrl])
|
|
|
|
const scheduleDocumentStylesSave = useCallback(() => {
|
|
if (!editable) return
|
|
if (saveTimer.current) clearTimeout(saveTimer.current)
|
|
saveTimer.current = setTimeout(() => {
|
|
void flushDocumentStylesSave().catch(() => undefined)
|
|
}, STYLES_DEBOUNCE_MS)
|
|
}, [editable, flushDocumentStylesSave])
|
|
|
|
const saveUserStyles = useCallback(async (catalog: DocParagraphStylesCatalog) => {
|
|
setUserStyles(catalog)
|
|
await apiClient.put("/richtext/user-paragraph-styles", catalog)
|
|
}, [])
|
|
|
|
const updateDocumentStyle = useCallback(
|
|
(styleId: string, definition: DocParagraphStyleDefinition) => {
|
|
setDocumentStyles((prev) => ({
|
|
definitions: {
|
|
...prev.definitions,
|
|
[styleId]: { ...definition, id: styleId, scope: "document" },
|
|
},
|
|
}))
|
|
scheduleDocumentStylesSave()
|
|
},
|
|
[scheduleDocumentStylesSave]
|
|
)
|
|
|
|
const updateUserStyle = useCallback(
|
|
(styleId: string, definition: DocParagraphStyleDefinition) => {
|
|
setUserStyles((prev) => {
|
|
const next = {
|
|
definitions: {
|
|
...prev.definitions,
|
|
[styleId]: { ...definition, id: styleId, scope: "user" as const },
|
|
},
|
|
}
|
|
void saveUserStyles(next)
|
|
return next
|
|
})
|
|
},
|
|
[saveUserStyles]
|
|
)
|
|
|
|
const createUserStyle = useCallback(
|
|
(input: { name: string; basedOn?: string }) => {
|
|
const template = mergedCatalog.definitions[input.basedOn ?? "normal"]
|
|
const created = createCustomParagraphStyle({ ...input, template })
|
|
setUserStyles((prev) => {
|
|
const next = {
|
|
definitions: { ...prev.definitions, [created.id]: created },
|
|
}
|
|
void saveUserStyles(next)
|
|
return next
|
|
})
|
|
return created
|
|
},
|
|
[mergedCatalog.definitions, saveUserStyles]
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!editor) return
|
|
const onUpdate = ({
|
|
styleId,
|
|
definition,
|
|
}: {
|
|
styleId: string
|
|
definition: DocParagraphStyleDefinition
|
|
}) => {
|
|
if (definition.scope === "user") updateUserStyle(styleId, definition)
|
|
else updateDocumentStyle(styleId, definition)
|
|
}
|
|
editor.on("docsParagraphStyleUpdate", onUpdate)
|
|
return () => {
|
|
editor.off("docsParagraphStyleUpdate", onUpdate)
|
|
}
|
|
}, [editor, updateDocumentStyle, updateUserStyle])
|
|
|
|
useEffect(
|
|
() => () => {
|
|
if (saveTimer.current) clearTimeout(saveTimer.current)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const state: DocParagraphStylesState = useMemo(
|
|
() => ({ documentStyles, userStyles, mergedCatalog }),
|
|
[documentStyles, mergedCatalog, userStyles]
|
|
)
|
|
|
|
return {
|
|
state,
|
|
updateDocumentStyle,
|
|
updateUserStyle,
|
|
createUserStyle,
|
|
applyStyle: (styleId: string) => {
|
|
editor?.commands.applyDocsParagraphStyle(styleId)
|
|
},
|
|
updateStyleFromSelection: (styleId: string) => {
|
|
editor?.commands.updateDocsParagraphStyleFromSelection(styleId)
|
|
},
|
|
}
|
|
}
|