feat(pm): new prosemirror package for dependency resolving

* chore:(core): migrate to tsup

* chore: migrate blockquote and bold to tsup

* chore: migrated bubble-menu and bullet-list to tsup

* chore: migrated more packages to tsup

* chore: migrate code and character extensions to tsup

* chore: update package.json to simplify build for all packages

* chore: move all packages to tsup as a build process

* chore: change ci build task

* feat(pm): add prosemirror meta package

* rfix: resolve issues with build paths & export mappings

* docs: update documentation to include notes for @tiptap/pm

* chore(pm): update tsconfig

* chore(packages): update packages

* fix(pm): add package export infos & fix dependencies

* chore(general): start moving to pm package as deps

* chore: move to tiptap pm package internally

* fix(demos): fix demos working with new pm package

* fix(tables): fix tables package

* fix(tables): fix tables package

* chore(demos): pinned typescript version

* chore: remove unnecessary tsconfig

* chore: fix netlify build

* fix(demos): fix package resolving for pm packages

* fix(tests): fix package resolving for pm packages

* fix(tests): fix package resolving for pm packages

* chore(tests): fix tests not running correctly after pm package

* chore(pm): add files to files array

* chore: update build workflow

* chore(tests): increase timeout time back to 12s

* chore(docs): update docs

* chore(docs): update installation guides & pm information to docs

* chore(docs): add link to prosemirror docs

* fix(vue-3): add missing build step

* chore(docs): comment out cdn link

* chore(docs): remove semicolons from docs

* chore(docs): remove unnecessary installation note

* chore(docs): remove unnecessary installation note
This commit is contained in:
Dominik 2023-02-02 17:37:33 +01:00 committed by GitHub
parent 0ecb5a8df8
commit f387ad3dd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
271 changed files with 3903 additions and 3124 deletions

View File

@ -23,50 +23,49 @@ jobs:
node-version: [16]
steps:
- uses: actions/checkout@v3.0.2
- uses: actions/checkout@v3.0.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ matrix.node-version }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ matrix.node-version }}
- name: Load cached dependencies
uses: actions/cache@v3.0.11
id: cache
with:
path: |
**/node_modules
/home/runner/.cache/Cypress
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
- name: Load cached dependencies
uses: actions/cache@v3.0.11
id: cache
with:
path: |
**/node_modules
/home/runner/.cache/Cypress
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
id: install-dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm install
- name: Install dependencies
id: install-dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm install
# - name: Fix code style linting errors
# id: lint-fix
# run: npm run lint:fix
# continue-on-error: true
#
# - name: Commit fixed linting errors
# id: commit
# uses: stefanzweifel/git-auto-commit-action@v4
# with:
# commit_message: "ci: fix code style linting errors"
# - name: Fix code style linting errors
# id: lint-fix
# run: npm run lint:fix
# continue-on-error: true
#
# - name: Commit fixed linting errors
# id: commit
# uses: stefanzweifel/git-auto-commit-action@v4
# with:
# commit_message: "ci: fix code style linting errors"
- name: Lint code
id: lint
run: npm run lint
- name: Lint code
id: lint
run: npm run lint
- name: Send Slack notifications
uses: act10ns/slack@v1
if: failure()
with:
status: ${{ job.status }}
steps: ${{ toJson(steps) }}
channel: '#tiptap-notifications'
- name: Send Slack notifications
uses: act10ns/slack@v1
if: failure()
with:
status: ${{ job.status }}
steps: ${{ toJson(steps) }}
channel: '#tiptap-notifications'
test:
runs-on: ubuntu-latest
@ -79,48 +78,55 @@ jobs:
node-version: [16]
steps:
- uses: actions/checkout@v3.0.2
- uses: actions/checkout@v3.0.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ matrix.node-version }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
id: install-dependencies
run: npm install
- name: Run tests with Cypress
id: cypress
uses: cypress-io/github-action@v4.2.0
with:
cache-key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
start: npm run start
wait-on: 'http://localhost:3000'
project: ./tests
browser: chrome
quiet: true
- name: Try to build the packages
id: build-packages
run: npm run build:pm
- name: Export screenshots (on failure only)
uses: actions/upload-artifact@v3.1.0
if: failure()
with:
name: cypress-screenshots
path: tests/cypress/screenshots
retention-days: 7
- name: Run tests with Cypress
id: cypress
uses: cypress-io/github-action@v4.2.0
with:
cache-key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
start: npm run start
wait-on: 'http://localhost:3000'
project: ./tests
browser: chrome
quiet: true
- name: Export screen recordings (on failure only)
uses: actions/upload-artifact@v3.1.0
if: failure()
with:
name: cypress-videos
path: tests/cypress/videos
retention-days: 7
- name: Export screenshots (on failure only)
uses: actions/upload-artifact@v3.1.0
if: failure()
with:
name: cypress-screenshots
path: tests/cypress/screenshots
retention-days: 7
- name: Send Slack notifications
uses: act10ns/slack@v1
if: failure()
with:
status: ${{ job.status }}
steps: ${{ toJson(steps) }}
channel: '#tiptap-notifications'
- name: Export screen recordings (on failure only)
uses: actions/upload-artifact@v3.1.0
if: failure()
with:
name: cypress-videos
path: tests/cypress/videos
retention-days: 7
- name: Send Slack notifications
uses: act10ns/slack@v1
if: failure()
with:
status: ${{ job.status }}
steps: ${{ toJson(steps) }}
channel: '#tiptap-notifications'
build:
runs-on: ubuntu-latest
@ -135,36 +141,35 @@ jobs:
node-version: [16]
steps:
- uses: actions/checkout@v3.0.2
- uses: actions/checkout@v3.0.2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ matrix.node-version }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ matrix.node-version }}
- name: Load cached dependencies
uses: actions/cache@v3.0.11
id: cache
with:
path: |
**/node_modules
/home/runner/.cache/Cypress
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
- name: Load cached dependencies
uses: actions/cache@v3.0.11
id: cache
with:
path: |
**/node_modules
/home/runner/.cache/Cypress
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
id: install-dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm install
- name: Install dependencies
id: install-dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm install
- name: Try to build the packages
id: build-packages
run: npm run build:ci
- name: Try to build the packages
id: build-packages
run: npm run build:ci
- name: Send Slack notifications
uses: act10ns/slack@v1
if: failure()
with:
status: ${{ job.status }}
steps: ${{ toJson(steps) }}
channel: '#tiptap-notifications'
- name: Send Slack notifications
uses: act10ns/slack@v1
if: failure()
with:
status: ${{ job.status }}
steps: ${{ toJson(steps) }}
channel: '#tiptap-notifications'

View File

@ -10,7 +10,7 @@ prosemirror-keymap
prosemirror-model
prosemirror-schema-list
prosemirror-state
@tiptap/prosemirror-tables
prosemirror-tables
prosemirror-transform
prosemirror-view
react

View File

@ -33,7 +33,7 @@
"sass": "^1.49.7",
"svelte": "^3.49.0",
"tailwindcss": "^2.2.19",
"typescript": "^4.5.5",
"typescript": "4.7.4",
"uuid": "^8.3.2",
"vite": "^2.9.13",
"vite-plugin-checker": "^0.3.4",

View File

@ -34,7 +34,7 @@
"sass": "^1.49.7",
"svelte": "^3.49.0",
"tailwindcss": "^2.2.19",
"typescript": "^4.5.5",
"typescript": "4.7.4",
"uuid": "^8.3.2",
"vite": "^2.9.13",
"vite-plugin-checker": "^0.3.4",

View File

@ -1,5 +1,5 @@
import { Extension } from '@tiptap/core'
import { Plugin } from 'prosemirror-state'
import { Plugin } from '@tiptap/pm/state'
import findColors from './findColors'

View File

