import { Extension } from '@tiptap/core' export const INDENT_MAX = 4 export const INDENT_TYPES = ['paragraph', 'heading'] declare module '@tiptap/core' { interface Commands { articleIndent: { indent: () => ReturnType outdent: () => ReturnType } } } function currentIndentableNode(state: { selection: { $from: { parent: unknown } } }) { return state.selection.$from.parent as { type: { name: string } attrs: { indent?: number } } } export const ArticleIndent = Extension.create({ name: 'articleIndent', addGlobalAttributes() { return [ { types: INDENT_TYPES, attributes: { indent: { default: 0, parseHTML: (element) => Number(element.getAttribute('data-indent')) || 0, renderHTML: (attributes) => { if (!attributes.indent) { return {} } return { 'data-indent': String(attributes.indent) } }, }, }, }, ] }, addCommands() { return { indent: () => ({ state, commands, tr, dispatch }) => { if (commands.sinkListItem('listItem')) { return true } const node = currentIndentableNode(state) if (!INDENT_TYPES.includes(node.type.name)) { return false } const level = Math.min(INDENT_MAX, (node.attrs.indent ?? 0) + 1) if (dispatch) { const { $from } = state.selection tr.setNodeAttribute($from.before($from.depth), 'indent', level) dispatch(tr) } return true }, outdent: () => ({ state, commands, tr, dispatch }) => { if (commands.liftListItem('listItem')) { return true } const node = currentIndentableNode(state) if (!INDENT_TYPES.includes(node.type.name)) { return false } const level = Math.max(0, (node.attrs.indent ?? 0) - 1) if (dispatch) { const { $from } = state.selection tr.setNodeAttribute($from.before($from.depth), 'indent', level) dispatch(tr) } return true }, } }, })