feature(core): add exit handling for marks (#2925)
* feat(core): add exit handling for marks * docs(core): add information about exitable marks
This commit is contained in:
@@ -272,6 +272,17 @@ Mark.create({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Exitable
|
||||||
|
By default a mark will "trap" the cursor meaning the cursor can't get out of the mark except by moving the cursor left to right into text without a mark.
|
||||||
|
If this is set to true, the mark will be exitable when the mark is at the end of a node. This is handy for example code marks.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Mark.create({
|
||||||
|
// make this mark exitable - default is false
|
||||||
|
exitable: true,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
#### Group
|
#### Group
|
||||||
Add this mark to a group of extensions, which can be referred to in the content attribute of the schema.
|
Add this mark to a group of extensions, which can be referred to in the content attribute of the schema.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Node as ProsemirrorNode, Schema } from 'prosemirror-model'
|
|||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import { Decoration, EditorView } from 'prosemirror-view'
|
import { Decoration, EditorView } from 'prosemirror-view'
|
||||||
|
|
||||||
import { NodeConfig } from '.'
|
import { Mark, NodeConfig } from '.'
|
||||||
import { Editor } from './Editor'
|
import { Editor } from './Editor'
|
||||||
import { getAttributesFromExtensions } from './helpers/getAttributesFromExtensions'
|
import { getAttributesFromExtensions } from './helpers/getAttributesFromExtensions'
|
||||||
import { getExtensionField } from './helpers/getExtensionField'
|
import { getExtensionField } from './helpers/getExtensionField'
|
||||||
@@ -252,6 +252,13 @@ export class ExtensionManager {
|
|||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let defaultBindings: Record<string, () => boolean> = {}
|
||||||
|
|
||||||
|
// bind exit handling
|
||||||
|
if (extension.type === 'mark' && extension.config.exitable) {
|
||||||
|
defaultBindings.ArrowRight = () => Mark.handleExit({ editor, mark: (extension as Mark) })
|
||||||
|
}
|
||||||
|
|
||||||
if (addKeyboardShortcuts) {
|
if (addKeyboardShortcuts) {
|
||||||
const bindings = Object.fromEntries(
|
const bindings = Object.fromEntries(
|
||||||
Object
|
Object
|
||||||
@@ -261,11 +268,13 @@ export class ExtensionManager {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const keyMapPlugin = keymap(bindings)
|
defaultBindings = { ...defaultBindings, ...bindings }
|
||||||
|
|
||||||
plugins.push(keyMapPlugin)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyMapPlugin = keymap(defaultBindings)
|
||||||
|
|
||||||
|
plugins.push(keyMapPlugin)
|
||||||
|
|
||||||
const addInputRules = getExtensionField<AnyConfig['addInputRules']>(
|
const addInputRules = getExtensionField<AnyConfig['addInputRules']>(
|
||||||
extension,
|
extension,
|
||||||
'addInputRules',
|
'addInputRules',
|
||||||
|
|||||||
@@ -304,6 +304,11 @@ declare module '@tiptap/core' {
|
|||||||
parent: ParentConfig<MarkConfig<Options, Storage>>['excludes'],
|
parent: ParentConfig<MarkConfig<Options, Storage>>['excludes'],
|
||||||
}) => MarkSpec['excludes']),
|
}) => MarkSpec['excludes']),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks this Mark as exitable
|
||||||
|
*/
|
||||||
|
exitable?: boolean | (() => boolean),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group
|
* Group
|
||||||
*/
|
*/
|
||||||
@@ -486,4 +491,38 @@ export class Mark<Options = any, Storage = any> {
|
|||||||
|
|
||||||
return extension
|
return extension
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static handleExit({
|
||||||
|
editor,
|
||||||
|
mark,
|
||||||
|
}: {
|
||||||
|
editor: Editor
|
||||||
|
mark: Mark
|
||||||
|
}) {
|
||||||
|
const { tr } = editor.state
|
||||||
|
const currentPos = editor.state.selection.$from
|
||||||
|
const isAtEnd = currentPos.pos === currentPos.end()
|
||||||
|
|
||||||
|
if (isAtEnd) {
|
||||||
|
const currentMarks = currentPos.marks()
|
||||||
|
const isInMark = !!currentMarks.find(m => m?.type.name === mark.name)
|
||||||
|
|
||||||
|
if (!isInMark) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMark = currentMarks.find(m => m?.type.name === mark.name)
|
||||||
|
|
||||||
|
if (removeMark) {
|
||||||
|
tr.removeStoredMark(removeMark)
|
||||||
|
}
|
||||||
|
tr.insertText(' ', currentPos.pos)
|
||||||
|
|
||||||
|
editor.view.dispatch(tr)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export const Code = Mark.create<CodeOptions>({
|
|||||||
|
|
||||||
code: true,
|
code: true,
|
||||||
|
|
||||||
|
exitable: true,
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{ tag: 'code' },
|
{ tag: 'code' },
|
||||||
|
|||||||
Reference in New Issue
Block a user