import { TextSelection, AllSelection, EditorState, Transaction } from "@tiptap/pm/state"; import { Node as ProsemirrorNode, NodeType } from "@tiptap/pm/model"; import type { Command } from "@tiptap/core"; export const LINE_HEIGHT_100 = 1.7; export const DEFAULT_LINE_HEIGHT = "100%"; export const ALLOWED_NODE_TYPES = ["paragraph", "heading", "list_item", "todo_item"]; const NUMBER_VALUE_PATTERN = /^\d+(.\d+)?$/; export function isLineHeightActive(state: EditorState, lineHeight: string): boolean { const { selection, doc } = state; const { from, to } = selection; let keepLooking = true; let active = false; doc.nodesBetween(from, to, (node) => { const nodeType = node.type; const lineHeightValue = node.attrs.lineHeight || DEFAULT_LINE_HEIGHT; if (ALLOWED_NODE_TYPES.includes(nodeType.name)) { if (keepLooking && lineHeight === lineHeightValue) { keepLooking = false; active = true; return false; } return nodeType.name !== "list_item" && nodeType.name !== "todo_item"; } return keepLooking; }); return active; } export function transformLineHeightToCSS(value: string | number): string { if (!value) return ""; let strValue = String(value); if (NUMBER_VALUE_PATTERN.test(strValue)) { const numValue = parseFloat(strValue); strValue = String(Math.round(numValue * 100)) + "%"; } return parseFloat(strValue) * LINE_HEIGHT_100 + "%"; } export function transformCSStoLineHeight(value: string): string { if (!value) return ""; if (value === DEFAULT_LINE_HEIGHT) return ""; let strValue = value; if (NUMBER_VALUE_PATTERN.test(value)) { const numValue = parseFloat(value); strValue = String(Math.round(numValue * 100)) + "%"; if (strValue === DEFAULT_LINE_HEIGHT) return ""; } return parseFloat(strValue) / LINE_HEIGHT_100 + "%"; } interface SetLineHeightTask { node: ProsemirrorNode; nodeType: NodeType; pos: number; } export function setTextLineHeight(tr: Transaction, lineHeight: string | null): Transaction { const { selection, doc } = tr; if (!selection || !doc) return tr; if (!(selection instanceof TextSelection || selection instanceof AllSelection)) { return tr; } const { from, to } = selection; const tasks: Array = []; const lineHeightValue = lineHeight && lineHeight !== DEFAULT_LINE_HEIGHT ? lineHeight : null; doc.nodesBetween(from, to, (node, pos) => { const nodeType = node.type; if (ALLOWED_NODE_TYPES.includes(nodeType.name)) { const lineHeight = node.attrs.lineHeight || null; if (lineHeight !== lineHeightValue) { tasks.push({ node, pos, nodeType, }); } return nodeType.name !== "list_item" && nodeType.name !== "todo_item"; } return true; }); if (!tasks.length) return tr; tasks.forEach((task) => { const { node, pos, nodeType } = task; let { attrs } = node; attrs = { ...attrs, lineHeight: lineHeightValue, }; tr = tr.setNodeMarkup(pos, nodeType, attrs, node.marks); }); return tr; } export function createLineHeightCommand(lineHeight: string): Command { return ({ state, dispatch }) => { const { selection } = state; let { tr } = state; tr = tr.setSelection(selection); tr = setTextLineHeight(tr, lineHeight); if (tr.docChanged) { dispatch && dispatch(tr); return true; } return false; }; }