diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 115bee70..a28e89f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Load cached dependencies - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 id: cache with: path: | @@ -144,7 +144,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Load cached dependencies - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 id: cache with: path: | diff --git a/README.md b/README.md index 7283d7a6..11130e89 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ To check out some live examples, visit [next.tiptap.dev](https://next.tiptap.dev Please see [CONTRIBUTING](CONTRIBUTING.md) for details. ## Maintainers -- [Philipp Kühn](https://github.com/philippkuehn) (development) -- [Hans Pagel](https://github.com/hanspagel) (documentation) +- [Philipp Kühn](https://github.com/philippkuehn) (developer) +- [Hans Pagel](https://github.com/hanspagel) (maintainer) ## Premium Sponsors - [überdosis](https://ueberdosis.io/) diff --git a/docs/src/docPages/api/commands.md b/docs/src/docPages/api/commands.md index 439c8aaa..5b01d12f 100644 --- a/docs/src/docPages/api/commands.md +++ b/docs/src/docPages/api/commands.md @@ -177,7 +177,9 @@ Have a look at all of the core commands listed below. They should give you a goo | .blur() | Removes focus from the editor. | | .deleteRange() | Delete a given range. | | .deleteSelection() | Delete the selection, if there is one. | +| .enter() | Trigger enter. | | .focus() | Focus the editor at the given position. | +| .keyboardShortcut() | Trigger a keyboard shortcut. | | .scrollIntoView() | Scroll the selection into view. | | .selectAll() | Select the whole document. | | .selectNodeBackward() | Select a node backward. | diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts index a457e564..c4300950 100644 --- a/packages/core/src/Editor.ts +++ b/packages/core/src/Editor.ts @@ -307,12 +307,40 @@ export class Editor extends EventEmitter { return this.createDocument('') } + public isCapturingTransaction = false + + private capturedTransaction: Transaction | null = null + + public captureTransaction(fn: Function) { + this.isCapturingTransaction = true + fn() + this.isCapturingTransaction = false + + const tr = this.capturedTransaction + + this.capturedTransaction = null + + return tr + } + /** * The callback over which to send transactions (state updates) produced by the view. * * @param transaction An editor state transaction */ private dispatchTransaction(transaction: Transaction): void { + if (this.isCapturingTransaction) { + if (!this.capturedTransaction) { + this.capturedTransaction = transaction + + return + } + + transaction.steps.forEach(step => this.capturedTransaction?.step(step)) + + return + } + const state = this.state.apply(transaction) const selectionHasChanged = !this.state.selection.eq(state.selection) diff --git a/packages/core/src/commands/enter.ts b/packages/core/src/commands/enter.ts new file mode 100644 index 00000000..60d52642 --- /dev/null +++ b/packages/core/src/commands/enter.ts @@ -0,0 +1,8 @@ +import { Command } from '../types' + +/** + * Trigger enter. + */ +export const enter = (): Command => ({ commands }) => { + return commands.keyboardShortcut('Enter') +} diff --git a/packages/core/src/commands/keyboardShortcut.ts b/packages/core/src/commands/keyboardShortcut.ts new file mode 100644 index 00000000..31db9eed --- /dev/null +++ b/packages/core/src/commands/keyboardShortcut.ts @@ -0,0 +1,95 @@ +import { Command } from '../types' + +const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false + +function normalizeKeyName(name: string) { + const parts = name.split(/-(?!$)/) + let result = parts[parts.length - 1] + + if (result === 'Space') { + result = ' ' + } + + let alt + let ctrl + let shift + let meta + + for (let i = 0; i < parts.length - 1; i += 1) { + const mod = parts[i] + + if (/^(cmd|meta|m)$/i.test(mod)) { + meta = true + } else if (/^a(lt)?$/i.test(mod)) { + alt = true + } else if (/^(c|ctrl|control)$/i.test(mod)) { + ctrl = true + } else if (/^s(hift)?$/i.test(mod)) { + shift = true + } else if (/^mod$/i.test(mod)) { + if (mac) { + meta = true + } else { + ctrl = true + } + } else { + throw new Error(`Unrecognized modifier name: ${mod}`) + } + } + + if (alt) { + result = `Alt-${result}` + } + + if (ctrl) { + result = `Ctrl-${result}` + } + + if (meta) { + result = `Meta-${result}` + } + + if (shift) { + result = `Shift-${result}` + } + + return result +} + +/** + * Trigger a keyboard shortcut. + */ +export const keyboardShortcut = (name: string): Command => ({ + editor, + view, + tr, + dispatch, +}) => { + const keys = normalizeKeyName(name).split(/-(?!$)/) + const key = keys.find(item => !['Alt', 'Ctrl', 'Meta', 'Shift'].includes(item)) + const event = new KeyboardEvent('keydown', { + key: key === 'Space' + ? ' ' + : key, + altKey: keys.includes('Alt'), + ctrlKey: keys.includes('Ctrl'), + metaKey: keys.includes('Meta'), + shiftKey: keys.includes('Shift'), + bubbles: true, + cancelable: true, + }) + + const capturedTransaction = editor.captureTransaction(() => { + view.someProp('handleKeyDown', f => f(view, event)) + }) + + capturedTransaction?.steps.forEach(step => { + const newStep = step.map(tr.mapping) + + if (newStep && dispatch) { + tr.maybeStep(newStep) + } + }) + + return true +} diff --git a/packages/core/src/extensions/commands.ts b/packages/core/src/extensions/commands.ts index 2c267821..cdab4283 100644 --- a/packages/core/src/extensions/commands.ts +++ b/packages/core/src/extensions/commands.ts @@ -6,6 +6,7 @@ import * as command from '../commands/command' import * as createParagraphNear from '../commands/createParagraphNear' import * as deleteRange from '../commands/deleteRange' import * as deleteSelection from '../commands/deleteSelection' +import * as enter from '../commands/enter' import * as exitCode from '../commands/exitCode' import * as extendMarkRange from '../commands/extendMarkRange' import * as first from '../commands/first' @@ -14,6 +15,7 @@ import * as insertHTML from '../commands/insertHTML' import * as insertText from '../commands/insertText' import * as joinBackward from '../commands/joinBackward' import * as joinForward from '../commands/joinForward' +import * as keyboardShortcut from '../commands/keyboardShortcut' import * as lift from '../commands/lift' import * as liftEmptyBlock from '../commands/liftEmptyBlock' import * as liftListItem from '../commands/liftListItem' @@ -55,6 +57,7 @@ export const Commands = Extension.create({ ...createParagraphNear, ...deleteRange, ...deleteSelection, + ...enter, ...exitCode, ...extendMarkRange, ...first, @@ -63,6 +66,7 @@ export const Commands = Extension.create({ ...insertText, ...joinBackward, ...joinForward, + ...keyboardShortcut, ...lift, ...liftEmptyBlock, ...liftListItem,