Skip to content

Commit

Permalink
feature(core): add exit handling for marks (#2925)
Browse files Browse the repository at this point in the history
* feat(core): add exit handling for marks

* docs(core): add information about exitable marks
  • Loading branch information
bdbch committed Aug 22, 2022
1 parent f558417 commit 5fed0f2
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 4 deletions.
11 changes: 11 additions & 0 deletions docs/api/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
17 changes: 13 additions & 4 deletions packages/core/src/ExtensionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -252,6 +252,13 @@ export class ExtensionManager {
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) {
const bindings = Object.fromEntries(
Object
Expand All @@ -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<AnyConfig['addInputRules']>(
extension,
'addInputRules',
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/Mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ declare module '@tiptap/core' {
parent: ParentConfig<MarkConfig<Options, Storage>>['excludes'],
}) => MarkSpec['excludes']),

/**
* Marks this Mark as exitable
*/
exitable?: boolean | (() => boolean),

/**
* Group
*/
Expand Down Expand Up @@ -486,4 +491,38 @@ export class Mark<Options = any, Storage = any> {

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
}
}
2 changes: 2 additions & 0 deletions packages/extension-code/src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export const Code = Mark.create<CodeOptions>({

code: true,

exitable: true,

parseHTML() {
return [
{ tag: 'code' },
Expand Down

0 comments on commit 5fed0f2

Please sign in to comment.