mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-01-05 09:37:48 +08:00
129 lines
2.9 KiB
TypeScript
129 lines
2.9 KiB
TypeScript
import { canSplit } from 'prosemirror-transform'
|
|
import { ContentMatch, Fragment } from 'prosemirror-model'
|
|
import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'
|
|
import { Command, Commands } from '../types'
|
|
import getSplittedAttributes from '../helpers/getSplittedAttributes'
|
|
|
|
function defaultBlockAt(match: ContentMatch) {
|
|
for (let i = 0; i < match.edgeCount; i + 1) {
|
|
const { type } = match.edge(i)
|
|
|
|
if (type.isTextblock && !type.hasRequiredAttrs()) {
|
|
return type
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
function ensureMarks(state: EditorState) {
|
|
const marks = state.storedMarks
|
|
|| (state.selection.$to.parentOffset && state.selection.$from.marks())
|
|
|
|
if (marks) {
|
|
state.tr.ensureMarks(marks)
|
|
}
|
|
}
|
|
|
|
declare module '@tiptap/core' {
|
|
interface AllCommands {
|
|
splitBlock: {
|
|
/**
|
|
* Forks a new node from an existing node.
|
|
*/
|
|
splitBlock: (options?: { keepMarks?: boolean }) => Command,
|
|
}
|
|
}
|
|
}
|
|
|
|
export const splitBlock: Commands['splitBlock'] = ({ keepMarks = true } = {}) => ({
|
|
tr,
|
|
state,
|
|
dispatch,
|
|
editor,
|
|
}) => {
|
|
const { selection, doc } = tr
|
|
const { $from, $to } = selection
|
|
const extensionAttributes = editor.extensionManager.attributes
|
|
const newAttributes = getSplittedAttributes(
|
|
extensionAttributes,
|
|
$from.node().type.name,
|
|
$from.node().attrs,
|
|
)
|
|
|
|
if (selection instanceof NodeSelection && selection.node.isBlock) {
|
|
if (!$from.parentOffset || !canSplit(doc, $from.pos)) {
|
|
return false
|
|
}
|
|
|
|
if (dispatch) {
|
|
if (keepMarks) {
|
|
ensureMarks(state)
|
|
}
|
|
|
|
tr.split($from.pos).scrollIntoView()
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
if (!$from.parent.isBlock) {
|
|
return false
|
|
}
|
|
|
|
if (dispatch) {
|
|
const atEnd = $to.parentOffset === $to.parent.content.size
|
|
|
|
if (selection instanceof TextSelection) {
|
|
tr.deleteSelection()
|
|
}
|
|
|
|
const deflt = $from.depth === 0
|
|
? undefined
|
|
: defaultBlockAt($from.node(-1).contentMatchAt($from.indexAfter(-1)))
|
|
|
|
let types = atEnd && deflt
|
|
? [{
|
|
type: deflt,
|
|
attrs: newAttributes,
|
|
}]
|
|
: undefined
|
|
|
|
let can = canSplit(tr.doc, tr.mapping.map($from.pos), 1, types)
|
|
|
|
if (
|
|
!types
|
|
&& !can
|
|
&& canSplit(tr.doc, tr.mapping.map($from.pos), 1, deflt ? [{ type: deflt }] : undefined)
|
|
) {
|
|
can = true
|
|
types = deflt
|
|
? [{
|
|
type: deflt,
|
|
attrs: newAttributes,
|
|
}]
|
|
: undefined
|
|
}
|
|
|
|
if (can) {
|
|
tr.split(tr.mapping.map($from.pos), 1, types)
|
|
|
|
if (
|
|
!atEnd
|
|
&& !$from.parentOffset
|
|
&& $from.parent.type !== deflt
|
|
&& $from.node(-1).canReplace($from.index(-1), $from.indexAfter(-1), Fragment.from(deflt?.create()))
|
|
) {
|
|
tr.setNodeMarkup(tr.mapping.map($from.before()), deflt || undefined)
|
|
}
|
|
}
|
|
|
|
if (keepMarks) {
|
|
ensureMarks(state)
|
|
}
|
|
|
|
tr.scrollIntoView()
|
|
}
|
|
|
|
return true
|
|
}
|