ultisuite-client/lib/drive/docs-paragraph-style-apply.ts
R3D347HR4Y 303b2b1074
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wow
2026-06-11 01:22:40 +02:00

170 lines
5.6 KiB
TypeScript

import type { Editor } from "@tiptap/react"
import type { DocParagraphStyleDefinition } from "@/lib/drive/docs-paragraph-styles"
import {
inferParagraphStyleIdFromEditorState,
normalizeParagraphStyleId,
resolveParagraphStyleDefinition,
} from "@/lib/drive/docs-paragraph-styles"
import { getActiveParagraphStyleCatalog } from "@/lib/drive/docs-paragraph-style-bridge"
import { fontFamilyStackForName } from "@/lib/drive/docs-font-family"
type BlockNodeType = "paragraph" | "heading"
function blockNodeType(def: DocParagraphStyleDefinition): BlockNodeType {
return def.blockType === "heading" && def.level ? "heading" : "paragraph"
}
function applyRunMarksToRange(
editor: Editor,
from: number,
to: number,
def: DocParagraphStyleDefinition
) {
if (from >= to) return
let chain = editor.chain().setTextSelection({ from, to })
const stack = def.fontFamily
? def.fontFamily.includes(",")
? def.fontFamily
: fontFamilyStackForName(def.fontFamily)
: null
if (stack) chain = chain.setFontFamily(stack)
else chain = chain.unsetFontFamily()
if (def.fontSizePx != null) chain = chain.setFontSize(`${def.fontSizePx}px`)
else chain = chain.unsetFontSize()
if (def.bold) chain = chain.setMark("bold")
else chain = chain.unsetMark("bold")
if (def.italic) chain = chain.setMark("italic")
else chain = chain.unsetMark("italic")
if (def.underline) chain = chain.setMark("underline")
else chain = chain.unsetMark("underline")
if (def.color) chain = chain.setColor(def.color)
else chain = chain.unsetColor()
chain.run()
}
function forEachSelectedBlock(
editor: Editor,
fn: (from: number, to: number, nodeType: BlockNodeType) => void
) {
const { state } = editor
const { from, to } = state.selection
state.doc.nodesBetween(from, to, (node, pos) => {
if (node.type.name !== "paragraph" && node.type.name !== "heading") return
const start = pos
const end = pos + node.content.size
fn(start + 1, end + 1, node.type.name as BlockNodeType)
return false
})
}
export function applyParagraphStyleDefinition(
editor: Editor,
def: DocParagraphStyleDefinition
) {
const nodeType = blockNodeType(def)
const chain = editor.chain().focus() as ReturnType<Editor["chain"]> & {
setParagraph: () => ReturnType<Editor["chain"]>
setHeading: (attrs: { level: number }) => ReturnType<Editor["chain"]>
updateAttributes: (
type: string,
attrs: Record<string, unknown>
) => ReturnType<Editor["chain"]>
setTextAlign: (align: string) => ReturnType<Editor["chain"]>
}
if (nodeType === "heading" && def.level) {
chain.setHeading({ level: def.level })
} else {
chain.setParagraph()
}
chain.updateAttributes(nodeType, {
styleId: def.id,
})
if (def.textAlign) chain.setTextAlign(def.textAlign)
chain.run()
forEachSelectedBlock(editor, (blockFrom, blockTo) => {
applyRunMarksToRange(editor, blockFrom, blockTo, def)
})
if (editor.state.selection.empty) {
const { $from } = editor.state.selection
if ($from.parent.type.name === "paragraph" || $from.parent.type.name === "heading") {
applyRunMarksToRange(editor, $from.start(), $from.end(), def)
}
}
}
export function applyDocsParagraphStyleById(editor: Editor, styleId: string) {
const catalog = getActiveParagraphStyleCatalog()
const def = resolveParagraphStyleDefinition(catalog, styleId)
if (!def) return false
applyParagraphStyleDefinition(editor, def)
return true
}
export function extractParagraphStyleFromSelection(
editor: Editor,
styleId: string
): DocParagraphStyleDefinition | null {
const catalog = getActiveParagraphStyleCatalog()
const existing =
resolveParagraphStyleDefinition(catalog, styleId) ??
resolveParagraphStyleDefinition(catalog, "normal")
if (!existing) return null
const { $from } = editor.state.selection
const node = $from.parent
if (node.type.name !== "paragraph" && node.type.name !== "heading") return null
const attrs = node.attrs as { styleId?: string; textAlign?: string }
const marks = editor.state.storedMarks ?? $from.marks()
const textStyle = marks.find((m) => m.type.name === "textStyle")
const fontFamily = (textStyle?.attrs.fontFamily as string | undefined) ?? existing.fontFamily
const fontSizeRaw = textStyle?.attrs.fontSize as string | undefined
const fontSizePx = fontSizeRaw ? Number.parseFloat(fontSizeRaw) : existing.fontSizePx
return {
...existing,
id: normalizeParagraphStyleId(styleId),
name: existing.name,
scope: existing.scope,
blockType: node.type.name === "heading" ? "heading" : "paragraph",
level: node.type.name === "heading" ? (node.attrs.level as number) : undefined,
fontFamily,
fontSizePx: Number.isFinite(fontSizePx) ? fontSizePx : existing.fontSizePx,
bold: marks.some((m) => m.type.name === "bold"),
italic: marks.some((m) => m.type.name === "italic"),
underline: marks.some((m) => m.type.name === "underline"),
color: (marks.find((m) => m.type.name === "textStyle")?.attrs.color as string) ?? existing.color,
textAlign:
(attrs.textAlign as DocParagraphStyleDefinition["textAlign"]) ?? existing.textAlign,
}
}
export function readActiveParagraphStyleId(editor: Editor): string {
const headingAttrs = editor.getAttributes("heading")
if (headingAttrs.styleId) return normalizeParagraphStyleId(String(headingAttrs.styleId))
const paragraphAttrs = editor.getAttributes("paragraph")
if (paragraphAttrs.styleId) return normalizeParagraphStyleId(String(paragraphAttrs.styleId))
return inferParagraphStyleIdFromEditorState({
isHeading: editor.isActive("heading"),
headingLevel: headingAttrs.level as number | undefined,
})
}