228 lines
8.4 KiB
TypeScript
228 lines
8.4 KiB
TypeScript
import type { Editor } from "@tiptap/react"
|
|
import { getDocsBlockTargets } from "@/lib/drive/docs-block-targets"
|
|
import { getActiveParagraphStyleCatalog } from "@/lib/drive/docs-paragraph-style-bridge"
|
|
import {
|
|
resolveParagraphStyleDefinition,
|
|
} from "@/lib/drive/docs-paragraph-styles"
|
|
import { readActiveParagraphStyleId } from "@/lib/drive/docs-paragraph-style-apply"
|
|
import {
|
|
DOCS_DEFAULT_LINE_HEIGHT,
|
|
DOCS_DEFAULT_PARAGRAPH_SPACING,
|
|
DOCS_QUICK_PARAGRAPH_SPACE_PT,
|
|
type DocsParagraphSpacingAttrs,
|
|
lineHeightPresetId,
|
|
readParagraphSpacingAttrs,
|
|
} from "@/lib/drive/docs-line-spacing"
|
|
|
|
export type DocsParagraphSpacingState = {
|
|
lineHeight: number | null
|
|
lineHeightPresetId: ReturnType<typeof lineHeightPresetId> | "mixed"
|
|
spaceBeforePt: number | "mixed"
|
|
spaceAfterPt: number | "mixed"
|
|
hasSpaceBefore: boolean | "mixed"
|
|
hasSpaceAfter: boolean | "mixed"
|
|
keepWithNext: boolean | "mixed"
|
|
keepLinesTogether: boolean | "mixed"
|
|
preventWidowOrphan: boolean | "mixed"
|
|
pageBreakBefore: boolean | "mixed"
|
|
}
|
|
|
|
function mergeMixed<T>(values: T[]): T | "mixed" {
|
|
if (values.length === 0) return values[0] as T
|
|
const first = values[0]
|
|
return values.every((value) => value === first) ? first : "mixed"
|
|
}
|
|
|
|
function effectiveLineHeight(editor: Editor, attrs: DocsParagraphSpacingAttrs, styleId: string | null): number {
|
|
if (attrs.lineHeight != null) return attrs.lineHeight
|
|
if (styleId) {
|
|
const catalog = getActiveParagraphStyleCatalog()
|
|
const def = resolveParagraphStyleDefinition(catalog, styleId)
|
|
if (def?.lineHeight) return def.lineHeight
|
|
}
|
|
return DOCS_DEFAULT_LINE_HEIGHT
|
|
}
|
|
|
|
function readBlockSpacingStates(editor: Editor): Array<{
|
|
attrs: DocsParagraphSpacingAttrs
|
|
effectiveLineHeight: number
|
|
}> {
|
|
const { state } = editor
|
|
const targets = getDocsBlockTargets(state.doc, state.selection.from, state.selection.to)
|
|
return targets.map(({ node }) => {
|
|
const attrs = readParagraphSpacingAttrs(node)
|
|
const styleId = (node.attrs.styleId as string | null | undefined) ?? null
|
|
return {
|
|
attrs,
|
|
effectiveLineHeight: effectiveLineHeight(editor, attrs, styleId),
|
|
}
|
|
})
|
|
}
|
|
|
|
export function readDocsParagraphSpacingState(editor: Editor | null): DocsParagraphSpacingState {
|
|
if (!editor || editor.isDestroyed) {
|
|
return {
|
|
lineHeight: DOCS_DEFAULT_LINE_HEIGHT,
|
|
lineHeightPresetId: "1.15",
|
|
spaceBeforePt: 0,
|
|
spaceAfterPt: 0,
|
|
hasSpaceBefore: false,
|
|
hasSpaceAfter: false,
|
|
keepWithNext: false,
|
|
keepLinesTogether: false,
|
|
preventWidowOrphan: false,
|
|
pageBreakBefore: false,
|
|
}
|
|
}
|
|
|
|
const blocks = readBlockSpacingStates(editor)
|
|
if (blocks.length === 0) {
|
|
const styleId = readActiveParagraphStyleId(editor)
|
|
const catalog = getActiveParagraphStyleCatalog()
|
|
const def = resolveParagraphStyleDefinition(catalog, styleId)
|
|
const lineHeight = def?.lineHeight ?? DOCS_DEFAULT_LINE_HEIGHT
|
|
return {
|
|
lineHeight,
|
|
lineHeightPresetId: lineHeightPresetId(lineHeight),
|
|
spaceBeforePt: 0,
|
|
spaceAfterPt: 0,
|
|
hasSpaceBefore: false,
|
|
hasSpaceAfter: false,
|
|
keepWithNext: false,
|
|
keepLinesTogether: false,
|
|
preventWidowOrphan: false,
|
|
pageBreakBefore: false,
|
|
}
|
|
}
|
|
|
|
const effectiveHeights = blocks.map((block) => block.effectiveLineHeight)
|
|
const mergedHeight = mergeMixed(effectiveHeights)
|
|
const presetIds = effectiveHeights.map((value) => lineHeightPresetId(value))
|
|
const mergedPreset = mergeMixed(presetIds)
|
|
|
|
const spaceBefore = blocks.map((block) => block.attrs.spaceBeforePt)
|
|
const spaceAfter = blocks.map((block) => block.attrs.spaceAfterPt)
|
|
|
|
return {
|
|
lineHeight: mergedHeight === "mixed" ? null : mergedHeight,
|
|
lineHeightPresetId: mergedPreset,
|
|
spaceBeforePt: mergeMixed(spaceBefore),
|
|
spaceAfterPt: mergeMixed(spaceAfter),
|
|
hasSpaceBefore: mergeMixed(spaceBefore.map((value) => value >= DOCS_QUICK_PARAGRAPH_SPACE_PT)),
|
|
hasSpaceAfter: mergeMixed(spaceAfter.map((value) => value >= DOCS_QUICK_PARAGRAPH_SPACE_PT)),
|
|
keepWithNext: mergeMixed(blocks.map((block) => block.attrs.keepWithNext)),
|
|
keepLinesTogether: mergeMixed(blocks.map((block) => block.attrs.keepLinesTogether)),
|
|
preventWidowOrphan: mergeMixed(blocks.map((block) => block.attrs.preventWidowOrphan)),
|
|
pageBreakBefore: mergeMixed(blocks.map((block) => block.attrs.pageBreakBefore)),
|
|
}
|
|
}
|
|
|
|
function applyBlockSpacingPatch(
|
|
editor: Editor,
|
|
patch: Partial<DocsParagraphSpacingAttrs>
|
|
): boolean {
|
|
const { state } = editor
|
|
const targets = getDocsBlockTargets(state.doc, state.selection.from, state.selection.to)
|
|
if (targets.length === 0) return false
|
|
|
|
let tr = state.tr
|
|
let changed = false
|
|
|
|
for (const { pos, node } of targets) {
|
|
const current = readParagraphSpacingAttrs(node)
|
|
const next = { ...current, ...patch }
|
|
const same =
|
|
current.lineHeight === next.lineHeight &&
|
|
current.spaceBeforePt === next.spaceBeforePt &&
|
|
current.spaceAfterPt === next.spaceAfterPt &&
|
|
current.keepWithNext === next.keepWithNext &&
|
|
current.keepLinesTogether === next.keepLinesTogether &&
|
|
current.preventWidowOrphan === next.preventWidowOrphan &&
|
|
current.pageBreakBefore === next.pageBreakBefore
|
|
if (same) continue
|
|
tr = tr.setNodeMarkup(pos, undefined, { ...node.attrs, ...next })
|
|
changed = true
|
|
}
|
|
|
|
if (!changed) return false
|
|
editor.view.dispatch(tr.scrollIntoView())
|
|
return true
|
|
}
|
|
|
|
export function setDocsLineHeight(editor: Editor | null, lineHeight: number): boolean {
|
|
if (!editor || editor.isDestroyed) return false
|
|
return applyBlockSpacingPatch(editor, { lineHeight })
|
|
}
|
|
|
|
export function setDocsCustomSpacing(
|
|
editor: Editor | null,
|
|
input: Pick<DocsParagraphSpacingAttrs, "lineHeight" | "spaceBeforePt" | "spaceAfterPt">
|
|
): boolean {
|
|
if (!editor || editor.isDestroyed) return false
|
|
return applyBlockSpacingPatch(editor, input)
|
|
}
|
|
|
|
export function toggleDocsSpaceBefore(editor: Editor | null): boolean {
|
|
if (!editor || editor.isDestroyed) return false
|
|
const blocks = readBlockSpacingStates(editor)
|
|
if (blocks.length === 0) return false
|
|
const allHaveQuick = blocks.every(
|
|
(block) => block.attrs.spaceBeforePt >= DOCS_QUICK_PARAGRAPH_SPACE_PT
|
|
)
|
|
return applyBlockSpacingPatch(editor, {
|
|
spaceBeforePt: allHaveQuick ? 0 : DOCS_QUICK_PARAGRAPH_SPACE_PT,
|
|
})
|
|
}
|
|
|
|
export function toggleDocsSpaceAfter(editor: Editor | null): boolean {
|
|
if (!editor || editor.isDestroyed) return false
|
|
const blocks = readBlockSpacingStates(editor)
|
|
if (blocks.length === 0) return false
|
|
const allHaveQuick = blocks.every(
|
|
(block) => block.attrs.spaceAfterPt >= DOCS_QUICK_PARAGRAPH_SPACE_PT
|
|
)
|
|
return applyBlockSpacingPatch(editor, {
|
|
spaceAfterPt: allHaveQuick ? 0 : DOCS_QUICK_PARAGRAPH_SPACE_PT,
|
|
})
|
|
}
|
|
|
|
export function toggleDocsKeepWithNext(editor: Editor | null): boolean {
|
|
if (!editor || editor.isDestroyed) return false
|
|
const blocks = readBlockSpacingStates(editor)
|
|
if (blocks.length === 0) return false
|
|
const allOn = blocks.every((block) => block.attrs.keepWithNext)
|
|
return applyBlockSpacingPatch(editor, { keepWithNext: !allOn })
|
|
}
|
|
|
|
export function toggleDocsKeepLinesTogether(editor: Editor | null): boolean {
|
|
if (!editor || editor.isDestroyed) return false
|
|
const blocks = readBlockSpacingStates(editor)
|
|
if (blocks.length === 0) return false
|
|
const allOn = blocks.every((block) => block.attrs.keepLinesTogether)
|
|
return applyBlockSpacingPatch(editor, { keepLinesTogether: !allOn })
|
|
}
|
|
|
|
export function toggleDocsPreventWidowOrphan(editor: Editor | null): boolean {
|
|
if (!editor || editor.isDestroyed) return false
|
|
const blocks = readBlockSpacingStates(editor)
|
|
if (blocks.length === 0) return false
|
|
const allOn = blocks.every((block) => block.attrs.preventWidowOrphan)
|
|
return applyBlockSpacingPatch(editor, { preventWidowOrphan: !allOn })
|
|
}
|
|
|
|
export function toggleDocsPageBreakBefore(editor: Editor | null): boolean {
|
|
if (!editor || editor.isDestroyed) return false
|
|
const blocks = readBlockSpacingStates(editor)
|
|
if (blocks.length === 0) return false
|
|
const allOn = blocks.every((block) => block.attrs.pageBreakBefore)
|
|
return applyBlockSpacingPatch(editor, { pageBreakBefore: !allOn })
|
|
}
|
|
|
|
export function readDocsCustomSpacingDraft(editor: Editor | null): DocsParagraphSpacingAttrs {
|
|
if (!editor || editor.isDestroyed) return { ...DOCS_DEFAULT_PARAGRAPH_SPACING }
|
|
const blocks = readBlockSpacingStates(editor)
|
|
if (blocks.length === 0) return { ...DOCS_DEFAULT_PARAGRAPH_SPACING }
|
|
const first = blocks[0].attrs
|
|
return { ...DOCS_DEFAULT_PARAGRAPH_SPACING, ...first }
|
|
}
|