@tiptap/core
Version:
headless rich text editor
117 lines (99 loc) • 3.61 kB
text/typescript
import { MarkType, ResolvedPos } from '@tiptap/pm/model'
import { EditorState, Transaction } from '@tiptap/pm/state'
import { getMarkAttributes } from '../helpers/getMarkAttributes.js'
import { getMarkType } from '../helpers/getMarkType.js'
import { isTextSelection } from '../helpers/index.js'
import { RawCommands } from '../types.js'
declare module '@tiptap/core' {
interface Commands<ReturnType> {
setMark: {
/**
* Add a mark with new attributes.
* @param typeOrName The mark type or name.
* @example editor.commands.setMark('bold', { level: 1 })
*/
setMark: (typeOrName: string | MarkType, attributes?: Record<string, any>) => ReturnType
}
}
}
function canSetMark(state: EditorState, tr: Transaction, newMarkType: MarkType) {
const { selection } = tr
let cursor: ResolvedPos | null = null
if (isTextSelection(selection)) {
cursor = selection.$cursor
}
if (cursor) {
const currentMarks = state.storedMarks ?? cursor.marks()
// There can be no current marks that exclude the new mark
return (
!!newMarkType.isInSet(currentMarks)
|| !currentMarks.some(mark => mark.type.excludes(newMarkType))
)
}
const { ranges } = selection
return ranges.some(({ $from, $to }) => {
let someNodeSupportsMark = $from.depth === 0
? state.doc.inlineContent && state.doc.type.allowsMarkType(newMarkType)
: false
state.doc.nodesBetween($from.pos, $to.pos, (node, _pos, parent) => {
// If we already found a mark that we can enable, return false to bypass the remaining search
if (someNodeSupportsMark) {
return false
}
if (node.isInline) {
const parentAllowsMarkType = !parent || parent.type.allowsMarkType(newMarkType)
const currentMarksAllowMarkType = !!newMarkType.isInSet(node.marks)
|| !node.marks.some(otherMark => otherMark.type.excludes(newMarkType))
someNodeSupportsMark = parentAllowsMarkType && currentMarksAllowMarkType
}
return !someNodeSupportsMark
})
return someNodeSupportsMark
})
}
export const setMark: RawCommands['setMark'] = (typeOrName, attributes = {}) => ({ tr, state, dispatch }) => {
const { selection } = tr
const { empty, ranges } = selection
const type = getMarkType(typeOrName, state.schema)
if (dispatch) {
if (empty) {
const oldAttributes = getMarkAttributes(state, type)
tr.addStoredMark(
type.create({
...oldAttributes,
...attributes,
}),
)
} else {
ranges.forEach(range => {
const from = range.$from.pos
const to = range.$to.pos
state.doc.nodesBetween(from, to, (node, pos) => {
const trimmedFrom = Math.max(pos, from)
const trimmedTo = Math.min(pos + node.nodeSize, to)
const someHasMark = node.marks.find(mark => mark.type === type)
// if there is already a mark of this type
// we know that we have to merge its attributes
// otherwise we add a fresh new mark
if (someHasMark) {
node.marks.forEach(mark => {
if (type === mark.type) {
tr.addMark(
trimmedFrom,
trimmedTo,
type.create({
...mark.attrs,
...attributes,
}),
)
}
})
} else {
tr.addMark(trimmedFrom, trimmedTo, type.create(attributes))
}
})
})
}
}
return canSetMark(state, tr, type)
}