From 5fed0f2fc69fc42e7e287c84f6414b8437becb4d Mon Sep 17 00:00:00 2001 From: Dominik <6538827+bdbch@users.noreply.github.com> Date: Mon, 22 Aug 2022 15:23:44 +0200 Subject: [PATCH] feature(core): add exit handling for marks (#2925) * feat(core): add exit handling for marks * docs(core): add information about exitable marks --- docs/api/schema.md | 11 ++++++++ packages/core/src/ExtensionManager.ts | 17 +++++++++--- packages/core/src/Mark.ts | 39 +++++++++++++++++++++++++++ packages/extension-code/src/code.ts | 2 ++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/docs/api/schema.md b/docs/api/schema.md index 8ad52f27..5329338b 100644 --- a/docs/api/schema.md +++ b/docs/api/schema.md @@ -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 Add this mark to a group of extensions, which can be referred to in the content attribute of the schema. diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts index 09f29791..b00cd555 100644 --- a/packages/core/src/ExtensionManager.ts +++ b/packages/core/src/ExtensionManager.ts @@ -3,7 +3,7 @@ import { Node as ProsemirrorNode, Schema } from 'prosemirror-model' import { Plugin } from 'prosemirror-state' import { Decoration, EditorView } from 'prosemirror-view' -import { NodeConfig } from '.' +import { Mark, NodeConfig } from '.' import { Editor } from './Editor' import { getAttributesFromExtensions } from './helpers/getAttributesFromExtensions' import { getExtensionField } from './helpers/getExtensionField' @@ -252,6 +252,13 @@ export class ExtensionManager { context, ) + let defaultBindings: Record boolean> = {} + + // bind exit handling + if (extension.type === 'mark' && extension.config.exitable) { + defaultBindings.ArrowRight = () => Mark.handleExit({ editor, mark: (extension as Mark) }) + } + if (addKeyboardShortcuts) { const bindings = Object.fromEntries( Object @@ -261,11 +268,13 @@ export class ExtensionManager { }), ) - const keyMapPlugin = keymap(bindings) - - plugins.push(keyMapPlugin) + defaultBindings = { ...defaultBindings, ...bindings } } + const keyMapPlugin = keymap(defaultBindings) + + plugins.push(keyMapPlugin) + const addInputRules = getExtensionField( extension, 'addInputRules', diff --git a/packages/core/src/Mark.ts b/packages/core/src/Mark.ts index 468ccde6..cb192f32 100644 --- a/packages/core/src/Mark.ts +++ b/packages/core/src/Mark.ts @@ -304,6 +304,11 @@ declare module '@tiptap/core' { parent: ParentConfig>['excludes'], }) => MarkSpec['excludes']), + /** + * Marks this Mark as exitable + */ + exitable?: boolean | (() => boolean), + /** * Group */ @@ -486,4 +491,38 @@ export class Mark { 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 + } } diff --git a/packages/extension-code/src/code.ts b/packages/extension-code/src/code.ts index a8b78b8e..be1203cf 100644 --- a/packages/extension-code/src/code.ts +++ b/packages/extension-code/src/code.ts @@ -44,6 +44,8 @@ export const Code = Mark.create({ code: true, + exitable: true, + parseHTML() { return [ { tag: 'code' },