@docs.plus/extension-indent
Version:
A Tiptap extension for managing text indentation in documents
197 lines (164 loc) • 5.44 kB
text/typescript
import { Extension } from '@tiptap/core'
import { TextSelection } from '@tiptap/pm/state'
/**
* Configuration options for the Indent extension
*/
export interface IndentOptions {
/**
* Character(s) to insert for each indentation
* @default ' ' (2 spaces)
*/
indentChars: string
/**
* Whether the extension is enabled
* @default true
*/
enabled: boolean
/**
* List of node types that can accept indentation
* If not provided or empty, all nodes will be indented
* @default ['paragraph', 'listItem', 'orderedList']
*/
allowedNodeTypes?: string[]
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
indent: {
/**
* Add indentation at cursor position or to selected content
*/
indent: () => ReturnType
/**
* Remove indentation at cursor position or from selected content
*/
outdent: () => ReturnType
}
}
}
/**
* Indent Extension for Tiptap
*
* Provides functionality to indent and outdent text in the editor.
*/
export const Indent = Extension.create<IndentOptions>({
name: 'indent',
addOptions() {
return {
indentChars: ' ',
enabled: true,
allowedNodeTypes: ['paragraph', 'listItem', 'orderedList']
}
},
addCommands() {
/**
* Checks if the current node type is allowed for indentation
* @param state - Editor state
* @returns boolean indicating if indentation is allowed
*/
const isNodeAllowed = (state: any) => {
const { allowedNodeTypes } = this.options
if (!allowedNodeTypes?.length) return true
const $pos = state.selection.$from
const node = $pos.node()
return allowedNodeTypes.includes(node.type.name)
}
return {
/**
* Adds indentation at cursor position or to selected content
*/
indent:
() =>
({ tr, state, dispatch }) => {
if (!this.options.enabled || !isNodeAllowed(state)) return false
const { selection } = state
const { indentChars } = this.options
// Handle empty selection (cursor position)
if (selection.empty) {
if (dispatch) {
tr.insertText(indentChars, selection.from, selection.to)
dispatch(tr)
}
return true
}
// Handle multiline selection
const lines = state.doc.textBetween(selection.from, selection.to).split('\n')
let pos = selection.from
if (dispatch) {
for (const line of lines) {
const lineStart = pos
const lineEnd = lineStart + line.length
tr.insertText(indentChars, lineStart, lineStart)
pos = lineEnd + indentChars.length + 1 // +1 for newline
}
dispatch(tr)
}
return true
},
/**
* Removes indentation at cursor position or from selected content
*/
outdent:
() =>
({ tr, state, dispatch }) => {
if (!this.options.enabled || !isNodeAllowed(state)) return false
const { selection } = state
const { indentChars } = this.options
let madeChanges = false
// Handle empty selection (cursor position)
if (selection.empty) {
const $cursor = (selection as TextSelection).$cursor
if (!$cursor) return false
const cursorPos = $cursor.pos
const lineStart = state.doc.resolve(cursorPos).start()
const textBeforeCursor = state.doc.textBetween(lineStart, cursorPos)
const isAtLineStart = cursorPos === lineStart
// Check if there are indentation characters to remove
if (textBeforeCursor.endsWith(indentChars)) {
if (dispatch) {
tr.delete(cursorPos - indentChars.length, cursorPos)
madeChanges = true
}
} else if (isAtLineStart && textBeforeCursor.startsWith(indentChars)) {
if (dispatch) {
tr.delete(lineStart, lineStart + indentChars.length)
madeChanges = true
}
}
if (dispatch) {
if (!madeChanges) {
tr.setSelection(TextSelection.create(tr.doc, cursorPos))
}
dispatch(tr)
}
return true
}
// Handle multiline selection
const lines = state.doc.textBetween(selection.from, selection.to).split('\n')
let pos = selection.from
if (dispatch) {
for (const line of lines) {
const lineStart = pos
if (line.startsWith(indentChars)) {
tr.delete(lineStart, lineStart + indentChars.length)
pos = lineStart + line.length - indentChars.length + 1
} else {
pos = lineStart + line.length + 1
}
}
dispatch(tr)
}
return true
}
}
},
/**
* Add keyboard shortcuts for indent/outdent functionality
*/
addKeyboardShortcuts() {
return {
Tab: () => (this.options.enabled ? this.editor.commands.indent() : false),
'Shift-Tab': () => (this.options.enabled ? this.editor.commands.outdent() : false)
}
}
})
export const indentCSS = null // No CSS needed for this approach