@ -1,5 +1,5 @@
import { Node } from 'prosemirror-model'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { Node } from '@tiptap/pm/model'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
export default function (doc: Node): DecorationSet {
const hexColor = /(#[0-9a-f]{3,6})\b/gi

View File

@ -1,5 +1,5 @@
import { Extension } from '@tiptap/core'
import { Plugin } from 'prosemirror-state'
import { Plugin } from '@tiptap/pm/state'
import findColors from './findColors'
@ -14,9 +14,7 @@ export const ColorHighlighter = Extension.create({
return findColors(doc)
},
apply(transaction, oldState) {
return transaction.docChanged
? findColors(transaction.doc)
: oldState
return transaction.docChanged ? findColors(transaction.doc) : oldState
},
},
props: {

View File

@ -1,8 +1,8 @@
import { Node } from 'prosemirror-model'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { Node } from '@tiptap/pm/model'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
export default function (doc: Node): DecorationSet {
const hexColor = /(#[0-9a-f]{3,6})\b/ig
const hexColor = /(#[0-9a-f]{3,6})\b/gi
const decorations: Decoration[] = []
doc.descendants((node, position) => {
@ -10,20 +10,18 @@ export default function (doc: Node): DecorationSet {
return
}
Array
.from(node.text.matchAll(hexColor))
.forEach(match => {
const color = match[0]
const index = match.index || 0
const from = position + index
const to = from + color.length
const decoration = Decoration.inline(from, to, {
class: 'color',
style: `--color: ${color}`,
})
decorations.push(decoration)
Array.from(node.text.matchAll(hexColor)).forEach(match => {
const color = match[0]
const index = match.index || 0
const from = position + index
const to = from + color.length
const decoration = Decoration.inline(from, to, {
class: 'color',
style: `--color: ${color}`,
})
decorations.push(decoration)
})
})
return DecorationSet.create(doc, decorations)

View File

@ -1,4 +1,4 @@
import { Plugin, PluginKey } from 'prosemirror-state'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import * as Y from 'yjs'
import { AnnotationState } from './AnnotationState'
@ -8,10 +8,10 @@ export const AnnotationPluginKey = new PluginKey('annotation')
export interface AnnotationPluginOptions {
HTMLAttributes: {
[key: string]: any
},
onUpdate: (items: [any?]) => {},
map: Y.Map<any>,
instance: string,
}
onUpdate: (items: [any?]) => {}
map: Y.Map<any>
instance: string
}
export const AnnotationPlugin = (options: AnnotationPluginOptions) => new Plugin({
@ -39,9 +39,7 @@ export const AnnotationPlugin = (options: AnnotationPluginOptions) => new Plugin
return decorations
}
const annotations = this
.getState(state)
.annotationsAt(selection.from)
const annotations = this.getState(state).annotationsAt(selection.from)
options.onUpdate(annotations)

View File

@ -1,18 +1,26 @@
import { EditorState, Transaction } from 'prosemirror-state'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { absolutePositionToRelativePosition, relativePositionToAbsolutePosition, ySyncPluginKey } from 'y-prosemirror'
import { EditorState, Transaction } from '@tiptap/pm/state'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
import {
absolutePositionToRelativePosition,
relativePositionToAbsolutePosition,
ySyncPluginKey,
} from 'y-prosemirror'
import * as Y from 'yjs'
import { AnnotationItem } from './AnnotationItem'
import { AnnotationPluginKey } from './AnnotationPlugin'
import { AddAnnotationAction, DeleteAnnotationAction, UpdateAnnotationAction } from './collaboration-annotation'
import {
AddAnnotationAction,
DeleteAnnotationAction,
UpdateAnnotationAction,
} from './collaboration-annotation'
export interface AnnotationStateOptions {
HTMLAttributes: {
[key: string]: any
},
map: Y.Map<any>,
instance: string,
}
map: Y.Map<any>
instance: string
}
export class AnnotationState {
@ -93,14 +101,27 @@ export class AnnotationState {
}
// eslint-disable-next-line
console.log(`[${this.options.instance}] Decoration.inline()`, from, to, HTMLAttributes, { id, data: annotation.data })
console.log(`[${this.options.instance}] Decoration.inline()`, from, to, HTMLAttributes, {
id,
data: annotation.data,
})
if (from === to) {
console.warn(`[${this.options.instance}] corrupt decoration `, annotation.from, from, annotation.to, to)
console.warn(
`[${this.options.instance}] corrupt decoration `,
annotation.from,
from,
annotation.to,
to,
)
}
decorations.push(
Decoration.inline(from, to, HTMLAttributes, { id, data: annotation.data, inclusiveEnd: true }),
Decoration.inline(from, to, HTMLAttributes, {
id,
data: annotation.data,
inclusiveEnd: true,
}),
)
})
@ -109,7 +130,10 @@ export class AnnotationState {
apply(transaction: Transaction, state: EditorState) {
// Add/Remove annotations
const action = transaction.getMeta(AnnotationPluginKey) as AddAnnotationAction | UpdateAnnotationAction | DeleteAnnotationAction
const action = transaction.getMeta(AnnotationPluginKey) as
| AddAnnotationAction
| UpdateAnnotationAction
| DeleteAnnotationAction
if (action && action.type) {
// eslint-disable-next-line

View File

@ -1,5 +1,5 @@
import { mergeAttributes, Node } from '@tiptap/core'
import { Plugin } from 'prosemirror-state'
import { Plugin } from '@tiptap/pm/state'
export const Figure = Node.create({
name: 'figure',

View File

@ -1,6 +1,6 @@
import { Extension } from '@tiptap/core'
import { NodeSelection, Plugin } from 'prosemirror-state'
import { __serializeForClipboard as serializeForClipboard } from 'prosemirror-view'
import { NodeSelection, Plugin } from '@tiptap/pm/state'
import { __serializeForClipboard as serializeForClipboard } from '@tiptap/pm/view'
function removeNode(node) {
node.parentNode.removeChild(node)
@ -25,7 +25,8 @@ export default Extension.create({
node = node.node
while (node && node.parentNode) {
if (node.parentNode?.classList?.contains('ProseMirror')) { // todo
if (node.parentNode?.classList?.contains('ProseMirror')) {
// todo
break
}
@ -131,7 +132,8 @@ export default Extension.create({
if (node) {
node = node.node
while (node && node.parentNode) {
if (node.parentNode?.classList?.contains('ProseMirror')) { // todo
if (node.parentNode?.classList?.contains('ProseMirror')) {
// todo
break
}
node = node.parentNode
@ -145,7 +147,7 @@ export default Extension.create({
const rect = absoluteRect(node)
const win = node.ownerDocument.defaultView
rect.top += win.pageYOffset + ((lineHeight - 24) / 2) + top
rect.top += win.pageYOffset + (lineHeight - 24) / 2 + top
rect.left += win.pageXOffset
rect.width = `${WIDTH}px`

View File

@ -1,7 +1,7 @@
import { Extension } from '@tiptap/core'
import { Node as ProsemirrorNode } from 'prosemirror-model'
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { Node as ProsemirrorNode } from '@tiptap/pm/model'
import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
import LinterPlugin, { Result as Issue } from './LinterPlugin'
@ -22,9 +22,11 @@ function renderIcon(issue: Issue) {
function runAllLinterPlugins(doc: ProsemirrorNode, plugins: Array<typeof LinterPlugin>) {
const decorations: [any?] = []
const results = plugins.map(RegisteredLinterPlugin => {
return new RegisteredLinterPlugin(doc).scan().getResults()
}).flat()
const results = plugins
.map(RegisteredLinterPlugin => {
return new RegisteredLinterPlugin(doc).scan().getResults()
})
.flat()
results.forEach(issue => {
decorations.push(
@ -39,7 +41,7 @@ function runAllLinterPlugins(doc: ProsemirrorNode, plugins: Array<typeof LinterP
}
export interface LinterOptions {
plugins: Array<typeof LinterPlugin>,
plugins: Array<typeof LinterPlugin>
}
export const Linter = Extension.create<LinterOptions>({
@ -62,9 +64,7 @@ export const Linter = Extension.create<LinterOptions>({
return runAllLinterPlugins(doc, plugins)
},
apply(transaction, oldState) {
return transaction.docChanged
? runAllLinterPlugins(transaction.doc, plugins)
: oldState
return transaction.docChanged ? runAllLinterPlugins(transaction.doc, plugins) : oldState
},
},
props: {
@ -72,7 +72,7 @@ export const Linter = Extension.create<LinterOptions>({
return this.getState(state)
},
handleClick(view, _, event) {
const target = (event.target as IconDivElement)
const target = event.target as IconDivElement
if (/lint-icon/.test(target.className) && target.issue) {
const { from, to } = target.issue
@ -89,7 +89,7 @@ export const Linter = Extension.create<LinterOptions>({
return false
},
handleDoubleClick(view, _, event) {
const target = (event.target as IconDivElement)
const target = event.target as IconDivElement
if (/lint-icon/.test((event.target as HTMLElement).className) && target.issue) {
const prob = target.issue

View File

@ -1,9 +1,9 @@
import { Node as ProsemirrorNode } from 'prosemirror-model'
import { Node as ProsemirrorNode } from '@tiptap/pm/model'
export interface Result {
message: string,
from: number,
to: number,
message: string
from: number
to: number
fix?: Function
}

View File

@ -1,4 +1,4 @@
import { EditorView } from 'prosemirror-view'
import { EditorView } from '@tiptap/pm/view'
import LinterPlugin, { Result as Issue } from '../LinterPlugin'

View File

@ -1,4 +1,4 @@
import { EditorView } from 'prosemirror-view'
import { EditorView } from '@tiptap/pm/view'
import LinterPlugin, { Result as Issue } from '../LinterPlugin'
@ -7,13 +7,7 @@ export class Punctuation extends LinterPlugin {
fix(replacement: any) {
return function ({ state, dispatch }: EditorView, issue: Issue) {
dispatch(
state.tr.replaceWith(
issue.from,
issue.to,
state.schema.text(replacement),
),
)
dispatch(state.tr.replaceWith(issue.from, issue.to, state.schema.text(replacement)))
}
}

View File

@ -1,5 +1,5 @@
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
import { Plugin, PluginKey } from '@tiptap/pm/state'
// @ts-ignore
function nodeEqualsType({ types, node }) {
@ -13,8 +13,8 @@ function nodeEqualsType({ types, node }) {
*/
export interface TrailingNodeOptions {
node: string,
notAfter: string[],
node: string
notAfter: string[]
}
export const TrailingNode = Extension.create<TrailingNodeOptions>({
@ -23,9 +23,7 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
addOptions() {
return {
node: 'paragraph',
notAfter: [
'paragraph',
],
notAfter: ['paragraph'],
}
},

View File

@ -11,8 +11,30 @@ import {
} from 'path'
import { v4 as uuid } from 'uuid'
import { defineConfig } from 'vite'
// import checker from 'vite-plugin-checker'
const getPackageDependencies = () => {
const paths: Array<{ find: string, replacement: any }> = []
fg.sync('../packages/*', { onlyDirectories: true })
.map(name => name.replace('../packages/', ''))
.forEach(name => {
if (name === 'pm') {
fg.sync(`../packages/${name}/*`, { onlyDirectories: true })
.forEach(subName => {
const subPkgName = subName.replace(`../packages/${name}/`, '')
paths.push({ find: `@tiptap/${name}/${subPkgName}`, replacement: resolve(`../packages/${name}/${subPkgName}/index.ts`) })
})
} else {
paths.push({ find: `@tiptap/${name}`, replacement: resolve(`../packages/${name}/src/index.ts`) })
}
})
return paths
}
const includeDependencies = fs.readFileSync('./includeDependencies.txt')
.toString()
.replace(/\r\n/g, '\n')
@ -271,12 +293,6 @@ export default defineConfig({
],
resolve: {
alias: [
...fg.sync('../packages/*', { onlyDirectories: true })
.map(name => name.replace('../packages/', ''))
.map(name => {
return { find: `@tiptap/${name}`, replacement: resolve(`../packages/${name}/src/index.ts`) }
}),
],
alias: getPackageDependencies(),
},
})

View File

@ -120,7 +120,7 @@ addCommands() {
If youre just wrapping a plain ProseMirror command, youll need to pass `dispatch` anyway. Then theres also no need to check it:
```js
import { exitCode } from 'prosemirror-commands'
import { exitCode } from '@tiptap/pm/commands'
export default () => ({ state, dispatch }) => {
return exitCode(state, dispatch)

View File

@ -112,7 +112,7 @@ Alternatively you can pass a ProseMirror `PluginKey`.
```js
import { Editor } from '@tiptap/core'
import BubbleMenu from '@tiptap/extension-bubble-menu'
import { PluginKey } from 'prosemirror-state'
import { PluginKey } from '@tiptap/pm/state'
new Editor({
extensions: [

View File

@ -20,10 +20,6 @@ We kindly ask you to [sponsor our work](/sponsor) when using this extension in p
npm install @tiptap/extension-collaboration-cursor
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please [see here](https://tiptap.dev/installation/peer-dependencies#tiptapextension-collaboration-cursor) which packages are needed and how to install them.
:::
This extension requires the [`Collaboration`](/api/extensions/collaboration) extension.
## Settings

View File

@ -20,10 +20,6 @@ We kindly ask you to [sponsor our work](/sponsor) when using this extension in p
npm install @tiptap/extension-collaboration yjs y-websocket
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please [see here](https://tiptap.dev/installation/peer-dependencies#tiptapextension-collaboration) which packages are needed and how to install them.
:::
## Settings
### document

View File

@ -4,6 +4,7 @@ icon: drag-drop-line
---
# Dropcursor
[![Version](https://img.shields.io/npm/v/@tiptap/extension-dropcursor.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-dropcursor)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-dropcursor.svg)](https://npmcharts.com/compare/@tiptap/extension-dropcursor?minimal=true)
@ -12,28 +13,27 @@ This extension loads the [ProseMirror Dropcursor plugin](https://github.com/Pros
Note that Tiptap is headless, but the dropcursor needs CSS for its appearance. There are settings for the color and width, and youre free to add a custom CSS class.
## Installation
```bash
npm install @tiptap/extension-dropcursor
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please [see here](https://tiptap.dev/installation/peer-dependencies#tiptapextension-dropcursor) which packages are needed and how to install them.
:::
## Settings
### color
Color of the dropcursor.
Default: `'currentColor'`
```js
Dropcursor.configure({
color: '#ff0000'
color: '#ff0000',
})
```
### width
Width of the dropcursor.
Default: `1`
@ -45,6 +45,7 @@ Dropcursor.configure({
```
### class
One or multiple CSS classes that should be applied to the dropcursor.
```js
@ -54,7 +55,9 @@ Dropcursor.configure({
```
## Source code
[packages/extension-dropcursor/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-dropcursor/)
## Usage
https://embed.tiptap.dev/preview/Extensions/Dropcursor

View File

@ -100,7 +100,7 @@ Alternatively you can pass a ProseMirror `PluginKey`.
```js
import { Editor } from '@tiptap/core'
import FloatingMenu from '@tiptap/extension-floating-menu'
import { PluginKey } from 'prosemirror-state'
import { PluginKey } from '@tiptap/pm/state'
new Editor({
extensions: [

View File

@ -4,6 +4,7 @@ icon: space
---
# Gapcursor
[![Version](https://img.shields.io/npm/v/@tiptap/extension-gapcursor.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-gapcursor)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-gapcursor.svg)](https://npmcharts.com/compare/@tiptap/extension-gapcursor?minimal=true)
@ -12,16 +13,15 @@ This extension loads the [ProseMirror Gapcursor plugin](https://github.com/Prose
Note that Tiptap is headless, but the gapcursor needs CSS for its appearance. The [default CSS](https://github.com/ueberdosis/tiptap/tree/main/packages/core/src/style.ts) is loaded through the Editor class.
## Installation
```bash
npm install @tiptap/extension-gapcursor
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please [see here](https://tiptap.dev/installation/peer-dependencies#tiptapextension-gapcursor) which packages are needed and how to install them.
:::
## Source code
[packages/extension-gapcursor/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-gapcursor/)
## Usage
https://embed.tiptap.dev/preview/Extensions/Gapcursor

View File

@ -4,23 +4,22 @@ icon: history-line
---
# History
[![Version](https://img.shields.io/npm/v/@tiptap/extension-history.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-history)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-history.svg)](https://npmcharts.com/compare/@tiptap/extension-history?minimal=true)
This extension provides history support. All changes to the document will be tracked and can be removed with `undo`. Undone changes can be applied with `redo` again.
## Installation
```bash
npm install @tiptap/extension-history
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please [see here](https://tiptap.dev/installation/peer-dependencies#tiptapextension-history) which packages are needed and how to install them.
:::
## Settings
### depth
The amount of history events that are collected before the oldest events are discarded. Defaults to 100.
Default: `100`
@ -32,6 +31,7 @@ History.configure({
```
### newGroupDelay
The delay between changes after which a new group should be started (in milliseconds). When changes arent adjacent, a new group is always started.
Default: `500`
@ -45,12 +45,15 @@ History.configure({
## Commands
### undo()
Undo the last change.
```js
editor.commands.undo()
```
### redo()
Redo the last change.
```js
@ -58,13 +61,16 @@ editor.commands.redo()
```
## Keyboard shortcuts
| Command | Windows/Linux | macOS |
| ------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| undo() | `Control`&nbsp;`Z`<br>`Control`&nbsp;`я` | `Cmd`&nbsp;`Z`<br>`Cmd`&nbsp;`я` |
| redo() | `Shift`&nbsp;`Control`&nbsp;`Z`<br>`Control`&nbsp;`Y`<br>`Shift`&nbsp;`Control`&nbsp;`я` | `Shift`&nbsp;`Cmd`&nbsp;`Z`<br>`Cmd`&nbsp;`Y`<br>`Shift`&nbsp;`Cmd`&nbsp;`я` |
## Source code
[packages/extension-history/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-history/)
## Usage
https://embed.tiptap.dev/preview/Extensions/History

View File

@ -14,10 +14,6 @@ The `StarterKit` is a collection of the most popular Tiptap extensions. If you
npm install @tiptap/starter-kit
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please [see here](https://tiptap.dev/installation/peer-dependencies#tiptapstarter-kit) which packages are needed and how to install them.
:::
## Included extensions
### Nodes

View File

@ -1,5 +1,5 @@
# Introduction
tiptap is a friendly wrapper around [ProseMirror](https://ProseMirror.net). Although Tiptap tries to hide most of the complexity of ProseMirror, its built on top of its APIs and we recommend you to read through the [ProseMirror Guide](https://ProseMirror.net/docs/guide/) for advanced usage.
Tiptap is a friendly wrapper around [ProseMirror](https://ProseMirror.net). Although Tiptap tries to hide most of the complexity of ProseMirror, its built on top of its APIs and we recommend you to read through the [ProseMirror Guide](https://ProseMirror.net/docs/guide/) for advanced usage.
### Structure
ProseMirror works with a strict [Schema](/api/schema), which defines the allowed structure of a document. A document is a tree of headings, paragraphs and others elements, so called nodes. Marks can be attached to a node, e. g. to emphasize part of it. [Commands](/api/commands) change that document programmatically.

View File

@ -28,10 +28,6 @@ First, install the dependencies:
npm install @tiptap/extension-collaboration yjs y-webrtc
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please [see here](https://tiptap.dev/installation/peer-dependencies#tiptapextension-collaboration) which packages are needed and how to install them.
:::
Now, create a new Y document, and register it with Tiptap:
```js
@ -78,10 +74,6 @@ For the client, the example is nearly the same, only the provider is different.
npm install @tiptap/extension-collaboration @hocuspocus/provider
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please [see here](https://tiptap.dev/installation/peer-dependencies#tiptapextension-collaboration) which packages are needed and how to install them.
:::
And then register the WebSocket provider with Tiptap:
```js
@ -293,7 +285,7 @@ server.listen()
## Pitfalls
### Schema updates
tiptap is very strict with the [schema](/api/schema), that means, if you add something thats not allowed according to the configured schema itll be thrown away. That can lead to a strange behaviour when multiple clients with different schemas share changes to a document.
Tiptap is very strict with the [schema](/api/schema), that means, if you add something thats not allowed according to the configured schema itll be thrown away. That can lead to a strange behaviour when multiple clients with different schemas share changes to a document.
Lets say you added an editor to your app and the first people use it already. They have all a loaded instance of Tiptap with all default extensions, and therefor a schema that only allows those. But you want to add task lists in the next update, so you add the extension and deploy again.

View File

@ -114,7 +114,7 @@ const awesomeness = editor.storage.customExtension.awesomeness
```
### Schema
tiptap works with a strict schema, which configures how the content can be structured, nested, how it behaves and many more things. You [can change all aspects of the schema](/api/schema) for existing extensions. Lets walk through a few common use cases.
Tiptap works with a strict schema, which configures how the content can be structured, nested, how it behaves and many more things. You [can change all aspects of the schema](/api/schema) for existing extensions. Lets walk through a few common use cases.
The default `Blockquote` extension can wrap other nodes, like headings. If you want to allow nothing but paragraphs in your blockquotes, set the `content` attribute accordingly:
@ -531,7 +531,7 @@ After all, Tiptap is built on ProseMirror and ProseMirror has a pretty powerful
You can wrap existing ProseMirror plugins in Tiptap extensions like shown in the example below.
```js
import { history } from 'prosemirror-history'
import { history } from '@tiptap/pm/history'
const History = Extension.create({
addProseMirrorPlugins() {
@ -550,7 +550,7 @@ Or you can add them to a Tiptap extension like shown in the below example.
```js
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
import { Plugin, PluginKey } from '@tiptap/pm/state'
export const EventHandler = Extension.create({
name: 'eventHandler',

39
docs/guide/prosemirror.md Normal file
View File

@ -0,0 +1,39 @@
---
tableOfContents: true
---
# Accessing ProseMirror internals
The ProseMirror internals are packaged in the `@tiptap/pm` package that you need to install with `npm install @tiptap/pm`. If you already have this done you can skip the following step.
```bash
npm i @tiptap/pm
```
After that you can access all internal ProseMirror packages like this:
```js
// this example loads the EditorState class from the ProseMirror state package
import { EditorState } from '@tiptap/pm/state'
```
The following packages are available:
- `@tiptap/pm/changeset`
- `@tiptap/pm/collab`
- `@tiptap/pm/commands`
- `@tiptap/pm/dropcursor`
- `@tiptap/pm/gapcursor`
- `@tiptap/pm/history`
- `@tiptap/pm/inputrules`
- `@tiptap/pm/keymap`
- `@tiptap/pm/markdown`
- `@tiptap/pm/menu`
- `@tiptap/pm/model`
- `@tiptap/pm/schema-basic`
- `@tiptap/pm/schema-list`
- `@tiptap/pm/state`
- `@tiptap/pm/tables`
- `@tiptap/pm/trailing-node`
- `@tiptap/pm/transform`
- `@tiptap/pm/view`

View File

@ -5,24 +5,33 @@ tableOfContents: true
# Installation
## Introduction
Tiptap is framework-agnostic and even works with Vanilla JavaScript (if thats your thing). The following integration guides help you integrating Tiptap in your JavaScript project.
## Integration guides
<!-- * [CDN](/installation/cdn) -->
* [Vanilla JavaScript](/installation/vanilla-javascript)
* [React](/installation/react)
* [Next.js](/installation/nextjs)
* [Vue 3](/installation/vue3)
* [Vue 2](/installation/vue2)
* [Nuxt.js](/installation/nuxt)
* [Svelte](/installation/svelte)
* [Alpine.js](/installation/alpine)
* [PHP](/installation/php)
## Base Setup
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please [read this](https://tiptap.dev/installation/peer-dependencies) to understand what is needed in that case.
:::
To get started you will need to install `@tiptap/core`, `@tiptap/pm` and `@tiptap/starter-kit` in your project like this:
```bash
npm install @tiptap/core @tiptap/pm @tiptap/starter-kit
```
After that, you can start using Tiptap in your project. To integrate it into your framework, please follow the guides below.
## Integration guides
- [Vanilla JavaScript](/installation/vanilla-javascript)
- [React](/installation/react)
- [Next.js](/installation/nextjs)
- [Vue 3](/installation/vue3)
- [Vue 2](/installation/vue2)
- [Nuxt.js](/installation/nuxt)
- [Svelte](/installation/svelte)
- [Alpine.js](/installation/alpine)
- [PHP](/installation/php)
<!-- [CDN](/installation/cdn)-->
### Community efforts
* [Angular](https://github.com/sibiraj-s/ngx-tiptap)
* [SolidJS](https://github.com/LXSMNSYC/solid-tiptap)
- [Angular](https://github.com/sibiraj-s/ngx-tiptap)
- [SolidJS](https://github.com/LXSMNSYC/solid-tiptap)

View File

@ -28,16 +28,12 @@ npm run dev
## 2. Install the dependencies
Okay, enough of the boring boilerplate work. Lets finally install Tiptap! For the following example youll need `alpinejs`, the `@tiptap/core` package and the `@tiptap/starter-kit` which has the most common extensions to get started quickly.
Okay, enough of the boring boilerplate work. Lets finally install Tiptap! For the following example youll need `alpinejs`, the `@tiptap/core` package, the `@tiptap/pm` package and the `@tiptap/starter-kit` which has the most common extensions to get started quickly.
```bash
npm install alpinejs @tiptap/core @tiptap/starter-kit
npm install alpinejs @tiptap/core @tiptap/pm @tiptap/starter-kit
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please check the following links to find out what dependencies are needed and how to install them: [@tiptap/core](https://tiptap.dev/installation/peer-dependencies#tiptapcore), [@tiptap/starter-kit](https://tiptap.dev/installation/peer-dependencies#tiptapstarter-kit)
:::
If you followed step 1, you can now start your project with `npm run dev`, and open [http://localhost:5173](http://localhost:5173) in your favorite browser. This might be different, if youre working with an existing project.
## 3. Initialize the editor
@ -74,7 +70,7 @@ document.addEventListener('alpine:init', () => {
onSelectionUpdate({ editor }) {
_this.updatedAt = Date.now()
}
});
})
},
isLoaded() {
return editor
@ -91,9 +87,9 @@ document.addEventListener('alpine:init', () => {
toggleItalic() {
editor.chain().toggleItalic().focus().run()
},
};
});
});
}
})
})
window.Alpine = Alpine
Alpine.start()

View File

@ -25,16 +25,12 @@ cd my-tiptap-project
```
## 2. Install the dependencies
Now that we have a standard boilerplate set up we can get started on getting Tiptap up and running! For this we will need to install two packages: `@tiptap/react` and `@tiptap/starter-kit` which includes all the extensions you need to get started quickly.
Now that we have a standard boilerplate set up we can get started on getting Tiptap up and running! For this we will need to install three packages: `@tiptap/react`, `@tiptap/pm` and `@tiptap/starter-kit` which includes all the extensions you need to get started quickly.
```bash
npm install @tiptap/react @tiptap/starter-kit
npm install @tiptap/react @tiptap/pm @tiptap/starter-kit
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please check the following links to find out what dependencies are needed and how to install them: [@tiptap/core](https://tiptap.dev/installation/peer-dependencies#tiptapcore), [@tiptap/starter-kit](https://tiptap.dev/installation/peer-dependencies#tiptapstarter-kit)
:::
If you followed step 1 and 2, you can now start your project with `npm run dev`, and open [http://localhost:3000/](http://localhost:3000/) in your favorite browser. This might be different, if youre working with an existing project.
## 3. Create a new component
@ -57,7 +53,7 @@ const Tiptap = () => {
)
}
export default Tiptap;
export default Tiptap
```
## 4. Add it to your app

View File

@ -26,16 +26,12 @@ cd my-tiptap-project
```
## 2. Install the dependencies
Okay, enough of the boring boilerplate work. Lets finally install Tiptap! For the following example youll need the `@tiptap/vue-2` package, with a few components, and `@tiptap/starter-kit` which has the most common extensions to get started quickly.
Okay, enough of the boring boilerplate work. Lets finally install Tiptap! For the following example youll need the `@tiptap/vue-2` package with a few components, the `@tiptap/pm` package, and `@tiptap/starter-kit` which has the most common extensions to get started quickly.
```bash
npm install @tiptap/vue-2 @tiptap/starter-kit
npm install @tiptap/vue-2 @tiptap/pm @tiptap/starter-kit
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please check the following links to find out what dependencies are needed and how to install them: [@tiptap/core](https://tiptap.dev/installation/peer-dependencies#tiptapcore), [@tiptap/starter-kit](https://tiptap.dev/installation/peer-dependencies#tiptapstarter-kit)
:::
If you followed step 1 and 2, you can now start your project with `npm run serve`, and open [http://localhost:8080/](http://localhost:8080/) in your favorite browser. This might be different, if youre working with an existing project.
## 3. Create a new component

View File

@ -1,80 +0,0 @@
---
tableOfContents: true
---
# Peer dependencies
## Introduction
With the release of version 2.0.0-beta.205 we introduced peer dependencies. Most packages require the installation of peer dependencies.
## Why peer dependencies
In the past it has happened that users installed ProseMirror or Yjs packages to develope their own extensions, which had a different version than the ones included in Tiptap. This has caused version clashes.
## How to install
### NPM 7 or higher
If you are using NPM 7 or higher, you can ignore the following notes. NPM installs peer dependencies automatically and no further action is required.
### Yarn, pNPM, npm 6 or less
#### @tiptap/core
| Package manager | Command |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Yarn | `yarn add prosemirror-commands prosemirror-keymap prosemirror-model prosemirror-schema-list prosemirror-state prosemirror-transform prosemirror-view` |
| pNPM | `pnpm install prosemirror-commands prosemirror-keymap prosemirror-model prosemirror-schema-list prosemirror-state prosemirror-transform prosemirror-view` |
| npm 6 or less | `npm install prosemirror-commands prosemirror-keymap prosemirror-model prosemirror-schema-list prosemirror-state prosemirror-transform prosemirror-view` |
#### @tiptap/starter-kit
| Package manager | Command |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| Yarn | `yarn add prosemirror-history prosemirror-dropcursor prosemirror-gapcursor` |
| pNPM | `pnpm install prosemirror-history prosemirror-dropcursor prosemirror-gapcursor` |
| npm 6 or less | `npm install prosemirror-history prosemirror-dropcursor prosemirror-gapcursor` |
#### @tiptap/extension-history
| Package manager | Command |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| Yarn | `yarn add prosemirror-history` |
| pNPM | `pnpm install prosemirror-history` |
| npm 6 or less | `npm install prosemirror-history` |
#### @tiptap/extension-gapcursor
| Package manager | Command |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| Yarn | `yarn add prosemirror-gapcursor` |
| pNPM | `pnpm install prosemirror-gapcursor` |
| npm 6 or less | `npm install prosemirror-gapcursor` |
#### @tiptap/extension-dropcursor
| Package manager | Command |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| Yarn | `yarn add prosemirror-dropcursor` |
| pNPM | `pnpm install prosemirror-dropcursor` |
| npm 6 or less | `npm install prosemirror-dropcursor` |
#### @tiptap/extension-collaboration
| Package manager | Command |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| Yarn | `yarn add y-prosemirror` |
| pNPM | `pnpm install y-prosemirror` |
| npm 6 or less | `npm install y-prosemirror` |
#### @tiptap/extension-collaboration-cursor
| Package manager | Command |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| Yarn | `yarn add y-prosemirror` |
| pNPM | `pnpm install y-prosemirror` |
| npm 6 or less | `npm install y-prosemirror` |

View File

@ -37,13 +37,9 @@ cd my-tiptap-project
Time to install the `@tiptap/react` package and our [`StarterKit`](/api/extensions/starter-kit), which has the most popular extensions to get started quickly.
```bash
npm install @tiptap/react @tiptap/starter-kit
npm install @tiptap/react @tiptap/pm @tiptap/starter-kit
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please check the following links to find out what dependencies are needed and how to install them: [@tiptap/core](https://tiptap.dev/installation/peer-dependencies#tiptapcore), [@tiptap/starter-kit](https://tiptap.dev/installation/peer-dependencies#tiptapstarter-kit)
:::
If you followed step 1 and 2, you can now start your project with `npm run start`, and open [http://localhost:3000](http://localhost:3000) in your browser.
#### 3. Create a new component

View File

@ -28,16 +28,12 @@ npm run dev
```
## 2. Install the dependencies
Okay, enough of the boring boilerplate work. Lets finally install Tiptap! For the following example youll need the `@tiptap/core` package, with a few components, and `@tiptap/starter-kit` which has the most common extensions to get started quickly.
Okay, enough of the boring boilerplate work. Lets finally install Tiptap! For the following example youll need the `@tiptap/core` package, with a few components, `@tiptap/pm` and `@tiptap/starter-kit` which has the most common extensions to get started quickly.
```bash
npm install @tiptap/core @tiptap/starter-kit
npm install @tiptap/core @tiptap/pm @tiptap/starter-kit
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please check the following links to find out what dependencies are needed and how to install them: [@tiptap/core](https://tiptap.dev/installation/peer-dependencies#tiptapcore), [@tiptap/starter-kit](https://tiptap.dev/installation/peer-dependencies#tiptapstarter-kit)
:::
If you followed step 1 and 2, you can now start your project with `npm run dev`, and open [http://localhost:3000/](http://localhost:3000/) in your favorite browser. This might be different, if youre working with an existing project.
## 3. Create a new component

View File

@ -9,18 +9,14 @@ tableOfContents: true
You are using plain JavaScript or a framework that is not listed here? No worries, we provide everything you need.
## 1. Install the dependencies
For the following example you will need `@tiptap/core` (the actual editor) and `@tiptap/starter-kit`.
For the following example you will need `@tiptap/core` (the actual editor), `@tiptap/pm` (the ProseMirror library) and `@tiptap/starter-kit`.
The StarterKit doesnt include all, but the most common extensions.
```bash
npm install @tiptap/core @tiptap/starter-kit
npm install @tiptap/core @tiptap/pm @tiptap/starter-kit
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please check the following links to find out what dependencies are needed and how to install them: [@tiptap/core](https://tiptap.dev/installation/peer-dependencies#tiptapcore), [@tiptap/starter-kit](https://tiptap.dev/installation/peer-dependencies#tiptapstarter-kit)
:::
## 2. Add some markup
Add the following HTML where you want the editor to be mounted:

View File

@ -33,10 +33,6 @@ Okay, enough of the boring boilerplate work. Lets finally install Tiptap! For
npm install @tiptap/vue-2 @tiptap/starter-kit
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please check the following links to find out what dependencies are needed and how to install them: [@tiptap/core](https://tiptap.dev/installation/peer-dependencies#tiptapcore), [@tiptap/starter-kit](https://tiptap.dev/installation/peer-dependencies#tiptapstarter-kit)
:::
If you followed step 1 and 2, you can now start your project with `npm run dev`, and open [http://localhost:8080](http://localhost:8080) in your favorite browser. This might be different, if youre working with an existing project.
## 3. Create a new component

View File

@ -33,10 +33,6 @@ Okay, enough of the boring boilerplate work. Lets finally install Tiptap! For
npm install @tiptap/vue-3 @tiptap/starter-kit
```
:::warning Are you using Yarn, pNPM, npm 6 or less?
Unfortunately your package manager does not install peer dependencies automatically and you have to install them by your own. Please check the following links to find out what dependencies are needed and how to install them: [@tiptap/core](https://tiptap.dev/installation/peer-dependencies#tiptapcore), [@tiptap/starter-kit](https://tiptap.dev/installation/peer-dependencies#tiptapstarter-kit)
:::
If you followed step 1 and 2, you can now start your project with `npm run serve`, and open [http://localhost:8080](http://localhost:8080) in your favorite browser. This might be different, if youre working with an existing project.
## 3. Create a new component

View File

@ -10,7 +10,7 @@ tableOfContents: true
[![License](https://img.shields.io/npm/l/@tiptap/core.svg)](https://www.npmjs.com/package/@tiptap/core)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)
tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
Create exactly the rich text editor you want out of customizable building blocks. Tiptap comes with sensible defaults, a lot of extensions and a friendly API to customize every aspect. Its backed by a welcoming community, open source, and free.

View File

@ -120,6 +120,8 @@
items:
- title: Configuration
link: /guide/configuration
- title: ProseMirror
link: /guide/prosemirror
- title: Menus
link: /guide/menus
- title: Styling

863
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,9 +13,10 @@
"lint": "eslint --quiet --no-error-on-unmatched-pattern ./",
"lint:fix": "eslint --fix --quiet --no-error-on-unmatched-pattern ./",
"lint:staged": "lint-staged",
"test:open": "cypress open --project tests",
"test": "cypress run --project tests",
"build": "npm run clean:packages && lerna run build",
"test:open": "npm run build:pm && cypress open --project tests",
"test": "npm run build:pm && cypress run --project tests",
"build": "npm run clean:packages && npm run clean:packs && lerna run build",
"build:pm": "npm --prefix ./packages/pm run build",
"build:demos": "npm --prefix ./demos run build:demos",
"build:ci": "npm run build",
"release:major": "lerna version major --force-publish",
@ -26,9 +27,10 @@
"release:patch:pre": "lerna version prepatch --force-publish",
"release:pre": "lerna version prerelease --force-publish",
"publish": "npm run build:packages && lerna exec --since --no-private -- npm publish --access public",
"pack": "lerna exec -- npm pack",
"clean:packages": "rm -rf ./packages/*/dist",
"reset": "npm run clean:packages && rm -rf ./**/.cache && rm -rf ./**/node_modules && rm -rf ./package-lock.json && npm install",
"pack": "npm run clean:packs && lerna exec -- npm pack",
"clean:packages": "rm -rf ./packages/*/dist && rm -rf ./packages/pm/*/dist",
"clean:packs": "rm -rf ./packages/*/*.tgz",
"reset": "npm run clean:packages && npm run clean:packs && rm -rf ./**/.cache && rm -rf ./**/node_modules && rm -rf ./package-lock.json && npm install",
"prepare": "husky install"
},
"devDependencies": {
@ -56,7 +58,7 @@
"lerna": "^5.5.1",
"lint-staged": "^13.0.3",
"minimist": "^1.2.5",
"ts-loader": "^9.2.6",
"ts-loader": "9.3.1",
"tsup": "^6.5.0",
"typescript": "4.7.4",
"webpack": "^5.68.0"

View File

@ -31,22 +31,10 @@
"dist"
],
"peerDependencies": {
"prosemirror-commands": "^1.3.1",
"prosemirror-keymap": "^1.2.0",
"prosemirror-model": "^1.18.1",
"prosemirror-schema-list": "^1.2.2",
"prosemirror-state": "^1.4.1",
"prosemirror-transform": "^1.7.0",
"prosemirror-view": "^1.28.2"
"@tiptap/pm": "^2.0.0-beta.209"
},
"devDependencies": {
"prosemirror-commands": "^1.3.1",
"prosemirror-keymap": "^1.2.0",
"prosemirror-model": "^1.18.1",
"prosemirror-schema-list": "^1.2.2",
"prosemirror-state": "^1.4.1",
"prosemirror-transform": "^1.7.0",
"prosemirror-view": "^1.28.2"
"@tiptap/pm": "^2.0.0-beta.209"
},
"repository": {
"type": "git",

View File

@ -1,27 +1,19 @@
import { EditorState, Transaction } from 'prosemirror-state'
import { EditorState, Transaction } from '@tiptap/pm/state'
import { Editor } from './Editor'
import { createChainableState } from './helpers/createChainableState'
import {
AnyCommands,
CanCommands,
ChainedCommands,
CommandProps,
SingleCommands,
AnyCommands, CanCommands, ChainedCommands, CommandProps, SingleCommands,
} from './types'
export class CommandManager {
editor: Editor
rawCommands: AnyCommands
customState?: EditorState
constructor(props: {
editor: Editor,
state?: EditorState,
}) {
constructor(props: { editor: Editor; state?: EditorState }) {
this.editor = props.editor
this.rawCommands = this.editor.extensionManager.commands
this.customState = props.state
@ -41,9 +33,8 @@ export class CommandManager {
const { tr } = state
const props = this.buildProps(tr)
return Object.fromEntries(Object
.entries(rawCommands)
.map(([name, command]) => {
return Object.fromEntries(
Object.entries(rawCommands).map(([name, command]) => {
const method = (...args: any[]) => {
const callback = command(...args)(props)
@ -55,7 +46,8 @@ export class CommandManager {
}
return [name, method]
})) as unknown as SingleCommands
}),
) as unknown as SingleCommands
}
get chain(): () => ChainedCommands {
@ -87,18 +79,20 @@ export class CommandManager {
}
const chain = {
...Object.fromEntries(Object.entries(rawCommands).map(([name, command]) => {
const chainedCommand = (...args: never[]) => {
const props = this.buildProps(tr, shouldDispatch)
const callback = command(...args)(props)
...Object.fromEntries(
Object.entries(rawCommands).map(([name, command]) => {
const chainedCommand = (...args: never[]) => {
const props = this.buildProps(tr, shouldDispatch)
const callback = command(...args)(props)
callbacks.push(callback)
callbacks.push(callback)
return chain
}
return chain
}
return [name, chainedCommand]
})),
return [name, chainedCommand]
}),
),
run,
} as unknown as ChainedCommands
@ -110,11 +104,11 @@ export class CommandManager {
const dispatch = false
const tr = startTr || state.tr
const props = this.buildProps(tr, dispatch)
const formattedCommands = Object.fromEntries(Object
.entries(rawCommands)
.map(([name, command]) => {
const formattedCommands = Object.fromEntries(
Object.entries(rawCommands).map(([name, command]) => {
return [name, (...args: never[]) => command(...args)({ ...props, dispatch: undefined })]
})) as unknown as SingleCommands
}),
) as unknown as SingleCommands
return {
...formattedCommands,
@ -138,21 +132,18 @@ export class CommandManager {
state,
transaction: tr,
}),
dispatch: shouldDispatch
? () => undefined
: undefined,
dispatch: shouldDispatch ? () => undefined : undefined,
chain: () => this.createChain(tr),
can: () => this.createCan(tr),
get commands() {
return Object.fromEntries(Object
.entries(rawCommands)
.map(([name, command]) => {
return Object.fromEntries(
Object.entries(rawCommands).map(([name, command]) => {
return [name, (...args: never[]) => command(...args)(props)]
})) as unknown as SingleCommands
}),
) as unknown as SingleCommands
},
}
return props
}
}

View File

@ -1,11 +1,8 @@
import { MarkType, NodeType, Schema } from 'prosemirror-model'
import { MarkType, NodeType, Schema } from '@tiptap/pm/model'
import {
EditorState,
Plugin,
PluginKey,
Transaction,
} from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
EditorState, Plugin, PluginKey, Transaction,
} from '@tiptap/pm/state'
import { EditorView } from '@tiptap/pm/view'
import { CommandManager } from './CommandManager'
import { EventEmitter } from './EventEmitter'
@ -39,7 +36,6 @@ export interface HTMLElement {
}
export class Editor extends EventEmitter<EditorEvents> {
private commandManager!: CommandManager
public extensionManager!: ExtensionManager
@ -182,9 +178,7 @@ export class Editor extends EventEmitter<EditorEvents> {
// since plugins are applied after creating the view
// `editable` is always `true` for one tick.
// thats why we also have to check for `options.editable`
return this.options.editable
&& this.view
&& this.view.editable
return this.options.editable && this.view && this.view.editable
}
/**
@ -200,7 +194,10 @@ export class Editor extends EventEmitter<EditorEvents> {
* @param plugin A ProseMirror plugin
* @param handlePlugins Control how to merge the plugin into the existing plugins.
*/
public registerPlugin(plugin: Plugin, handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[]): void {
public registerPlugin(
plugin: Plugin,
handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
): void {
const plugins = isFunction(handlePlugins)
? handlePlugins(plugin, [...this.state.plugins])
: [...this.state.plugins, plugin]
@ -220,10 +217,8 @@ export class Editor extends EventEmitter<EditorEvents> {
return
}
const name = typeof nameOrPluginKey === 'string'
? `${nameOrPluginKey}$`
// @ts-ignore
: nameOrPluginKey.key
// @ts-ignore
const name = typeof nameOrPluginKey === 'string' ? `${nameOrPluginKey}$` : nameOrPluginKey.key
const state = this.state.reconfigure({
// @ts-ignore
@ -237,9 +232,7 @@ export class Editor extends EventEmitter<EditorEvents> {
* Creates an extension manager.
*/
private createExtensionManager(): void {
const coreExtensions = this.options.enableCoreExtensions
? Object.values(extensions)
: []
const coreExtensions = this.options.enableCoreExtensions ? Object.values(extensions) : []
const allExtensions = [...coreExtensions, ...this.options.extensions].filter(extension => {
return ['extension', 'node', 'mark'].includes(extension?.type)
})
@ -397,16 +390,12 @@ export class Editor extends EventEmitter<EditorEvents> {
* @param name Name of the node or mark
* @param attributes Attributes of the node or mark
*/
public isActive(name: string, attributes?: {}): boolean;
public isActive(attributes: {}): boolean;
public isActive(name: string, attributes?: {}): boolean
public isActive(attributes: {}): boolean
public isActive(nameOrAttributes: string, attributesOrUndefined?: {}): boolean {
const name = typeof nameOrAttributes === 'string'
? nameOrAttributes
: null
const name = typeof nameOrAttributes === 'string' ? nameOrAttributes : null
const attributes = typeof nameOrAttributes === 'string'
? attributesOrUndefined
: nameOrAttributes
const attributes = typeof nameOrAttributes === 'string' ? attributesOrUndefined : nameOrAttributes
return isActive(this.state, name, attributes)
}
@ -429,13 +418,10 @@ export class Editor extends EventEmitter<EditorEvents> {
* Get the document as text.
*/
public getText(options?: {
blockSeparator?: string,
textSerializers?: Record<string, TextSerializer>,
blockSeparator?: string
textSerializers?: Record<string, TextSerializer>
}): string {
const {
blockSeparator = '\n\n',
textSerializers = {},
} = options || {}
const { blockSeparator = '\n\n', textSerializers = {} } = options || {}
return getText(this.state.doc, {
blockSeparator,
@ -459,7 +445,9 @@ export class Editor extends EventEmitter<EditorEvents> {
* @deprecated
*/
public getCharacterCount(): number {
console.warn('[tiptap warn]: "editor.getCharacterCount()" is deprecated. Please use "editor.storage.characterCount.characters()" instead.')
console.warn(
'[tiptap warn]: "editor.getCharacterCount()" is deprecated. Please use "editor.storage.characterCount.characters()" instead.',
)
return this.state.doc.content.size - 2
}
@ -484,5 +472,4 @@ export class Editor extends EventEmitter<EditorEvents> {
// @ts-ignore
return !this.view?.docView
}
}

View File

@ -1,4 +1,4 @@
import { Plugin, Transaction } from 'prosemirror-state'
import { Plugin, Transaction } from '@tiptap/pm/state'
import { ExtensionConfig } from '.'
import { Editor } from './Editor'
@ -20,245 +20,265 @@ import { mergeDeep } from './utilities/mergeDeep'
declare module '@tiptap/core' {
interface ExtensionConfig<Options = any, Storage = any> {
[key: string]: any;
[key: string]: any
/**
* Name
*/
name: string,
name: string
/**
* Priority
*/
priority?: number,
priority?: number
/**
* Default options
*/
defaultOptions?: Options,
defaultOptions?: Options
/**
* Default Options
*/
addOptions?: (this: {
name: string,
parent: Exclude<ParentConfig<ExtensionConfig<Options, Storage>>['addOptions'], undefined>,
}) => Options,
name: string
parent: Exclude<ParentConfig<ExtensionConfig<Options, Storage>>['addOptions'], undefined>
}) => Options
/**
* Default Storage
*/
addStorage?: (this: {
name: string,
options: Options,
parent: Exclude<ParentConfig<ExtensionConfig<Options, Storage>>['addStorage'], undefined>,
}) => Storage,
name: string
options: Options
parent: Exclude<ParentConfig<ExtensionConfig<Options, Storage>>['addStorage'], undefined>
}) => Storage
/**
* Global attributes
*/
addGlobalAttributes?: (this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addGlobalAttributes'],
}) => GlobalAttributes | {},
name: string
options: Options
storage: Storage
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addGlobalAttributes']
}) => GlobalAttributes | {}
/**
* Raw
*/
addCommands?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addCommands'],
}) => Partial<RawCommands>,
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addCommands']
}) => Partial<RawCommands>
/**
* Keyboard shortcuts
*/
addKeyboardShortcuts?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addKeyboardShortcuts'],
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addKeyboardShortcuts']
}) => {
[key: string]: KeyboardShortcutCommand,
},
[key: string]: KeyboardShortcutCommand
}
/**
* Input rules
*/
addInputRules?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addInputRules'],
}) => InputRule[],
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addInputRules']
}) => InputRule[]
/**
* Paste rules
*/
addPasteRules?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addPasteRules'],
}) => PasteRule[],
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addPasteRules']
}) => PasteRule[]
/**
* ProseMirror plugins
*/
addProseMirrorPlugins?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addProseMirrorPlugins'],
}) => Plugin[],
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addProseMirrorPlugins']
}) => Plugin[]
/**
* Extensions
*/
addExtensions?: (this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addExtensions'],
}) => Extensions,
name: string
options: Options
storage: Storage
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addExtensions']
}) => Extensions
/**
* Extend Node Schema
*/
extendNodeSchema?: ((
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['extendNodeSchema'],
},
extension: Node,
) => Record<string, any>) | null,
extendNodeSchema?:
| ((
this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<ExtensionConfig<Options, Storage>>['extendNodeSchema']
},
extension: Node,
) => Record<string, any>)
| null
/**
* Extend Mark Schema
*/
extendMarkSchema?: ((
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['extendMarkSchema'],
},
extension: Mark,
) => Record<string, any>) | null,
extendMarkSchema?:
| ((
this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<ExtensionConfig<Options, Storage>>['extendMarkSchema']
},
extension: Mark,
) => Record<string, any>)
| null
/**
* The editor is not ready yet.
*/
onBeforeCreate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onBeforeCreate'],
}) => void) | null,
onBeforeCreate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onBeforeCreate']
}) => void)
| null
/**
* The editor is ready.
*/
onCreate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onCreate'],
}) => void) | null,
onCreate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onCreate']
}) => void)
| null
/**
* The content has changed.
*/
onUpdate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onUpdate'],
}) => void) | null,
onUpdate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onUpdate']
}) => void)
| null
/**
* The selection has changed.
*/
onSelectionUpdate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onSelectionUpdate'],
}) => void) | null,
onSelectionUpdate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onSelectionUpdate']
}) => void)
| null
/**
* The editor state has changed.
*/
onTransaction?: ((
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onTransaction'],
},
props: {
transaction: Transaction,
},
) => void) | null,
onTransaction?:
| ((
this: {
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onTransaction']
},
props: {
transaction: Transaction
},
) => void)
| null
/**
* The editor is focused.
*/
onFocus?: ((
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onFocus'],
},
props: {
event: FocusEvent,
},
) => void) | null,
onFocus?:
| ((
this: {
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onFocus']
},
props: {
event: FocusEvent
},
) => void)
| null
/**
* The editor isnt focused anymore.
*/
onBlur?: ((
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onBlur'],
},
props: {
event: FocusEvent,
},
) => void) | null,
onBlur?:
| ((
this: {
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onBlur']
},
props: {
event: FocusEvent
},
) => void)
| null
/**
* The editor is destroyed.
*/
onDestroy?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onDestroy'],
}) => void) | null,
onDestroy?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onDestroy']
}) => void)
| null
}
}
@ -289,30 +309,28 @@ export class Extension<Options = any, Storage = any> {
this.name = this.config.name
if (config.defaultOptions) {
console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`)
console.warn(
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`,
)
}
// TODO: remove `addOptions` fallback
this.options = this.config.defaultOptions
if (this.config.addOptions) {
this.options = callOrReturn(getExtensionField<AnyConfig['addOptions']>(
this,
'addOptions',
{
this.options = callOrReturn(
getExtensionField<AnyConfig['addOptions']>(this, 'addOptions', {
name: this.name,
},
))
}),
)
}
this.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
this,
'addStorage',
{
this.storage = callOrReturn(
getExtensionField<AnyConfig['addStorage']>(this, 'addStorage', {
name: this.name,
options: this.options,
},
)) || {}
}),
) || {}
}
static create<O = any, S = any>(config: Partial<ExtensionConfig<O, S>> = {}) {
@ -326,49 +344,45 @@ export class Extension<Options = any, Storage = any> {
extension.options = mergeDeep(this.options as Record<string, any>, options) as Options
extension.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
extension,
'addStorage',
{
extension.storage = callOrReturn(
getExtensionField<AnyConfig['addStorage']>(extension, 'addStorage', {
name: extension.name,
options: extension.options,
},
))
}),
)
return extension
}
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig: Partial<ExtensionConfig<ExtendedOptions, ExtendedStorage>> = {}) {
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(
extendedConfig: Partial<ExtensionConfig<ExtendedOptions, ExtendedStorage>> = {},
) {
const extension = new Extension<ExtendedOptions, ExtendedStorage>(extendedConfig)
extension.parent = this
this.child = extension
extension.name = extendedConfig.name
? extendedConfig.name
: extension.parent.name
extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name
if (extendedConfig.defaultOptions) {
console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`)
console.warn(
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`,
)
}
extension.options = callOrReturn(getExtensionField<AnyConfig['addOptions']>(
extension,
'addOptions',
{
extension.options = callOrReturn(
getExtensionField<AnyConfig['addOptions']>(extension, 'addOptions', {
name: extension.name,
},
))
}),
)
extension.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
extension,
'addStorage',
{
extension.storage = callOrReturn(
getExtensionField<AnyConfig['addStorage']>(extension, 'addStorage', {
name: extension.name,
options: extension.options,
},
))
}),
)
return extension
}

View File

@ -1,7 +1,7 @@
import { keymap } from 'prosemirror-keymap'
import { Node as ProsemirrorNode, Schema } from 'prosemirror-model'
import { Plugin } from 'prosemirror-state'
import { Decoration, EditorView } from 'prosemirror-view'
import { keymap } from '@tiptap/pm/keymap'
import { Node as ProsemirrorNode, Schema } from '@tiptap/pm/model'
import { Plugin } from '@tiptap/pm/state'
import { Decoration, EditorView } from '@tiptap/pm/view'
import { Mark, NodeConfig } from '.'
import { Editor } from './Editor'
@ -20,7 +20,6 @@ import { callOrReturn } from './utilities/callOrReturn'
import { findDuplicates } from './utilities/findDuplicates'
export class ExtensionManager {
editor: Editor
schema: Schema
@ -64,21 +63,13 @@ export class ExtensionManager {
this.editor.on('beforeCreate', onBeforeCreate)
}
const onCreate = getExtensionField<AnyConfig['onCreate']>(
extension,
'onCreate',
context,
)
const onCreate = getExtensionField<AnyConfig['onCreate']>(extension, 'onCreate', context)
if (onCreate) {
this.editor.on('create', onCreate)
}
const onUpdate = getExtensionField<AnyConfig['onUpdate']>(
extension,
'onUpdate',
context,
)
const onUpdate = getExtensionField<AnyConfig['onUpdate']>(extension, 'onUpdate', context)
if (onUpdate) {
this.editor.on('update', onUpdate)
@ -104,31 +95,19 @@ export class ExtensionManager {
this.editor.on('transaction', onTransaction)
}
const onFocus = getExtensionField<AnyConfig['onFocus']>(
extension,
'onFocus',
context,
)
const onFocus = getExtensionField<AnyConfig['onFocus']>(extension, 'onFocus', context)
if (onFocus) {
this.editor.on('focus', onFocus)
}
const onBlur = getExtensionField<AnyConfig['onBlur']>(
extension,
'onBlur',
context,
)
const onBlur = getExtensionField<AnyConfig['onBlur']>(extension, 'onBlur', context)
if (onBlur) {
this.editor.on('blur', onBlur)
}
const onDestroy = getExtensionField<AnyConfig['onDestroy']>(
extension,
'onDestroy',
context,
)
const onDestroy = getExtensionField<AnyConfig['onDestroy']>(extension, 'onDestroy', context)
if (onDestroy) {
this.editor.on('destroy', onDestroy)
@ -141,38 +120,41 @@ export class ExtensionManager {
const duplicatedNames = findDuplicates(resolvedExtensions.map(extension => extension.name))
if (duplicatedNames.length) {
console.warn(`[tiptap warn]: Duplicate extension names found: [${duplicatedNames.map(item => `'${item}'`).join(', ')}]. This can lead to issues.`)
console.warn(
`[tiptap warn]: Duplicate extension names found: [${duplicatedNames
.map(item => `'${item}'`)
.join(', ')}]. This can lead to issues.`,
)
}
return resolvedExtensions
}
static flatten(extensions: Extensions): Extensions {
return extensions
.map(extension => {
const context = {
name: extension.name,
options: extension.options,
storage: extension.storage,
}
return (
extensions
.map(extension => {
const context = {
name: extension.name,
options: extension.options,
storage: extension.storage,
}
const addExtensions = getExtensionField<AnyConfig['addExtensions']>(
extension,
'addExtensions',
context,
)
if (addExtensions) {
return [
const addExtensions = getExtensionField<AnyConfig['addExtensions']>(
extension,
...this.flatten(addExtensions()),
]
}
'addExtensions',
context,
)
return extension
})
// `Infinity` will break TypeScript so we set a number that is probably high enough
.flat(10)
if (addExtensions) {
return [extension, ...this.flatten(addExtensions())]
}
return extension
})
// `Infinity` will break TypeScript so we set a number that is probably high enough
.flat(10)
)
}
static sort(extensions: Extensions): Extensions {
@ -256,16 +238,14 @@ export class ExtensionManager {
// bind exit handling
if (extension.type === 'mark' && extension.config.exitable) {
defaultBindings.ArrowRight = () => Mark.handleExit({ editor, mark: (extension as Mark) })
defaultBindings.ArrowRight = () => Mark.handleExit({ editor, mark: extension as Mark })
}
if (addKeyboardShortcuts) {
const bindings = Object.fromEntries(
Object
.entries(addKeyboardShortcuts())
.map(([shortcut, method]) => {
return [shortcut, () => method({ editor })]
}),
Object.entries(addKeyboardShortcuts()).map(([shortcut, method]) => {
return [shortcut, () => method({ editor })]
}),
)
defaultBindings = { ...defaultBindings, ...bindings }
@ -332,46 +312,50 @@ export class ExtensionManager {
const { editor } = this
const { nodeExtensions } = splitExtensions(this.extensions)
return Object.fromEntries(nodeExtensions
.filter(extension => !!getExtensionField(extension, 'addNodeView'))
.map(extension => {
const extensionAttributes = this.attributes.filter(attribute => attribute.type === extension.name)
const context = {
name: extension.name,
options: extension.options,
storage: extension.storage,
editor,
type: getNodeType(extension.name, this.schema),
}
const addNodeView = getExtensionField<NodeConfig['addNodeView']>(
extension,
'addNodeView',
context,
)
if (!addNodeView) {
return []
}
const nodeview = (
node: ProsemirrorNode,
view: EditorView,
getPos: (() => number) | boolean,
decorations: Decoration[],
) => {
const HTMLAttributes = getRenderedAttributes(node, extensionAttributes)
return addNodeView()({
return Object.fromEntries(
nodeExtensions
.filter(extension => !!getExtensionField(extension, 'addNodeView'))
.map(extension => {
const extensionAttributes = this.attributes.filter(
attribute => attribute.type === extension.name,
)
const context = {
name: extension.name,
options: extension.options,
storage: extension.storage,
editor,
node,
getPos,
decorations,
HTMLAttributes,
type: getNodeType(extension.name, this.schema),
}
const addNodeView = getExtensionField<NodeConfig['addNodeView']>(
extension,
})
}
'addNodeView',
context,
)
return [extension.name, nodeview]
}))
if (!addNodeView) {
return []
}
const nodeview = (
node: ProsemirrorNode,
view: EditorView,
getPos: (() => number) | boolean,
decorations: Decoration[],
) => {
const HTMLAttributes = getRenderedAttributes(node, extensionAttributes)
return addNodeView()({
editor,
node,
getPos,
decorations,
HTMLAttributes,
extension,
})
}
return [extension.name, nodeview]
}),
)
}
}

View File

@ -1,4 +1,4 @@
import { EditorState, Plugin, TextSelection } from 'prosemirror-state'
import { EditorState, Plugin, TextSelection } from '@tiptap/pm/state'
import { CommandManager } from './CommandManager'
import { Editor } from './Editor'
@ -14,46 +14,47 @@ import {
import { isRegExp } from './utilities/isRegExp'
export type InputRuleMatch = {
index: number,
text: string,
replaceWith?: string,
match?: RegExpMatchArray,
data?: Record<string, any>,
index: number
text: string
replaceWith?: string
match?: RegExpMatchArray
data?: Record<string, any>
}
export type InputRuleFinder =
| RegExp
| ((text: string) => InputRuleMatch | null)
export type InputRuleFinder = RegExp | ((text: string) => InputRuleMatch | null)
export class InputRule {
find: InputRuleFinder
handler: (props: {
state: EditorState,
range: Range,
match: ExtendedRegExpMatchArray,
commands: SingleCommands,
chain: () => ChainedCommands,
can: () => CanCommands,
state: EditorState
range: Range
match: ExtendedRegExpMatchArray
commands: SingleCommands
chain: () => ChainedCommands
can: () => CanCommands
}) => void | null
constructor(config: {
find: InputRuleFinder,
find: InputRuleFinder
handler: (props: {
state: EditorState,
range: Range,
match: ExtendedRegExpMatchArray,
commands: SingleCommands,
chain: () => ChainedCommands,
can: () => CanCommands,
}) => void | null,
state: EditorState
range: Range
match: ExtendedRegExpMatchArray
commands: SingleCommands
chain: () => ChainedCommands
can: () => CanCommands
}) => void | null
}) {
this.find = config.find
this.handler = config.handler
}
}
const inputRuleMatcherHandler = (text: string, find: InputRuleFinder): ExtendedRegExpMatchArray | null => {
const inputRuleMatcherHandler = (
text: string,
find: InputRuleFinder,
): ExtendedRegExpMatchArray | null => {
if (isRegExp(find)) {
return find.exec(text)
}
@ -72,7 +73,9 @@ const inputRuleMatcherHandler = (text: string, find: InputRuleFinder): ExtendedR
if (inputRuleMatch.replaceWith) {
if (!inputRuleMatch.text.includes(inputRuleMatch.replaceWith)) {
console.warn('[tiptap warn]: "inputRuleMatch.replaceWith" must be part of "inputRuleMatch.text".')
console.warn(
'[tiptap warn]: "inputRuleMatch.replaceWith" must be part of "inputRuleMatch.text".',
)
}
result.push(inputRuleMatch.replaceWith)
@ -82,20 +85,15 @@ const inputRuleMatcherHandler = (text: string, find: InputRuleFinder): ExtendedR
}
function run(config: {
editor: Editor,
from: number,
to: number,
text: string,
rules: InputRule[],
plugin: Plugin,
editor: Editor
from: number
to: number
text: string
rules: InputRule[]
plugin: Plugin
}): boolean {
const {
editor,
from,
to,
text,
rules,
plugin,
editor, from, to, text, rules, plugin,
} = config
const { view } = editor
@ -179,7 +177,7 @@ function run(config: {
* input that matches any of the given rules to trigger the rules
* action.
*/
export function inputRulesPlugin(props: { editor: Editor, rules: InputRule[] }): Plugin {
export function inputRulesPlugin(props: { editor: Editor; rules: InputRule[] }): Plugin {
const { editor, rules } = props
const plugin = new Plugin({
state: {
@ -193,9 +191,7 @@ export function inputRulesPlugin(props: { editor: Editor, rules: InputRule[] }):
return stored
}
return tr.selectionSet || tr.docChanged
? null
: prev
return tr.selectionSet || tr.docChanged ? null : prev
},
},

View File

@ -1,10 +1,7 @@
import {
DOMOutputSpec,
Mark as ProseMirrorMark,
MarkSpec,
MarkType,
} from 'prosemirror-model'
import { Plugin, Transaction } from 'prosemirror-state'
DOMOutputSpec, Mark as ProseMirrorMark, MarkSpec, MarkType,
} from '@tiptap/pm/model'
import { Plugin, Transaction } from '@tiptap/pm/state'
import { MarkConfig } from '.'
import { Editor } from './Editor'
@ -26,358 +23,386 @@ import { mergeDeep } from './utilities/mergeDeep'
declare module '@tiptap/core' {
export interface MarkConfig<Options = any, Storage = any> {
[key: string]: any;
[key: string]: any
/**
* Name
*/
name: string,
name: string
/**
* Priority
*/
priority?: number,
priority?: number
/**
* Default options
*/
defaultOptions?: Options,
defaultOptions?: Options
/**
* Default Options
*/
addOptions?: (this: {
name: string,
parent: Exclude<ParentConfig<MarkConfig<Options, Storage>>['addOptions'], undefined>,
}) => Options,
name: string
parent: Exclude<ParentConfig<MarkConfig<Options, Storage>>['addOptions'], undefined>
}) => Options
/**
* Default Storage
*/
addStorage?: (this: {
name: string,
options: Options,
parent: Exclude<ParentConfig<MarkConfig<Options, Storage>>['addStorage'], undefined>,
}) => Storage,
name: string
options: Options
parent: Exclude<ParentConfig<MarkConfig<Options, Storage>>['addStorage'], undefined>
}) => Storage
/**
* Global attributes
*/
addGlobalAttributes?: (this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['addGlobalAttributes'],
}) => GlobalAttributes | {},
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['addGlobalAttributes']
}) => GlobalAttributes | {}
/**
* Raw
*/
addCommands?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['addCommands'],
}) => Partial<RawCommands>,
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['addCommands']
}) => Partial<RawCommands>
/**
* Keyboard shortcuts
*/
addKeyboardShortcuts?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['addKeyboardShortcuts'],
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['addKeyboardShortcuts']
}) => {
[key: string]: KeyboardShortcutCommand,
},
[key: string]: KeyboardShortcutCommand
}
/**
* Input rules
*/
addInputRules?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['addInputRules'],
}) => InputRule[],
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['addInputRules']
}) => InputRule[]
/**
* Paste rules
*/
addPasteRules?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['addPasteRules'],
}) => PasteRule[],
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['addPasteRules']
}) => PasteRule[]
/**
* ProseMirror plugins
*/
addProseMirrorPlugins?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['addProseMirrorPlugins'],
}) => Plugin[],
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['addProseMirrorPlugins']
}) => Plugin[]
/**
* Extensions
*/
addExtensions?: (this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['addExtensions'],
}) => Extensions,
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['addExtensions']
}) => Extensions
/**
* Extend Node Schema
*/
extendNodeSchema?: ((
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['extendNodeSchema'],
},
extension: Node,
) => Record<string, any>) | null,
extendNodeSchema?:
| ((
this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['extendNodeSchema']
},
extension: Node,
) => Record<string, any>)
| null
/**
* Extend Mark Schema
*/
extendMarkSchema?: ((
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['extendMarkSchema'],
},
extension: Mark,
) => Record<string, any>) | null,
extendMarkSchema?:
| ((
this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['extendMarkSchema']
},
extension: Mark,
) => Record<string, any>)
| null
/**
* The editor is not ready yet.
*/
onBeforeCreate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['onBeforeCreate'],
}) => void) | null,
onBeforeCreate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['onBeforeCreate']
}) => void)
| null
/**
* The editor is ready.
*/
onCreate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['onCreate'],
}) => void) | null,
onCreate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['onCreate']
}) => void)
| null
/**
* The content has changed.
*/
onUpdate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['onUpdate'],
}) => void) | null,
onUpdate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['onUpdate']
}) => void)
| null
/**
* The selection has changed.
*/
onSelectionUpdate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['onSelectionUpdate'],
}) => void) | null,
onSelectionUpdate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['onSelectionUpdate']
}) => void)
| null
/**
* The editor state has changed.
*/
onTransaction?: ((
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['onTransaction'],
},
props: {
transaction: Transaction,
},
) => void) | null,
onTransaction?:
| ((
this: {
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['onTransaction']
},
props: {
transaction: Transaction
},
) => void)
| null
/**
* The editor is focused.
*/
onFocus?: ((
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['onFocus'],
},
props: {
event: FocusEvent,
},
) => void) | null,
onFocus?:
| ((
this: {
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['onFocus']
},
props: {
event: FocusEvent
},
) => void)
| null
/**
* The editor isnt focused anymore.
*/
onBlur?: ((
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['onBlur'],
},
props: {
event: FocusEvent,
},
) => void) | null,
onBlur?:
| ((
this: {
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['onBlur']
},
props: {
event: FocusEvent
},
) => void)
| null
/**
* The editor is destroyed.
*/
onDestroy?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options, Storage>>['onDestroy'],
}) => void) | null,
onDestroy?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: MarkType
parent: ParentConfig<MarkConfig<Options, Storage>>['onDestroy']
}) => void)
| null
/**
* Keep mark after split node
*/
keepOnSplit?: boolean | (() => boolean),
keepOnSplit?: boolean | (() => boolean)
/**
* Inclusive
*/
inclusive?: MarkSpec['inclusive'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['inclusive'],
}) => MarkSpec['inclusive']),
inclusive?:
| MarkSpec['inclusive']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['inclusive']
}) => MarkSpec['inclusive'])
/**
* Excludes
*/
excludes?: MarkSpec['excludes'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['excludes'],
}) => MarkSpec['excludes']),
excludes?:
| MarkSpec['excludes']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['excludes']
}) => MarkSpec['excludes'])
/**
* Marks this Mark as exitable
*/
exitable?: boolean | (() => boolean),
exitable?: boolean | (() => boolean)
/**
* Group
*/
group?: MarkSpec['group'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['group'],
}) => MarkSpec['group']),
group?:
| MarkSpec['group']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['group']
}) => MarkSpec['group'])
/**
* Spanning
*/
spanning?: MarkSpec['spanning'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['spanning'],
}) => MarkSpec['spanning']),
spanning?:
| MarkSpec['spanning']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['spanning']
}) => MarkSpec['spanning'])
/**
* Code
*/
code?: boolean | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['code'],
}) => boolean),
code?:
| boolean
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['code']
}) => boolean)
/**
* Parse HTML
*/
parseHTML?: (
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['parseHTML'],
},
) => MarkSpec['parseDOM'],
parseHTML?: (this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['parseHTML']
}) => MarkSpec['parseDOM']
/**
* Render HTML
*/
renderHTML?: ((
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['renderHTML'],
},
props: {
mark: ProseMirrorMark,
HTMLAttributes: Record<string, any>,
},
) => DOMOutputSpec) | null,
renderHTML?:
| ((
this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['renderHTML']
},
props: {
mark: ProseMirrorMark
HTMLAttributes: Record<string, any>
},
) => DOMOutputSpec)
| null
/**
* Attributes
*/
addAttributes?: (
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['addAttributes'],
},
) => Attributes | {},
addAttributes?: (this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<MarkConfig<Options, Storage>>['addAttributes']
}) => Attributes | {}
}
}
@ -408,30 +433,28 @@ export class Mark<Options = any, Storage = any> {
this.name = this.config.name
if (config.defaultOptions) {
console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`)
console.warn(
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`,
)
}
// TODO: remove `addOptions` fallback
this.options = this.config.defaultOptions
if (this.config.addOptions) {
this.options = callOrReturn(getExtensionField<AnyConfig['addOptions']>(
this,
'addOptions',
{
this.options = callOrReturn(
getExtensionField<AnyConfig['addOptions']>(this, 'addOptions', {
name: this.name,
},
))
}),
)
}
this.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
this,
'addStorage',
{
this.storage = callOrReturn(
getExtensionField<AnyConfig['addStorage']>(this, 'addStorage', {
name: this.name,
options: this.options,
},
)) || {}
}),
) || {}
}
static create<O = any, S = any>(config: Partial<MarkConfig<O, S>> = {}) {
@ -445,60 +468,50 @@ export class Mark<Options = any, Storage = any> {
extension.options = mergeDeep(this.options as Record<string, any>, options) as Options
extension.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
extension,
'addStorage',
{
extension.storage = callOrReturn(
getExtensionField<AnyConfig['addStorage']>(extension, 'addStorage', {
name: extension.name,
options: extension.options,
},
))
}),
)
return extension
}
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig: Partial<MarkConfig<ExtendedOptions, ExtendedStorage>> = {}) {
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(
extendedConfig: Partial<MarkConfig<ExtendedOptions, ExtendedStorage>> = {},
) {
const extension = new Mark<ExtendedOptions, ExtendedStorage>(extendedConfig)
extension.parent = this
this.child = extension
extension.name = extendedConfig.name
? extendedConfig.name
: extension.parent.name
extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name
if (extendedConfig.defaultOptions) {
console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`)
console.warn(
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`,
)
}
extension.options = callOrReturn(getExtensionField<AnyConfig['addOptions']>(
extension,
'addOptions',
{
extension.options = callOrReturn(
getExtensionField<AnyConfig['addOptions']>(extension, 'addOptions', {
name: extension.name,
},
))
}),
)
extension.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
extension,
'addStorage',
{
extension.storage = callOrReturn(
getExtensionField<AnyConfig['addStorage']>(extension, 'addStorage', {
name: extension.name,
options: extension.options,
},
))
}),
)
return extension
}
static handleExit({
editor,
mark,
}: {
editor: Editor
mark: Mark
}) {
static handleExit({ editor, mark }: { editor: Editor; mark: Mark }) {
const { tr } = editor.state
const currentPos = editor.state.selection.$from
const isAtEnd = currentPos.pos === currentPos.end()

View File

@ -1,10 +1,7 @@
import {
DOMOutputSpec,
Node as ProseMirrorNode,
NodeSpec,
NodeType,
} from 'prosemirror-model'
import { Plugin, Transaction } from 'prosemirror-state'
DOMOutputSpec, Node as ProseMirrorNode, NodeSpec, NodeType,
} from '@tiptap/pm/model'
import { Plugin, Transaction } from '@tiptap/pm/state'
import { NodeConfig } from '.'
import { Editor } from './Editor'
@ -26,443 +23,487 @@ import { mergeDeep } from './utilities/mergeDeep'
declare module '@tiptap/core' {
interface NodeConfig<Options = any, Storage = any> {
[key: string]: any;
[key: string]: any
/**
* Name
*/
name: string,
name: string
/**
* Priority
*/
priority?: number,
priority?: number
/**
* Default options
*/
defaultOptions?: Options,
defaultOptions?: Options
/**
* Default Options
*/
addOptions?: (this: {
name: string,
parent: Exclude<ParentConfig<NodeConfig<Options, Storage>>['addOptions'], undefined>,
}) => Options,
name: string
parent: Exclude<ParentConfig<NodeConfig<Options, Storage>>['addOptions'], undefined>
}) => Options
/**
* Default Storage
*/
addStorage?: (this: {
name: string,
options: Options,
parent: Exclude<ParentConfig<NodeConfig<Options, Storage>>['addStorage'], undefined>,
}) => Storage,
name: string
options: Options
parent: Exclude<ParentConfig<NodeConfig<Options, Storage>>['addStorage'], undefined>
}) => Storage
/**
* Global attributes
*/
addGlobalAttributes?: (this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['addGlobalAttributes'],
}) => GlobalAttributes | {},
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['addGlobalAttributes']
}) => GlobalAttributes | {}
/**
* Raw
*/
addCommands?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['addCommands'],
}) => Partial<RawCommands>,
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['addCommands']
}) => Partial<RawCommands>
/**
* Keyboard shortcuts
*/
addKeyboardShortcuts?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['addKeyboardShortcuts'],
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['addKeyboardShortcuts']
}) => {
[key: string]: KeyboardShortcutCommand,
},
[key: string]: KeyboardShortcutCommand
}
/**
* Input rules
*/
addInputRules?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['addInputRules'],
}) => InputRule[],
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['addInputRules']
}) => InputRule[]
/**
* Paste rules
*/
addPasteRules?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['addPasteRules'],
}) => PasteRule[],
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['addPasteRules']
}) => PasteRule[]
/**
* ProseMirror plugins
*/
addProseMirrorPlugins?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['addProseMirrorPlugins'],
}) => Plugin[],
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['addProseMirrorPlugins']
}) => Plugin[]
/**
* Extensions
*/
addExtensions?: (this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['addExtensions'],
}) => Extensions,
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['addExtensions']
}) => Extensions
/**
* Extend Node Schema
*/
extendNodeSchema?: ((
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['extendNodeSchema'],
},
extension: Node,
) => Record<string, any>) | null,
extendNodeSchema?:
| ((
this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['extendNodeSchema']
},
extension: Node,
) => Record<string, any>)
| null
/**
* Extend Mark Schema
*/
extendMarkSchema?: ((
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['extendMarkSchema'],
},
extension: Node,
) => Record<string, any>) | null,
extendMarkSchema?:
| ((
this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['extendMarkSchema']
},
extension: Node,
) => Record<string, any>)
| null
/**
* The editor is not ready yet.
*/
onBeforeCreate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['onBeforeCreate'],
}) => void) | null,
onBeforeCreate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['onBeforeCreate']
}) => void)
| null
/**
* The editor is ready.
*/
onCreate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['onCreate'],
}) => void) | null,
onCreate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['onCreate']
}) => void)
| null
/**
* The content has changed.
*/
onUpdate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['onUpdate'],
}) => void) | null,
onUpdate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['onUpdate']
}) => void)
| null
/**
* The selection has changed.
*/
onSelectionUpdate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['onSelectionUpdate'],
}) => void) | null,
onSelectionUpdate?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['onSelectionUpdate']
}) => void)
| null
/**
* The editor state has changed.
*/
onTransaction?: ((
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['onTransaction'],
},
props: {
transaction: Transaction,
},
) => void) | null,
onTransaction?:
| ((
this: {
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['onTransaction']
},
props: {
transaction: Transaction
},
) => void)
| null
/**
* The editor is focused.
*/
onFocus?: ((
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['onFocus'],
},
props: {
event: FocusEvent,
},
) => void) | null,
onFocus?:
| ((
this: {
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['onFocus']
},
props: {
event: FocusEvent
},
) => void)
| null
/**
* The editor isnt focused anymore.
*/
onBlur?: ((
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['onBlur'],
},
props: {
event: FocusEvent,
},
) => void) | null,
onBlur?:
| ((
this: {
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['onBlur']
},
props: {
event: FocusEvent
},
) => void)
| null
/**
* The editor is destroyed.
*/
onDestroy?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['onDestroy'],
}) => void) | null,
onDestroy?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['onDestroy']
}) => void)
| null
/**
* Node View
*/
addNodeView?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options, Storage>>['addNodeView'],
}) => NodeViewRenderer) | null,
addNodeView?:
| ((this: {
name: string
options: Options
storage: Storage
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['addNodeView']
}) => NodeViewRenderer)
| null
/**
* TopNode
*/
topNode?: boolean,
topNode?: boolean
/**
* Content
*/
content?: NodeSpec['content'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['content'],
}) => NodeSpec['content']),
content?:
| NodeSpec['content']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['content']
}) => NodeSpec['content'])
/**
* Marks
*/
marks?: NodeSpec['marks'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['marks'],
}) => NodeSpec['marks']),
marks?:
| NodeSpec['marks']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['marks']
}) => NodeSpec['marks'])
/**
* Group
*/
group?: NodeSpec['group'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['group'],
}) => NodeSpec['group']),
group?:
| NodeSpec['group']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['group']
}) => NodeSpec['group'])
/**
* Inline
*/
inline?: NodeSpec['inline'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['inline'],
}) => NodeSpec['inline']),
inline?:
| NodeSpec['inline']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['inline']
}) => NodeSpec['inline'])
/**
* Atom
*/
atom?: NodeSpec['atom'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['atom'],
}) => NodeSpec['atom']),
atom?:
| NodeSpec['atom']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['atom']
}) => NodeSpec['atom'])
/**
* Selectable
*/
selectable?: NodeSpec['selectable'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['selectable'],
}) => NodeSpec['selectable']),
selectable?:
| NodeSpec['selectable']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['selectable']
}) => NodeSpec['selectable'])
/**
* Draggable
*/
draggable?: NodeSpec['draggable'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['draggable'],
}) => NodeSpec['draggable']),
draggable?:
| NodeSpec['draggable']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['draggable']
}) => NodeSpec['draggable'])
/**
* Code
*/
code?: NodeSpec['code'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['code'],
}) => NodeSpec['code']),
code?:
| NodeSpec['code']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['code']
}) => NodeSpec['code'])
/**
* Whitespace
*/
whitespace?: NodeSpec['whitespace'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['whitespace'],
}) => NodeSpec['whitespace']),
whitespace?:
| NodeSpec['whitespace']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['whitespace']
}) => NodeSpec['whitespace'])
/**
* Defining
*/
defining?: NodeSpec['defining'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['defining'],
}) => NodeSpec['defining']),
defining?:
| NodeSpec['defining']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['defining']
}) => NodeSpec['defining'])
/**
* Isolating
*/
isolating?: NodeSpec['isolating'] | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['isolating'],
}) => NodeSpec['isolating']),
isolating?:
| NodeSpec['isolating']
| ((this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['isolating']
}) => NodeSpec['isolating'])
/**
* Parse HTML
*/
parseHTML?: (
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['parseHTML'],
},
) => NodeSpec['parseDOM'],
parseHTML?: (this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['parseHTML']
}) => NodeSpec['parseDOM']
/**
* Render HTML
*/
renderHTML?: ((
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['renderHTML'],
},
props: {
node: ProseMirrorNode,
HTMLAttributes: Record<string, any>,
}
) => DOMOutputSpec) | null,
renderHTML?:
| ((
this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['renderHTML']
},
props: {
node: ProseMirrorNode
HTMLAttributes: Record<string, any>
},
) => DOMOutputSpec)
| null
/**
* Render Text
*/
renderText?: ((
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['renderText'],
},
props: {
node: ProseMirrorNode,
pos: number,
parent: ProseMirrorNode,
index: number,
}
) => string) | null,
renderText?:
| ((
this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['renderText']
},
props: {
node: ProseMirrorNode
pos: number
parent: ProseMirrorNode
index: number
},
) => string)
| null
/**
* Add Attributes
*/
addAttributes?: (
this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['addAttributes'],
},
) => Attributes | {},
addAttributes?: (this: {
name: string
options: Options
storage: Storage
parent: ParentConfig<NodeConfig<Options, Storage>>['addAttributes']
}) => Attributes | {}
}
}
@ -493,30 +534,28 @@ export class Node<Options = any, Storage = any> {
this.name = this.config.name
if (config.defaultOptions) {
console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`)
console.warn(
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`,
)
}
// TODO: remove `addOptions` fallback
this.options = this.config.defaultOptions
if (this.config.addOptions) {
this.options = callOrReturn(getExtensionField<AnyConfig['addOptions']>(
this,
'addOptions',
{
this.options = callOrReturn(
getExtensionField<AnyConfig['addOptions']>(this, 'addOptions', {
name: this.name,
},
))
}),
)
}
this.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
this,
'addStorage',
{
this.storage = callOrReturn(
getExtensionField<AnyConfig['addStorage']>(this, 'addStorage', {
name: this.name,
options: this.options,
},
)) || {}
}),
) || {}
}
static create<O = any, S = any>(config: Partial<NodeConfig<O, S>> = {}) {
@ -530,49 +569,45 @@ export class Node<Options = any, Storage = any> {
extension.options = mergeDeep(this.options as Record<string, any>, options) as Options
extension.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
extension,
'addStorage',
{
extension.storage = callOrReturn(
getExtensionField<AnyConfig['addStorage']>(extension, 'addStorage', {
name: extension.name,
options: extension.options,
},
))
}),
)
return extension
}
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig: Partial<NodeConfig<ExtendedOptions, ExtendedStorage>> = {}) {
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(
extendedConfig: Partial<NodeConfig<ExtendedOptions, ExtendedStorage>> = {},
) {
const extension = new Node<ExtendedOptions, ExtendedStorage>(extendedConfig)
extension.parent = this
this.child = extension
extension.name = extendedConfig.name
? extendedConfig.name
: extension.parent.name
extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name
if (extendedConfig.defaultOptions) {
console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`)
console.warn(
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`,
)
}
extension.options = callOrReturn(getExtensionField<AnyConfig['addOptions']>(
extension,
'addOptions',
{
extension.options = callOrReturn(
getExtensionField<AnyConfig['addOptions']>(extension, 'addOptions', {
name: extension.name,
},
))
}),
)
extension.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
extension,
'addStorage',
{
extension.storage = callOrReturn(
getExtensionField<AnyConfig['addStorage']>(extension, 'addStorage', {
name: extension.name,
options: extension.options,
},
))
}),
)
return extension
}

