@tiptap/core
Version:
headless rich text editor
86 lines (75 loc) • 2.99 kB
text/typescript
import { Node as ProseMirrorNode, NodeType } from '@tiptap/pm/model'
import { canJoin, findWrapping } from '@tiptap/pm/transform'
import { Editor } from '../Editor.js'
import { InputRule, InputRuleFinder } from '../InputRule.js'
import { ExtendedRegExpMatchArray } from '../types.js'
import { callOrReturn } from '../utilities/callOrReturn.js'
/**
* Build an input rule for automatically wrapping a textblock when a
* given string is typed. When using a regular expresion you’ll
* probably want the regexp to start with `^`, so that the pattern can
* only occur at the start of a textblock.
*
* `type` is the type of node to wrap in.
*
* By default, if there’s a node with the same type above the newly
* wrapped node, the rule will try to join those
* two nodes. You can pass a join predicate, which takes a regular
* expression match and the node before the wrapped node, and can
* return a boolean to indicate whether a join should happen.
* @see https://tiptap.dev/guide/custom-extensions/#input-rules
*/
export function wrappingInputRule(config: {
find: InputRuleFinder,
type: NodeType,
keepMarks?: boolean,
keepAttributes?: boolean,
editor?: Editor
getAttributes?:
| Record<string, any>
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
| false
| null
,
joinPredicate?: (match: ExtendedRegExpMatchArray, node: ProseMirrorNode) => boolean,
}) {
return new InputRule({
find: config.find,
handler: ({
state, range, match, chain,
}) => {
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
const tr = state.tr.delete(range.from, range.to)
const $start = tr.doc.resolve(range.from)
const blockRange = $start.blockRange()
const wrapping = blockRange && findWrapping(blockRange, config.type, attributes)
if (!wrapping) {
return null
}
tr.wrap(blockRange, wrapping)
if (config.keepMarks && config.editor) {
const { selection, storedMarks } = state
const { splittableMarks } = config.editor.extensionManager
const marks = storedMarks || (selection.$to.parentOffset && selection.$from.marks())
if (marks) {
const filteredMarks = marks.filter(mark => splittableMarks.includes(mark.type.name))
tr.ensureMarks(filteredMarks)
}
}
if (config.keepAttributes) {
/** If the nodeType is `bulletList` or `orderedList` set the `nodeType` as `listItem` */
const nodeType = config.type.name === 'bulletList' || config.type.name === 'orderedList' ? 'listItem' : 'taskList'
chain().updateAttributes(nodeType, attributes).run()
}
const before = tr.doc.resolve(range.from - 1).nodeBefore
if (
before
&& before.type === config.type
&& canJoin(tr.doc, range.from - 1)
&& (!config.joinPredicate || config.joinPredicate(match, before))
) {
tr.join(range.from - 1)
}
},
})
}