View File

@ -1,6 +1,6 @@
import { Node as ProseMirrorNode } from 'prosemirror-model'
import { NodeSelection } from 'prosemirror-state'
import { Decoration, NodeView as ProseMirrorNodeView } from 'prosemirror-view'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
import { NodeSelection } from '@tiptap/pm/state'
import { Decoration, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
import { Editor as CoreEditor } from './Editor'
import { Node } from './Node'
@ -12,7 +12,6 @@ export class NodeView<
Editor extends CoreEditor = CoreEditor,
Options extends NodeViewRendererOptions = NodeViewRendererOptions,
> implements ProseMirrorNodeView {
component: Component
editor: Editor
@ -59,7 +58,7 @@ export class NodeView<
onDragStart(event: DragEvent) {
const { view } = this.editor
const target = (event.target as HTMLElement)
const target = event.target as HTMLElement
// get the drag handle element
// `closest` is not available for text nodes so we may have to use its parent
@ -67,11 +66,7 @@ export class NodeView<
? target.parentElement?.closest('[data-drag-handle]')
: target.closest('[data-drag-handle]')
if (
!this.dom
|| this.contentDOM?.contains(target)
|| !dragHandle
) {
if (!this.dom || this.contentDOM?.contains(target) || !dragHandle) {
return
}
@ -110,7 +105,7 @@ export class NodeView<
return this.options.stopEvent({ event })
}
const target = (event.target as HTMLElement)
const target = event.target as HTMLElement
const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target)
// any event from child nodes should be handled by ProseMirror
@ -119,8 +114,7 @@ export class NodeView<
}
const isDropEvent = event.type === 'drop'
const isInput = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA'].includes(target.tagName)
|| target.isContentEditable
const isInput = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA'].includes(target.tagName) || target.isContentEditable
// any input event within node views should be ignored by ProseMirror
if (isInput && !isDropEvent) {
@ -152,19 +146,26 @@ export class NodeView<
// we have to store that dragging started
if (isDraggable && isEditable && !isDragging && isClickEvent) {
const dragHandle = target.closest('[data-drag-handle]')
const isValidDragHandle = dragHandle
&& (this.dom === dragHandle || (this.dom.contains(dragHandle)))
const isValidDragHandle = dragHandle && (this.dom === dragHandle || this.dom.contains(dragHandle))
if (isValidDragHandle) {
this.isDragging = true
document.addEventListener('dragend', () => {
this.isDragging = false
}, { once: true })
document.addEventListener(
'dragend',
() => {
this.isDragging = false
},
{ once: true },
)
document.addEventListener('mouseup', () => {
this.isDragging = false
}, { once: true })
document.addEventListener(
'mouseup',
() => {
this.isDragging = false
},
{ once: true },
)
}
}
@ -183,7 +184,7 @@ export class NodeView<
return true
}
ignoreMutation(mutation: MutationRecord | { type: 'selection', target: Element }) {
ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) {
if (!this.dom || !this.contentDOM) {
return true
}

View File

@ -1,4 +1,4 @@
import { EditorState, Plugin } from 'prosemirror-state'
import { EditorState, Plugin } from '@tiptap/pm/state'
import { CommandManager } from './CommandManager'
import { Editor } from './Editor'
@ -14,46 +14,47 @@ import { isNumber } from './utilities/isNumber'
import { isRegExp } from './utilities/isRegExp'
export type PasteRuleMatch = {
index: number,
text: string,
replaceWith?: string,
match?: RegExpMatchArray,
data?: Record<string, any>,
index: number
text: string
replaceWith?: string
match?: RegExpMatchArray
data?: Record<string, any>
}
export type PasteRuleFinder =
| RegExp
| ((text: string) => PasteRuleMatch[] | null | undefined)
export type PasteRuleFinder = RegExp | ((text: string) => PasteRuleMatch[] | null | undefined)
export class PasteRule {
find: PasteRuleFinder
handler: (props: {
state: EditorState,
range: Range,
match: ExtendedRegExpMatchArray,
commands: SingleCommands,
chain: () => ChainedCommands,
can: () => CanCommands,
state: EditorState
range: Range
match: ExtendedRegExpMatchArray
commands: SingleCommands
chain: () => ChainedCommands
can: () => CanCommands
}) => void | null
constructor(config: {
find: PasteRuleFinder,
find: PasteRuleFinder
handler: (props: {
state: EditorState,
range: Range,
match: ExtendedRegExpMatchArray,
commands: SingleCommands,
chain: () => ChainedCommands,
can: () => CanCommands,
}) => void | null,
state: EditorState
range: Range
match: ExtendedRegExpMatchArray
commands: SingleCommands
chain: () => ChainedCommands
can: () => CanCommands
}) => void | null
}) {
this.find = config.find
this.handler = config.handler
}
}
const pasteRuleMatcherHandler = (text: string, find: PasteRuleFinder): ExtendedRegExpMatchArray[] => {
const pasteRuleMatcherHandler = (
text: string,
find: PasteRuleFinder,
): ExtendedRegExpMatchArray[] => {
if (isRegExp(find)) {
return [...text.matchAll(find)]
}
@ -73,7 +74,9 @@ const pasteRuleMatcherHandler = (text: string, find: PasteRuleFinder): ExtendedR
if (pasteRuleMatch.replaceWith) {
if (!pasteRuleMatch.text.includes(pasteRuleMatch.replaceWith)) {
console.warn('[tiptap warn]: "pasteRuleMatch.replaceWith" must be part of "pasteRuleMatch.text".')
console.warn(
'[tiptap warn]: "pasteRuleMatch.replaceWith" must be part of "pasteRuleMatch.text".',
)
}
result.push(pasteRuleMatch.replaceWith)
@ -84,18 +87,14 @@ const pasteRuleMatcherHandler = (text: string, find: PasteRuleFinder): ExtendedR
}
function run(config: {
editor: Editor,
state: EditorState,
from: number,
to: number,
rule: PasteRule,
editor: Editor
state: EditorState
from: number
to: number
rule: PasteRule
}): boolean {
const {
editor,
state,
from,
to,
rule,
editor, state, from, to, rule,
} = config
const { commands, chain, can } = new CommandManager({
@ -112,12 +111,7 @@ function run(config: {
const resolvedFrom = Math.max(from, pos)
const resolvedTo = Math.min(to, pos + node.content.size)
const textToMatch = node.textBetween(
resolvedFrom - pos,
resolvedTo - pos,
undefined,
'\ufffc',
)
const textToMatch = node.textBetween(resolvedFrom - pos, resolvedTo - pos, undefined, '\ufffc')
const matches = pasteRuleMatcherHandler(textToMatch, rule.find)
@ -156,7 +150,7 @@ function run(config: {
* text that matches any of the given rules to trigger the rules
* action.
*/
export function pasteRulesPlugin(props: { editor: Editor, rules: PasteRule[] }): Plugin[] {
export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }): Plugin[] {
const { editor, rules } = props
let dragSourceElement: Element | null = null
let isPastedFromProseMirror = false

View File

@ -1,12 +1,11 @@
import { Transaction } from 'prosemirror-state'
import { Transaction } from '@tiptap/pm/state'
export interface TrackerResult {
position: number,
deleted: boolean,
position: number
deleted: boolean
}
export class Tracker {
transaction: Transaction
currentStep: number
@ -22,9 +21,7 @@ export class Tracker {
const mappedPosition = this.transaction.steps
.slice(this.currentStep)
.reduce((newPosition, step) => {
const mapResult = step
.getMap()
.mapResult(newPosition)
const mapResult = step.getMap().mapResult(newPosition)
if (mapResult.deleted) {
deleted = true
@ -38,5 +35,4 @@ export class Tracker {
deleted,
}
}
}

View File

@ -1,4 +1,4 @@
import { liftTarget } from 'prosemirror-transform'
import { liftTarget } from '@tiptap/pm/transform'
import { RawCommands } from '../types'

View File

@ -1,4 +1,4 @@
import { createParagraphNear as originalCreateParagraphNear } from 'prosemirror-commands'
import { createParagraphNear as originalCreateParagraphNear } from '@tiptap/pm/commands'
import { RawCommands } from '../types'
@ -8,7 +8,7 @@ declare module '@tiptap/core' {
/**
* Create a paragraph nearby.
*/
createParagraphNear: () => ReturnType,
createParagraphNear: () => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { NodeType } from 'prosemirror-model'
import { NodeType } from '@tiptap/pm/model'
import { getNodeType } from '../helpers/getNodeType'
import { RawCommands } from '../types'

View File

@ -1,4 +1,4 @@
import { deleteSelection as originalDeleteSelection } from 'prosemirror-commands'
import { deleteSelection as originalDeleteSelection } from '@tiptap/pm/commands'
import { RawCommands } from '../types'
@ -8,7 +8,7 @@ declare module '@tiptap/core' {
/**
* Delete the selection, if there is one.
*/
deleteSelection: () => ReturnType,
deleteSelection: () => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { exitCode as originalExitCode } from 'prosemirror-commands'
import { exitCode as originalExitCode } from '@tiptap/pm/commands'
import { RawCommands } from '../types'
@ -8,7 +8,7 @@ declare module '@tiptap/core' {
/**
* Exit from a code block.
*/
exitCode: () => ReturnType,
exitCode: () => ReturnType
}
}
}

View File

@ -1,5 +1,5 @@
import { MarkType } from 'prosemirror-model'
import { TextSelection } from 'prosemirror-state'
import { MarkType } from '@tiptap/pm/model'
import { TextSelection } from '@tiptap/pm/state'
import { getMarkRange } from '../helpers/getMarkRange'
import { getMarkType } from '../helpers/getMarkType'
@ -11,7 +11,10 @@ declare module '@tiptap/core' {
/**
* Extends the text selection to the current mark.
*/
extendMarkRange: (typeOrName: string | MarkType, attributes?: Record<string, any>) => ReturnType,
extendMarkRange: (
typeOrName: string | MarkType,
attributes?: Record<string, any>,
) => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { ParseOptions } from 'prosemirror-model'
import { ParseOptions } from '@tiptap/pm/model'
import { Content, RawCommands } from '../types'
@ -11,14 +11,18 @@ declare module '@tiptap/core' {
insertContent: (
value: Content,
options?: {
parseOptions?: ParseOptions,
updateSelection?: boolean,
parseOptions?: ParseOptions
updateSelection?: boolean
},
) => ReturnType,
) => ReturnType
}
}
}
export const insertContent: RawCommands['insertContent'] = (value, options) => ({ tr, commands }) => {
return commands.insertContentAt({ from: tr.selection.from, to: tr.selection.to }, value, options)
return commands.insertContentAt(
{ from: tr.selection.from, to: tr.selection.to },
value,
options,
)
}

View File

@ -1,12 +1,8 @@
import { Fragment, Node as ProseMirrorNode, ParseOptions } from 'prosemirror-model'
import { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model'
import { createNodeFromContent } from '../helpers/createNodeFromContent'
import { selectionToInsertionEnd } from '../helpers/selectionToInsertionEnd'
import {
Content,
Range,
RawCommands,
} from '../types'
import { Content, Range, RawCommands } from '../types'
declare module '@tiptap/core' {
interface Commands<ReturnType> {
@ -18,10 +14,10 @@ declare module '@tiptap/core' {
position: number | Range,
value: Content,
options?: {
parseOptions?: ParseOptions,
updateSelection?: boolean,
parseOptions?: ParseOptions
updateSelection?: boolean
},
) => ReturnType,
) => ReturnType
}
}
}
@ -50,27 +46,19 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
return true
}
let { from, to } = typeof position === 'number'
? { from: position, to: position }
: position
let { from, to } = typeof position === 'number' ? { from: position, to: position } : position
let isOnlyTextContent = true
let isOnlyBlockContent = true
const nodes = isFragment(content)
? content
: [content]
const nodes = isFragment(content) ? content : [content]
nodes.forEach(node => {
// check if added node is valid
node.check()
isOnlyTextContent = isOnlyTextContent
? node.isText && node.marks.length === 0
: false
isOnlyTextContent = isOnlyTextContent ? node.isText && node.marks.length === 0 : false
isOnlyBlockContent = isOnlyBlockContent
? node.isBlock
: false
isOnlyBlockContent = isOnlyBlockContent ? node.isBlock : false
})
// check if we can replace the wrapping node by
@ -80,9 +68,7 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
// instead of inserting the image below the paragraph
if (from === to && isOnlyBlockContent) {
const { parent } = tr.doc.resolve(from)
const isEmptyTextBlock = parent.isTextblock
&& !parent.type.spec.code
&& !parent.childCount
const isEmptyTextBlock = parent.isTextblock && !parent.type.spec.code && !parent.childCount
if (isEmptyTextBlock) {
from -= 1

View File

@ -1,6 +1,9 @@
import {
joinBackward as originalJoinBackward, joinDown as originalJoinDown, joinForward as originalJoinForward, joinUp as originalJoinUp,
} from 'prosemirror-commands'
joinBackward as originalJoinBackward,
joinDown as originalJoinDown,
joinForward as originalJoinForward,
joinUp as originalJoinUp,
} from '@tiptap/pm/commands'
import { RawCommands } from '../types'
@ -10,25 +13,25 @@ declare module '@tiptap/core' {
/**
* Join two nodes Up.
*/
joinUp: () => ReturnType,
joinUp: () => ReturnType
}
joinDown: {
/**
* Join two nodes Down.
*/
joinDown: () => ReturnType,
joinDown: () => ReturnType
}
joinBackward: {
/**
* Join two nodes Backwards.
*/
joinBackward: () => ReturnType,
joinBackward: () => ReturnType
}
joinForward: {
/**
* Join two nodes Forwards.
*/
joinForward: () => ReturnType,
joinForward: () => ReturnType
}
}
}

View File

@ -1,5 +1,5 @@
import { lift as originalLift } from 'prosemirror-commands'
import { NodeType } from 'prosemirror-model'
import { lift as originalLift } from '@tiptap/pm/commands'
import { NodeType } from '@tiptap/pm/model'
import { getNodeType } from '../helpers/getNodeType'
import { isNodeActive } from '../helpers/isNodeActive'
@ -11,7 +11,7 @@ declare module '@tiptap/core' {
/**
* Removes an existing wrap.
*/
lift: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType,
lift: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { liftEmptyBlock as originalLiftEmptyBlock } from 'prosemirror-commands'
import { liftEmptyBlock as originalLiftEmptyBlock } from '@tiptap/pm/commands'
import { RawCommands } from '../types'

View File

@ -1,5 +1,5 @@
import { NodeType } from 'prosemirror-model'
import { liftListItem as originalLiftListItem } from 'prosemirror-schema-list'
import { NodeType } from '@tiptap/pm/model'
import { liftListItem as originalLiftListItem } from '@tiptap/pm/schema-list'
import { getNodeType } from '../helpers/getNodeType'
import { RawCommands } from '../types'
@ -10,7 +10,7 @@ declare module '@tiptap/core' {
/**
* Lift the list item into a wrapping list.
*/
liftListItem: (typeOrName: string | NodeType) => ReturnType,
liftListItem: (typeOrName: string | NodeType) => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { newlineInCode as originalNewlineInCode } from 'prosemirror-commands'
import { newlineInCode as originalNewlineInCode } from '@tiptap/pm/commands'
import { RawCommands } from '../types'
@ -8,7 +8,7 @@ declare module '@tiptap/core' {
/**
* Add a newline character in code.
*/
newlineInCode: () => ReturnType,
newlineInCode: () => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { MarkType, NodeType } from 'prosemirror-model'
import { MarkType, NodeType } from '@tiptap/pm/model'
import { getMarkType } from '../helpers/getMarkType'
import { getNodeType } from '../helpers/getNodeType'
@ -12,7 +12,10 @@ declare module '@tiptap/core' {
/**
* Resets some node attributes to the default value.
*/
resetAttributes: (typeOrName: string | NodeType | MarkType, attributes: string | string[]) => ReturnType,
resetAttributes: (
typeOrName: string | NodeType | MarkType,
attributes: string | string[],
) => ReturnType
}
}
}
@ -22,9 +25,7 @@ export const resetAttributes: RawCommands['resetAttributes'] = (typeOrName, attr
let markType: MarkType | null = null
const schemaType = getSchemaTypeNameByName(
typeof typeOrName === 'string'
? typeOrName
: typeOrName.name,
typeof typeOrName === 'string' ? typeOrName : typeOrName.name,
state.schema,
)
@ -50,7 +51,11 @@ export const resetAttributes: RawCommands['resetAttributes'] = (typeOrName, attr
if (markType && node.marks.length) {
node.marks.forEach(mark => {
if (markType === mark.type) {
tr.addMark(pos, pos + node.nodeSize, markType.create(deleteProps(mark.attrs, attributes)))
tr.addMark(
pos,
pos + node.nodeSize,
markType.create(deleteProps(mark.attrs, attributes)),
)
}
})
}

View File

@ -1,4 +1,4 @@
import { selectNodeBackward as originalSelectNodeBackward } from 'prosemirror-commands'
import { selectNodeBackward as originalSelectNodeBackward } from '@tiptap/pm/commands'
import { RawCommands } from '../types'
@ -8,7 +8,7 @@ declare module '@tiptap/core' {
/**
* Select a node backward.
*/
selectNodeBackward: () => ReturnType,
selectNodeBackward: () => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { selectNodeForward as originalSelectNodeForward } from 'prosemirror-commands'
import { selectNodeForward as originalSelectNodeForward } from '@tiptap/pm/commands'
import { RawCommands } from '../types'
@ -8,7 +8,7 @@ declare module '@tiptap/core' {
/**
* Select a node forward.
*/
selectNodeForward: () => ReturnType,
selectNodeForward: () => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { selectParentNode as originalSelectParentNode } from 'prosemirror-commands'
import { selectParentNode as originalSelectParentNode } from '@tiptap/pm/commands'
import { RawCommands } from '../types'
@ -8,7 +8,7 @@ declare module '@tiptap/core' {
/**
* Select the parent node.
*/
selectParentNode: () => ReturnType,
selectParentNode: () => ReturnType
}
}
}

View File

@ -1,6 +1,6 @@
// @ts-ignore
// TODO: add types to @types/prosemirror-commands
import { selectTextblockEnd as originalSelectTextblockEnd } from 'prosemirror-commands'
import { selectTextblockEnd as originalSelectTextblockEnd } from '@tiptap/pm/commands'
import { RawCommands } from '../types'
@ -10,7 +10,7 @@ declare module '@tiptap/core' {
/**
* Moves the cursor to the end of current text block.
*/
selectTextblockEnd: () => ReturnType,
selectTextblockEnd: () => ReturnType
}
}
}

View File

@ -1,6 +1,6 @@
// @ts-ignore
// TODO: add types to @types/prosemirror-commands
import { selectTextblockStart as originalSelectTextblockStart } from 'prosemirror-commands'
import { selectTextblockStart as originalSelectTextblockStart } from '@tiptap/pm/commands'
import { RawCommands } from '../types'
@ -10,7 +10,7 @@ declare module '@tiptap/core' {
/**
* Moves the cursor to the start of current text block.
*/
selectTextblockStart: () => ReturnType,
selectTextblockStart: () => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { ParseOptions } from 'prosemirror-model'
import { ParseOptions } from '@tiptap/pm/model'
import { createDocument } from '../helpers/createDocument'
import { Content, RawCommands } from '../types'
@ -13,7 +13,7 @@ declare module '@tiptap/core' {
content: Content,
emitUpdate?: boolean,
parseOptions?: ParseOptions,
) => ReturnType,
) => ReturnType
}
}
}
@ -23,8 +23,7 @@ export const setContent: RawCommands['setContent'] = (content, emitUpdate = fals
const document = createDocument(content, editor.schema, parseOptions)
if (dispatch) {
tr.replaceWith(0, doc.content.size, document)
.setMeta('preventUpdate', !emitUpdate)
tr.replaceWith(0, doc.content.size, document).setMeta('preventUpdate', !emitUpdate)
}
return true

View File

@ -1,5 +1,5 @@
import { MarkType, ResolvedPos } from 'prosemirror-model'
import { EditorState, Transaction } from 'prosemirror-state'
import { MarkType, ResolvedPos } from '@tiptap/pm/model'
import { EditorState, Transaction } from '@tiptap/pm/state'
import { isTextSelection } from '../helpers'
import { getMarkAttributes } from '../helpers/getMarkAttributes'
@ -12,7 +12,7 @@ declare module '@tiptap/core' {
/**
* Add a mark with new attributes.
*/
setMark: (typeOrName: string | MarkType, attributes?: Record<string, any>) => ReturnType,
setMark: (typeOrName: string | MarkType, attributes?: Record<string, any>) => ReturnType
}
}
}
@ -29,13 +29,18 @@ function canSetMark(state: EditorState, tr: Transaction, newMarkType: MarkType)
const currentMarks = state.storedMarks ?? cursor.marks()
// There can be no current marks that exclude the new mark
return !!newMarkType.isInSet(currentMarks) || !currentMarks.some(mark => mark.type.excludes(newMarkType))
return (
!!newMarkType.isInSet(currentMarks)
|| !currentMarks.some(mark => mark.type.excludes(newMarkType))
)
}
const { ranges } = selection
return ranges.some(({ $from, $to }) => {
let someNodeSupportsMark = $from.depth === 0 ? state.doc.inlineContent && state.doc.type.allowsMarkType(newMarkType) : false
let someNodeSupportsMark = $from.depth === 0
? state.doc.inlineContent && state.doc.type.allowsMarkType(newMarkType)
: false
state.doc.nodesBetween($from.pos, $to.pos, (node, _pos, parent) => {
// If we already found a mark that we can enable, return false to bypass the remaining search
@ -45,7 +50,8 @@ function canSetMark(state: EditorState, tr: Transaction, newMarkType: MarkType)
if (node.isInline) {
const parentAllowsMarkType = !parent || parent.type.allowsMarkType(newMarkType)
const currentMarksAllowMarkType = !!newMarkType.isInSet(node.marks) || !node.marks.some(otherMark => otherMark.type.excludes(newMarkType))
const currentMarksAllowMarkType = !!newMarkType.isInSet(node.marks)
|| !node.marks.some(otherMark => otherMark.type.excludes(newMarkType))
someNodeSupportsMark = parentAllowsMarkType && currentMarksAllowMarkType
}
@ -54,7 +60,6 @@ function canSetMark(state: EditorState, tr: Transaction, newMarkType: MarkType)
return someNodeSupportsMark
})
}
export const setMark: RawCommands['setMark'] = (typeOrName, attributes = {}) => ({ tr, state, dispatch }) => {
const { selection } = tr
@ -65,10 +70,12 @@ export const setMark: RawCommands['setMark'] = (typeOrName, attributes = {}) =>
if (empty) {
const oldAttributes = getMarkAttributes(state, type)
tr.addStoredMark(type.create({
...oldAttributes,
...attributes,
}))
tr.addStoredMark(
type.create({
...oldAttributes,
...attributes,
}),
)
} else {
ranges.forEach(range => {
const from = range.$from.pos
@ -83,13 +90,16 @@ export const setMark: RawCommands['setMark'] = (typeOrName, attributes = {}) =>
// we know that we have to merge its attributes
// otherwise we add a fresh new mark
if (someHasMark) {
node.marks.forEach(mark => {
if (type === mark.type) {
tr.addMark(trimmedFrom, trimmedTo, type.create({
...mark.attrs,
...attributes,
}))
tr.addMark(
trimmedFrom,
trimmedTo,
type.create({
...mark.attrs,
...attributes,
}),
)
}
})
} else {

View File

@ -1,5 +1,5 @@
import { setBlockType } from 'prosemirror-commands'
import { NodeType } from 'prosemirror-model'
import { setBlockType } from '@tiptap/pm/commands'
import { NodeType } from '@tiptap/pm/model'
import { getNodeType } from '../helpers/getNodeType'
import { RawCommands } from '../types'
@ -10,7 +10,7 @@ declare module '@tiptap/core' {
/**
* Replace a given range with a node.
*/
setNode: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType,
setNode: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType
}
}
}
@ -25,19 +25,21 @@ export const setNode: RawCommands['setNode'] = (typeOrName, attributes = {}) =>
return false
}
return chain()
return (
chain()
// try to convert node to default node if needed
.command(({ commands }) => {
const canSetBlock = setBlockType(type, attributes)(state)
.command(({ commands }) => {
const canSetBlock = setBlockType(type, attributes)(state)
if (canSetBlock) {
return true
}
if (canSetBlock) {
return true
}
return commands.clearNodes()
})
.command(({ state: updatedState }) => {
return setBlockType(type, attributes)(updatedState, dispatch)
})
.run()
return commands.clearNodes()
})
.command(({ state: updatedState }) => {
return setBlockType(type, attributes)(updatedState, dispatch)
})
.run()
)
}

View File

@ -1,4 +1,4 @@
import { NodeSelection } from 'prosemirror-state'
import { NodeSelection } from '@tiptap/pm/state'
import { RawCommands } from '../types'
import { minMax } from '../utilities/minMax'
@ -9,7 +9,7 @@ declare module '@tiptap/core' {
/**
* Creates a NodeSelection.
*/
setNodeSelection: (position: number) => ReturnType,
setNodeSelection: (position: number) => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { TextSelection } from 'prosemirror-state'
import { TextSelection } from '@tiptap/pm/state'
import { Range, RawCommands } from '../types'
import { minMax } from '../utilities/minMax'
@ -9,7 +9,7 @@ declare module '@tiptap/core' {
/**
* Creates a TextSelection.
*/
setTextSelection: (position: number | Range) => ReturnType,
setTextSelection: (position: number | Range) => ReturnType
}
}
}
@ -17,9 +17,7 @@ declare module '@tiptap/core' {
export const setTextSelection: RawCommands['setTextSelection'] = position => ({ tr, dispatch }) => {
if (dispatch) {
const { doc } = tr
const { from, to } = typeof position === 'number'
? { from: position, to: position }
: position
const { from, to } = typeof position === 'number' ? { from: position, to: position } : position
const minPos = TextSelection.atStart(doc).from
const maxPos = TextSelection.atEnd(doc).to
const resolvedFrom = minMax(from, minPos, maxPos)

View File

@ -1,5 +1,5 @@
import { NodeType } from 'prosemirror-model'
import { sinkListItem as originalSinkListItem } from 'prosemirror-schema-list'
import { NodeType } from '@tiptap/pm/model'
import { sinkListItem as originalSinkListItem } from '@tiptap/pm/schema-list'
import { getNodeType } from '../helpers/getNodeType'
import { RawCommands } from '../types'
@ -10,7 +10,7 @@ declare module '@tiptap/core' {
/**
* Sink the list item down into an inner list.
*/
sinkListItem: (typeOrName: string | NodeType) => ReturnType,
sinkListItem: (typeOrName: string | NodeType) => ReturnType
}
}
}

View File

@ -1,13 +1,12 @@
import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'
import { canSplit } from 'prosemirror-transform'
import { EditorState, NodeSelection, TextSelection } from '@tiptap/pm/state'
import { canSplit } from '@tiptap/pm/transform'
import { defaultBlockAt } from '../helpers/defaultBlockAt'
import { getSplittedAttributes } from '../helpers/getSplittedAttributes'
import { RawCommands } from '../types'
function ensureMarks(state: EditorState, splittableMarks?: string[]) {
const marks = state.storedMarks
|| (state.selection.$to.parentOffset && state.selection.$from.marks())
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks())
if (marks) {
const filteredMarks = marks.filter(mark => splittableMarks?.includes(mark.type.name))
@ -22,16 +21,13 @@ declare module '@tiptap/core' {
/**
* Forks a new node from an existing node.
*/
splitBlock: (options?: { keepMarks?: boolean }) => ReturnType,
splitBlock: (options?: { keepMarks?: boolean }) => ReturnType
}
}
}
export const splitBlock: RawCommands['splitBlock'] = ({ keepMarks = true } = {}) => ({
tr,
state,
dispatch,
editor,
tr, state, dispatch, editor,
}) => {
const { selection, doc } = tr
const { $from, $to } = selection
@ -74,37 +70,36 @@ export const splitBlock: RawCommands['splitBlock'] = ({ keepMarks = true } = {})
: defaultBlockAt($from.node(-1).contentMatchAt($from.indexAfter(-1)))
let types = atEnd && deflt
? [{
type: deflt,
attrs: newAttributes,
}]
? [
{
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
&& canSplit(tr.doc, tr.mapping.map($from.pos), 1, deflt ? [{ type: deflt }] : undefined)
) {
can = true
types = deflt
? [{
type: deflt,
attrs: newAttributes,
}]
? [
{
type: deflt,
attrs: newAttributes,
},
]
: undefined
}
if (can) {
tr.split(tr.mapping.map($from.pos), 1, types)
if (
deflt
&& !atEnd
&& !$from.parentOffset
&& $from.parent.type !== deflt
) {
if (deflt && !atEnd && !$from.parentOffset && $from.parent.type !== deflt) {
const first = tr.mapping.map($from.before())
const $first = tr.doc.resolve(first)

View File

@ -1,11 +1,8 @@
import {
Fragment,
Node as ProseMirrorNode,
NodeType,
Slice,
} from 'prosemirror-model'
import { TextSelection } from 'prosemirror-state'
import { canSplit } from 'prosemirror-transform'
Fragment, Node as ProseMirrorNode, NodeType, Slice,
} from '@tiptap/pm/model'
import { TextSelection } from '@tiptap/pm/state'
import { canSplit } from '@tiptap/pm/transform'
import { getNodeType } from '../helpers/getNodeType'
import { getSplittedAttributes } from '../helpers/getSplittedAttributes'
@ -17,7 +14,7 @@ declare module '@tiptap/core' {
/**
* Splits one list item into two list items.
*/
splitListItem: (typeOrName: string | NodeType) => ReturnType,
splitListItem: (typeOrName: string | NodeType) => ReturnType
}
}
}
@ -30,7 +27,7 @@ export const splitListItem: RawCommands['splitListItem'] = typeOrName => ({
// @ts-ignore
// eslint-disable-next-line
const node: ProseMirrorNode = state.selection.node
const node: ProseMirrorNode = state.selection.node
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) {
return false
@ -50,8 +47,8 @@ export const splitListItem: RawCommands['splitListItem'] = typeOrName => ({
// command handle lifting.
if (
$from.depth === 2
|| $from.node(-3).type !== type
|| $from.index(-2) !== $from.node(-2).childCount - 1
|| $from.node(-3).type !== type
|| $from.index(-2) !== $from.node(-2).childCount - 1
) {
return false
}
@ -59,11 +56,7 @@ export const splitListItem: RawCommands['splitListItem'] = typeOrName => ({
if (dispatch) {
let wrap = Fragment.empty
// eslint-disable-next-line
const depthBefore = $from.index(-1)
? 1
: $from.index(-2)
? 2
: 3
const depthBefore = $from.index(-1) ? 1 : $from.index(-2) ? 2 : 3
// Build a fragment containing empty versions of the structure
// from the outer list item to the parent node of the cursor
@ -72,11 +65,7 @@ export const splitListItem: RawCommands['splitListItem'] = typeOrName => ({
}
// eslint-disable-next-line
const depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount
? 1
: $from.indexAfter(-2) < $from.node(-3).childCount
? 2
: 3
const depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount ? 1 : $from.indexAfter(-2) < $from.node(-3).childCount ? 2 : 3
// Add a second list item with an empty default start node
const newNextTypeAttributes = getSplittedAttributes(
@ -114,9 +103,7 @@ export const splitListItem: RawCommands['splitListItem'] = typeOrName => ({
return true
}
const nextType = $to.pos === $from.end()
? grandParent.contentMatchAt(0).defaultType
: null
const nextType = $to.pos === $from.end() ? grandParent.contentMatchAt(0).defaultType : null
const newTypeAttributes = getSplittedAttributes(
extensionAttributes,
@ -132,7 +119,10 @@ export const splitListItem: RawCommands['splitListItem'] = typeOrName => ({
tr.delete($from.pos, $to.pos)
const types = nextType
? [{ type, attrs: newTypeAttributes }, { type: nextType, attrs: newNextTypeAttributes }]
? [
{ type, attrs: newTypeAttributes },
{ type: nextType, attrs: newNextTypeAttributes },
]
: [{ type, attrs: newTypeAttributes }]
if (!canSplit(tr.doc, $from.pos, 2)) {

View File

@ -1,6 +1,6 @@
import { NodeType } from 'prosemirror-model'
import { Transaction } from 'prosemirror-state'
import { canJoin } from 'prosemirror-transform'
import { NodeType } from '@tiptap/pm/model'
import { Transaction } from '@tiptap/pm/state'
import { canJoin } from '@tiptap/pm/transform'
import { findParentNode } from '../helpers/findParentNode'
import { getNodeType } from '../helpers/getNodeType'
@ -21,8 +21,7 @@ const joinListBackwards = (tr: Transaction, listType: NodeType): boolean => {
}
const nodeBefore = tr.doc.nodeAt(before)
const canJoinBackwards = list.node.type === nodeBefore?.type
&& canJoin(tr.doc, list.pos)
const canJoinBackwards = list.node.type === nodeBefore?.type && canJoin(tr.doc, list.pos)
if (!canJoinBackwards) {
return true
@ -47,8 +46,7 @@ const joinListForwards = (tr: Transaction, listType: NodeType): boolean => {
}
const nodeAfter = tr.doc.nodeAt(after)
const canJoinForwards = list.node.type === nodeAfter?.type
&& canJoin(tr.doc, after)
const canJoinForwards = list.node.type === nodeAfter?.type && canJoin(tr.doc, after)
if (!canJoinForwards) {
return true
@ -65,7 +63,10 @@ declare module '@tiptap/core' {
/**
* Toggle between different list types.
*/
toggleList: (listTypeOrName: string | NodeType, itemTypeOrName: string | NodeType) => ReturnType,
toggleList: (
listTypeOrName: string | NodeType,
itemTypeOrName: string | NodeType,
) => ReturnType
}
}
}
@ -95,8 +96,8 @@ export const toggleList: RawCommands['toggleList'] = (listTypeOrName, itemTypeOr
// change list type
if (
isList(parentList.node.type.name, extensions)
&& listType.validContent(parentList.node.content)
&& dispatch
&& listType.validContent(parentList.node.content)
&& dispatch
) {
return chain()
.command(() => {
@ -110,19 +111,21 @@ export const toggleList: RawCommands['toggleList'] = (listTypeOrName, itemTypeOr
}
}
return chain()
return (
chain()
// try to convert node to default node if needed
.command(() => {
const canWrapInList = can().wrapInList(listType)
.command(() => {
const canWrapInList = can().wrapInList(listType)
if (canWrapInList) {
return true
}
if (canWrapInList) {
return true
}
return commands.clearNodes()
})
.wrapInList(listType)
.command(() => joinListBackwards(tr, listType))
.command(() => joinListForwards(tr, listType))
.run()
return commands.clearNodes()
})
.wrapInList(listType)
.command(() => joinListBackwards(tr, listType))
.command(() => joinListForwards(tr, listType))
.run()
)
}

View File

@ -1,4 +1,4 @@
import { MarkType } from 'prosemirror-model'
import { MarkType } from '@tiptap/pm/model'
import { getMarkType } from '../helpers/getMarkType'
import { isMarkActive } from '../helpers/isMarkActive'
@ -17,9 +17,9 @@ declare module '@tiptap/core' {
/**
* Removes the mark even across the current selection. Defaults to `false`.
*/
extendEmptyMarkRange?: boolean,
extendEmptyMarkRange?: boolean
},
) => ReturnType,
) => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { NodeType } from 'prosemirror-model'
import { NodeType } from '@tiptap/pm/model'
import { getNodeType } from '../helpers/getNodeType'
import { isNodeActive } from '../helpers/isNodeActive'
@ -10,7 +10,11 @@ declare module '@tiptap/core' {
/**
* Toggle a node with another node.
*/
toggleNode: (typeOrName: string | NodeType, toggleTypeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType,
toggleNode: (
typeOrName: string | NodeType,
toggleTypeOrName: string | NodeType,
attributes?: Record<string, any>,
) => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { NodeType } from 'prosemirror-model'
import { NodeType } from '@tiptap/pm/model'
import { getNodeType } from '../helpers/getNodeType'
import { isNodeActive } from '../helpers/isNodeActive'
@ -10,7 +10,7 @@ declare module '@tiptap/core' {
/**
* Wraps nodes in another node, or removes an existing wrap.
*/
toggleWrap: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType,
toggleWrap: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { MarkType } from 'prosemirror-model'
import { MarkType } from '@tiptap/pm/model'
import { getMarkRange } from '../helpers/getMarkRange'
import { getMarkType } from '../helpers/getMarkType'
@ -16,9 +16,9 @@ declare module '@tiptap/core' {
/**
* Removes the mark even across the current selection. Defaults to `false`.
*/
extendEmptyMarkRange?: boolean,
extendEmptyMarkRange?: boolean
},
) => ReturnType,
) => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { MarkType, NodeType } from 'prosemirror-model'
import { MarkType, NodeType } from '@tiptap/pm/model'
import { getMarkType } from '../helpers/getMarkType'
import { getNodeType } from '../helpers/getNodeType'
@ -11,7 +11,10 @@ declare module '@tiptap/core' {
/**
* Update attributes of a node or mark.
*/
updateAttributes: (typeOrName: string | NodeType | MarkType, attributes: Record<string, any>) => ReturnType,
updateAttributes: (
typeOrName: string | NodeType | MarkType,
attributes: Record<string, any>,
) => ReturnType
}
}
}
@ -21,9 +24,7 @@ export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, at
let markType: MarkType | null = null
const schemaType = getSchemaTypeNameByName(
typeof typeOrName === 'string'
? typeOrName
: typeOrName.name,
typeof typeOrName === 'string' ? typeOrName : typeOrName.name,
state.schema,
)
@ -58,10 +59,14 @@ export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, at
const trimmedFrom = Math.max(pos, from)
const trimmedTo = Math.min(pos + node.nodeSize, to)
tr.addMark(trimmedFrom, trimmedTo, markType.create({
...mark.attrs,
...attributes,
}))
tr.addMark(
trimmedFrom,
trimmedTo,
markType.create({
...mark.attrs,
...attributes,
}),
)
}
})
}

View File

@ -1,5 +1,5 @@
import { wrapIn as originalWrapIn } from 'prosemirror-commands'
import { NodeType } from 'prosemirror-model'
import { wrapIn as originalWrapIn } from '@tiptap/pm/commands'
import { NodeType } from '@tiptap/pm/model'
import { getNodeType } from '../helpers/getNodeType'
import { RawCommands } from '../types'
@ -10,7 +10,7 @@ declare module '@tiptap/core' {
/**
* Wraps nodes in another node.
*/
wrapIn: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType,
wrapIn: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType
}
}
}

View File

@ -1,5 +1,5 @@
import { NodeType } from 'prosemirror-model'
import { wrapInList as originalWrapInList } from 'prosemirror-schema-list'
import { NodeType } from '@tiptap/pm/model'
import { wrapInList as originalWrapInList } from '@tiptap/pm/schema-list'
import { getNodeType } from '../helpers/getNodeType'
import { RawCommands } from '../types'
@ -10,7 +10,7 @@ declare module '@tiptap/core' {
/**
* Wrap a node in a list.
*/
wrapInList: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType,
wrapInList: (typeOrName: string | NodeType, attributes?: Record<string, any>) => ReturnType
}
}
}

View File

@ -1,4 +1,4 @@
import { Plugin, PluginKey } from 'prosemirror-state'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Extension } from '../Extension'
import { getTextBetween } from '../helpers/getTextBetween'

View File

@ -1,4 +1,4 @@
import { Plugin, PluginKey } from 'prosemirror-state'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Extension } from '../Extension'

View File

@ -1,4 +1,4 @@
import { Plugin, PluginKey } from 'prosemirror-state'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Extension } from '../Extension'

View File

@ -1,4 +1,4 @@
import { Plugin, PluginKey, Selection } from 'prosemirror-state'
import { Plugin, PluginKey, Selection } from '@tiptap/pm/state'
import { CommandManager } from '../CommandManager'
import { Extension } from '../Extension'
@ -19,12 +19,7 @@ export const Keymap = Extension.create({
const { pos, parent } = $anchor
const isAtStart = Selection.atStart(doc).from === pos
if (
!empty
|| !isAtStart
|| !parent.type.isTextblock
|| parent.textContent.length
) {
if (!empty || !isAtStart || !parent.type.isTextblock || parent.textContent.length) {
return false
}

View File

@ -1,4 +1,4 @@
import { Plugin, PluginKey } from 'prosemirror-state'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Extension } from '../Extension'

View File

@ -1,11 +1,14 @@
import { Node as ProseMirrorNode } from 'prosemirror-model'
import { Transaction } from 'prosemirror-state'
import { Transform } from 'prosemirror-transform'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
import { Transaction } from '@tiptap/pm/state'
import { Transform } from '@tiptap/pm/transform'
/**
* Returns a new `Transform` based on all steps of the passed transactions.
*/
export function combineTransactionSteps(oldDoc: ProseMirrorNode, transactions: Transaction[]): Transform {
export function combineTransactionSteps(
oldDoc: ProseMirrorNode,
transactions: Transaction[],
): Transform {
const transform = new Transform(oldDoc)
transactions.forEach(transaction => {

View File

@ -1,8 +1,8 @@
import { EditorState, Transaction } from 'prosemirror-state'
import { EditorState, Transaction } from '@tiptap/pm/state'
export function createChainableState(config: {
transaction: Transaction,
state: EditorState,
transaction: Transaction
state: EditorState
}): EditorState {
const { state, transaction } = config
let { selection } = transaction

View File

@ -1,4 +1,4 @@
import { Node as ProseMirrorNode, ParseOptions, Schema } from 'prosemirror-model'
import { Node as ProseMirrorNode, ParseOptions, Schema } from '@tiptap/pm/model'
import { Content } from '../types'
import { createNodeFromContent } from './createNodeFromContent'

View File

@ -4,14 +4,14 @@ import {
Node as ProseMirrorNode,
ParseOptions,
Schema,
} from 'prosemirror-model'
} from '@tiptap/pm/model'
import { Content } from '../types'
import { elementFromString } from '../utilities/elementFromString'
export type CreateNodeFromContentOptions = {
slice?: boolean,
parseOptions?: ParseOptions,
slice?: boolean
parseOptions?: ParseOptions
}
export function createNodeFromContent(
@ -33,13 +33,7 @@ export function createNodeFromContent(
return schema.nodeFromJSON(content)
} catch (error) {
console.warn(
'[tiptap warn]: Invalid content.',
'Passed value:',
content,
'Error:',
error,
)
console.warn('[tiptap warn]: Invalid content.', 'Passed value:', content, 'Error:', error)
return createNodeFromContent('', schema, options)
}

Some files were not shown because too many files have changed in this diff Show More