add new syntax to all extensions

This commit is contained in:
Philipp Kühn
2020-10-22 12:34:49 +02:00
parent e442b5a8fe
commit 79172753ef
22 changed files with 873 additions and 703 deletions

View File

@@ -3,11 +3,7 @@
</template> </template>
<script> <script>
// import { Editor, EditorContent, defaultExtensions } from '@tiptap/vue-starter-kit' import { Editor, EditorContent, defaultExtensions } from '@tiptap/vue-starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-starter-kit'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
export default { export default {
components: { components: {
@@ -23,12 +19,7 @@ export default {
mounted() { mounted() {
this.editor = new Editor({ this.editor = new Editor({
content: '<p>Im running tiptap with Vue.js. 🎉</p>', content: '<p>Im running tiptap with Vue.js. 🎉</p>',
// extensions: defaultExtensions(), extensions: defaultExtensions(),
extensions: [
new Document(),
new Paragraph(),
new Text(),
],
}) })
}, },

View File

@@ -1,141 +1,73 @@
// import cloneDeep from 'clone-deep' import { Plugin } from 'prosemirror-state'
// import { Plugin } from 'prosemirror-state'
// import { Editor, CommandsSpec } from './Editor'
// type AnyObject = {
// [key: string]: any
// }
// type NoInfer<T> = [T][T extends any ? 0 : never]
// type MergeStrategy = 'extend' | 'overwrite'
// type Configs = {
// [key: string]: {
// stategy: MergeStrategy
// value: any
// }[]
// }
// export interface ExtensionProps<Options> {
// name: string
// editor: Editor
// options: Options
// }
// export interface ExtensionMethods<Props, Options> {
// name: string
// options: Options
// commands: (params: Props) => CommandsSpec
// inputRules: (params: Props) => any[]
// pasteRules: (params: Props) => any[]
// keys: (params: Props) => {
// [key: string]: Function
// }
// plugins: (params: Props) => Plugin[]
// }
// export default class Extension<
// Options = {},
// Props = ExtensionProps<Options>,
// Methods extends ExtensionMethods<Props, Options> = ExtensionMethods<Props, Options>,
// > {
// type = 'extension'
// config: AnyObject = {}
// configs: Configs = {}
// options: Partial<Options> = {}
// protected storeConfig(key: string, value: any, stategy: MergeStrategy) {
// const item = {
// stategy,
// value,
// }
// if (this.configs[key]) {
// this.configs[key].push(item)
// } else {
// this.configs[key] = [item]
// }
// }
// public configure(options: Partial<Options>) {
// this.options = { ...this.options, ...options }
// return this
// }
// public name(value: Methods['name']) {
// this.storeConfig('name', value, 'overwrite')
// return this
// }
// public defaults(value: Options) {
// this.storeConfig('defaults', value, 'overwrite')
// return this
// }
// public commands(value: Methods['commands']) {
// this.storeConfig('commands', value, 'overwrite')
// return this
// }
// public keys(value: Methods['keys']) {
// this.storeConfig('keys', value, 'overwrite')
// return this
// }
// public inputRules(value: Methods['inputRules']) {
// this.storeConfig('inputRules', value, 'overwrite')
// return this
// }
// public pasteRules(value: Methods['pasteRules']) {
// this.storeConfig('pasteRules', value, 'overwrite')
// return this
// }
// public plugins(value: Methods['plugins']) {
// this.storeConfig('plugins', value, 'overwrite')
// return this
// }
// public extend<T extends Extract<keyof Methods, string>>(key: T, value: Methods[T]) {
// this.storeConfig(key, value, 'extend')
// return this
// }
// public create() {
// return <NewOptions = Options>(options?: Partial<NoInfer<NewOptions>>) => {
// return cloneDeep(this, true).configure(options as NewOptions)
// }
// }
// }
import { Editor } from './Editor' import { Editor } from './Editor'
import { GlobalAttributes } from './types' import { GlobalAttributes } from './types'
export interface ExtensionSpec<Options = {}, Commands = {}> { export interface ExtensionSpec<Options = {}, Commands = {}> {
/**
* The name of your extension
*/
name: string, name: string,
/**
* Default options
*/
defaultOptions?: Options, defaultOptions?: Options,
/**
* Global attributes
*/
addGlobalAttributes?: ( addGlobalAttributes?: (
this: { this: {
options: Options, options: Options,
}, },
) => GlobalAttributes, ) => GlobalAttributes,
/**
* Commands
*/
addCommands?: (this: { addCommands?: (this: {
options: Options, options: Options,
editor: Editor, editor: Editor,
}) => Commands, }) => Commands,
/**
* Keyboard shortcuts
*/
addKeyboardShortcuts?: (this: { addKeyboardShortcuts?: (this: {
options: Options, options: Options,
editor: Editor, editor: Editor,
}) => { }) => {
[key: string]: any [key: string]: any
}, },
/**
* Input rules
*/
addInputRules?: (this: {
options: Options,
editor: Editor,
}) => any[],
/**
* Paste rules
*/
addPasteRules?: (this: {
options: Options,
editor: Editor,
}) => any[],
/**
* ProseMirror plugins
*/
addProseMirrorPlugins?: (this: {
options: Options,
editor: Editor,
}) => Plugin[],
} }
/**
* Extension interface for internal usage
*/
export type Extension = Required<Omit<ExtensionSpec, 'defaultOptions'> & { export type Extension = Required<Omit<ExtensionSpec, 'defaultOptions'> & {
type: string, type: string,
options: { options: {
@@ -143,6 +75,9 @@ export type Extension = Required<Omit<ExtensionSpec, 'defaultOptions'> & {
}, },
}> }>
/**
* Default extension
*/
export const defaultExtension: Extension = { export const defaultExtension: Extension = {
type: 'extension', type: 'extension',
name: 'extension', name: 'extension',
@@ -150,6 +85,9 @@ export const defaultExtension: Extension = {
addGlobalAttributes: () => [], addGlobalAttributes: () => [],
addCommands: () => ({}), addCommands: () => ({}),
addKeyboardShortcuts: () => ({}), addKeyboardShortcuts: () => ({}),
addInputRules: () => [],
addPasteRules: () => [],
addProseMirrorPlugins: () => [],
} }
export function createExtension<Options extends {}, Commands extends {}>(config: ExtensionSpec<Options, Commands>) { export function createExtension<Options extends {}, Commands extends {}>(config: ExtensionSpec<Options, Commands>) {

View File

@@ -1,37 +1,52 @@
import { Command, Node } from '@tiptap/core' import { Command, createNode } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules' import { wrappingInputRule } from 'prosemirror-inputrules'
export type BlockquoteCommand = () => Command // export type BlockquoteCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
blockquote: BlockquoteCommand, // blockquote: BlockquoteCommand,
} // }
} // }
export const inputRegex = /^\s*>\s$/gm export const inputRegex = /^\s*>\s$/gm
export default new Node() export default createNode({
.name('blockquote') name: 'blockquote',
.schema(() => ({
content: 'block*', content: 'block*',
group: 'block',
defining: true, group: 'block',
draggable: false,
parseDOM: [ defining: true,
parseHTML() {
return [
{ tag: 'blockquote' }, { tag: 'blockquote' },
], ]
toDOM: () => ['blockquote', 0], },
}))
.commands(({ name }) => ({ renderHTML({ attributes }) {
[name]: () => ({ commands }) => { return ['blockquote', attributes, 0]
return commands.toggleWrap(name) },
},
})) addCommands() {
.keys(({ editor }) => ({ return {
'Shift-Mod-9': () => editor.blockquote(), blockquote: () => ({ commands }) => {
})) return commands.toggleWrap('blockquote')
.inputRules(({ type }) => [ },
wrappingInputRule(inputRegex, type), }
]) },
.create()
addKeyboardShortcuts() {
return {
'Shift-Mod-9': () => this.editor.blockquote(),
}
},
addInputRules() {
return [
wrappingInputRule(inputRegex, this.type),
]
},
})

View File

@@ -1,24 +1,25 @@
import { import {
Command, Mark, markInputRule, markPasteRule, Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core' } from '@tiptap/core'
export type BoldCommand = () => Command // export type BoldCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
bold: BoldCommand, // bold: BoldCommand,
} // }
} // }
export const starInputRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))$/gm export const starInputRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))$/gm
export const starPasteRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))/gm export const starPasteRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))/gm
export const underscoreInputRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))$/gm export const underscoreInputRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))$/gm
export const underscorePasteRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))/gm export const underscorePasteRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))/gm
export default new Mark() export default createMark({
.name('bold') name: 'bold',
.schema(() => ({
parseDOM: [ parseHTML() {
return [
{ {
tag: 'strong', tag: 'strong',
}, },
@@ -30,23 +31,38 @@ export default new Mark()
style: 'font-weight', style: 'font-weight',
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null, getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null,
}, },
], ]
toDOM: () => ['strong', 0], },
}))
.commands(({ name }) => ({ renderHTML({ attributes }) {
bold: () => ({ commands }) => { return ['strong', attributes, 0]
return commands.toggleMark(name) },
},
})) addCommands() {
.keys(({ editor }) => ({ return {
'Mod-b': () => editor.bold(), bold: () => ({ commands }) => {
})) return commands.toggleMark('bold')
.inputRules(({ type }) => [ },
markInputRule(starInputRegex, type), }
markInputRule(underscoreInputRegex, type), },
])
.pasteRules(({ type }) => [ addKeyboardShortcuts() {
markPasteRule(starPasteRegex, type), return {
markPasteRule(underscorePasteRegex, type), 'Mod-b': () => this.editor.bold(),
]) }
.create() },
addInputRules() {
return [
markInputRule(starInputRegex, this.type),
markInputRule(underscoreInputRegex, this.type),
]
},
addPasteRules() {
return [
markPasteRule(starPasteRegex, this.type),
markPasteRule(underscorePasteRegex, this.type),
]
},
})

View File

@@ -1,33 +1,48 @@
import { Command, Node } from '@tiptap/core' import { Command, createNode } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules' import { wrappingInputRule } from 'prosemirror-inputrules'
export type BulletListCommand = () => Command // export type BulletListCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
bulletList: BulletListCommand, // bulletList: BulletListCommand,
} // }
} // }
export default new Node() export default createNode({
.name('bullet_list') name: 'bullet_list',
.schema(() => ({
content: 'list_item+', content: 'list_item+',
group: 'block',
parseDOM: [ group: 'block',
parseHTML() {
return [
{ tag: 'ul' }, { tag: 'ul' },
], ]
toDOM: () => ['ul', 0], },
}))
.commands(({ name }) => ({ renderHTML({ attributes }) {
bulletList: () => ({ commands }) => { return ['ul', attributes, 0]
return commands.toggleList(name, 'list_item') },
},
})) addCommands() {
.keys(({ editor }) => ({ return {
'Shift-Control-8': () => editor.bulletList(), bulletList: () => ({ commands }) => {
})) return commands.toggleList('bullet_list', 'list_item')
.inputRules(({ type }) => [ },
wrappingInputRule(/^\s*([-+*])\s$/, type), }
]) },
.create()
addKeyboardShortcuts() {
return {
'Shift-Control-8': () => this.editor.bulletList(),
}
},
addInputRules() {
return [
wrappingInputRule(/^\s*([-+*])\s$/, this.type),
]
},
})

View File

@@ -1,69 +1,91 @@
import { Command, Node } from '@tiptap/core' import { Command, createNode } from '@tiptap/core'
import { textblockTypeInputRule } from 'prosemirror-inputrules' import { textblockTypeInputRule } from 'prosemirror-inputrules'
export interface CodeBlockOptions { export interface CodeBlockOptions {
languageClassPrefix: string, languageClassPrefix: string,
} }
export type CodeBlockCommand = () => Command // export type CodeBlockCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
codeBlock: CodeBlockCommand, // codeBlock: CodeBlockCommand,
} // }
} // }
export const backtickInputRegex = /^```(?<language>[a-z]*)? $/ export const backtickInputRegex = /^```(?<language>[a-z]*)? $/
export const tildeInputRegex = /^~~~(?<language>[a-z]*)? $/ export const tildeInputRegex = /^~~~(?<language>[a-z]*)? $/
export default new Node<CodeBlockOptions>() export default createNode({
.name('code_block') name: 'code_block',
.defaults({
defaultOptions: <CodeBlockOptions>{
languageClassPrefix: 'language-', languageClassPrefix: 'language-',
}) },
.schema(({ options }) => ({
attrs: { content: 'text*',
marks: '',
group: 'block',
code: true,
defining: true,
addAttributes() {
return {
language: { language: {
default: null, default: null,
rendered: false,
}, },
}, }
content: 'text*', },
marks: '',
group: 'block', parseHTML() {
code: true, return [
defining: true,
draggable: false,
parseDOM: [
{ {
tag: 'pre', tag: 'pre',
preserveWhitespace: 'full', preserveWhitespace: 'full',
getAttrs(node) { getAttrs: node => {
const classAttribute = (node as Element).firstElementChild?.getAttribute('class') const classAttribute = (node as Element).firstElementChild?.getAttribute('class')
if (!classAttribute) { if (!classAttribute) {
return null return null
} }
const regexLanguageClassPrefix = new RegExp(`^(${options.languageClassPrefix})`) const regexLanguageClassPrefix = new RegExp(`^(${this.options.languageClassPrefix})`)
return { language: classAttribute.replace(regexLanguageClassPrefix, '') } return { language: classAttribute.replace(regexLanguageClassPrefix, '') }
}, },
}, },
], ]
toDOM: node => ['pre', ['code', { },
class: node.attrs.language && options.languageClassPrefix + node.attrs.language,
}, 0]], renderHTML({ node, attributes }) {
})) return ['pre', attributes, ['code', {
.commands(({ name }) => ({ class: node.attrs.language && this.options.languageClassPrefix + node.attrs.language,
codeBlock: attrs => ({ commands }) => { }, 0]]
return commands.toggleBlockType(name, 'paragraph', attrs) },
},
})) addCommands() {
.keys(({ editor }) => ({ return {
'Mod-Shift-c': () => editor.codeBlock(), codeBlock: attrs => ({ commands }) => {
})) return commands.toggleBlockType('code_block', 'paragraph', attrs)
.inputRules(({ type }) => [ },
textblockTypeInputRule(backtickInputRegex, type, ({ groups }: any) => groups), }
textblockTypeInputRule(tildeInputRegex, type, ({ groups }: any) => groups), },
])
.create() addKeyboardShortcuts() {
return {
'Mod-Shift-c': () => this.editor.codeBlock(),
}
},
addInputRules() {
return [
textblockTypeInputRule(backtickInputRegex, this.type, ({ groups }: any) => groups),
textblockTypeInputRule(tildeInputRegex, this.type, ({ groups }: any) => groups),
]
},
})

View File

@@ -1,39 +1,56 @@
import { import {
Command, Mark, markInputRule, markPasteRule, Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core' } from '@tiptap/core'
export type CodeCommand = () => Command // export type CodeCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
code: CodeCommand, // code: CodeCommand,
} // }
} // }
export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/gm export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/gm
export const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/gm export const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/gm
export default new Mark() export default createMark({
.name('code') name: 'code',
.schema(() => ({
excludes: '_', excludes: '_',
parseDOM: [
parseHTML() {
return [
{ tag: 'code' }, { tag: 'code' },
], ]
toDOM: () => ['code', 0], },
}))
.commands(({ name }) => ({ renderHTML({ attributes }) {
code: () => ({ commands }) => { return ['strong', attributes, 0]
return commands.toggleMark(name) },
},
})) addCommands() {
.keys(({ editor }) => ({ return {
'Mod-`': () => editor.code(), code: () => ({ commands }) => {
})) return commands.toggleMark('code')
.inputRules(({ type }) => [ },
markInputRule(inputRegex, type), }
]) },
.pasteRules(({ type }) => [
markPasteRule(inputRegex, type), addKeyboardShortcuts() {
]) return {
.create() 'Mod-`': () => this.editor.code(),
}
},
addInputRules() {
return [
markInputRule(inputRegex, this.type),
]
},
addPasteRules() {
return [
markPasteRule(inputRegex, this.type),
]
},
})

View File

@@ -1,4 +1,4 @@
import { Extension, Command } from '@tiptap/core' import { createExtension, Command } from '@tiptap/core'
import { yCursorPlugin } from 'y-prosemirror' import { yCursorPlugin } from 'y-prosemirror'
export interface CollaborationCursorOptions { export interface CollaborationCursorOptions {
@@ -8,27 +8,21 @@ export interface CollaborationCursorOptions {
render (user: { name: string, color: string }): HTMLElement, render (user: { name: string, color: string }): HTMLElement,
} }
export type UserCommand = (attributes: { // export type UserCommand = (attributes: {
name: string, // name: string,
color: string, // color: string,
}) => Command // }) => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
user: UserCommand, // user: UserCommand,
} // }
} // }
export default new Extension<CollaborationCursorOptions>() export default createExtension({
.name('collaboration_cursor') name: 'collaboration_cursor',
.commands(({ options }) => ({
user: attributes => () => {
options.provider.awareness.setLocalStateField('user', attributes)
return true defaultOptions: <CollaborationCursorOptions>{
},
}))
.defaults({
provider: null, provider: null,
name: 'Someone', name: 'Someone',
color: '#cccccc', color: '#cccccc',
@@ -45,19 +39,32 @@ export default new Extension<CollaborationCursorOptions>()
return cursor return cursor
}, },
}) },
.plugins(({ options }) => [
yCursorPlugin((() => {
options.provider.awareness.setLocalStateField('user', {
name: options.name,
color: options.color,
})
return options.provider.awareness addCommands() {
})(), return {
// @ts-ignore user: attributes => () => {
{ this.options.provider.awareness.setLocalStateField('user', attributes)
cursorBuilder: options.render,
}), return true
]) },
.create() }
},
addProseMirrorPlugins() {
return [
yCursorPlugin((() => {
this.options.provider.awareness.setLocalStateField('user', {
name: this.options.name,
color: this.options.color,
})
return this.options.provider.awareness
})(),
// @ts-ignore
{
cursorBuilder: this.options.render,
}),
]
},
})

View File

@@ -1,4 +1,4 @@
import { Extension } from '@tiptap/core' import { createExtension } from '@tiptap/core'
import { import {
redo, undo, ySyncPlugin, yUndoPlugin, redo, undo, ySyncPlugin, yUndoPlugin,
} from 'y-prosemirror' } from 'y-prosemirror'
@@ -8,21 +8,26 @@ export interface CollaborationOptions {
type: any, type: any,
} }
export default new Extension<CollaborationOptions>() export default createExtension({
.name('collaboration') name: 'collaboration',
.defaults({
defaultOptions: <CollaborationOptions>{
provider: null, provider: null,
type: null, type: null,
}) },
.plugins(({ options }) => [
ySyncPlugin(options.type), addProseMirrorPlugins() {
yUndoPlugin(), return [
]) ySyncPlugin(this.options.type),
.keys(() => { yUndoPlugin(),
]
},
addKeyboardShortcuts() {
return { return {
'Mod-z': undo, 'Mod-z': undo,
'Mod-y': redo, 'Mod-y': redo,
'Mod-Shift-z': redo, 'Mod-Shift-z': redo,
} }
}) },
.create() })

View File

@@ -1,4 +1,4 @@
import { Extension } from '@tiptap/core' import { createExtension } from '@tiptap/core'
import { Plugin } from 'prosemirror-state' import { Plugin } from 'prosemirror-state'
import { DecorationSet, Decoration } from 'prosemirror-view' import { DecorationSet, Decoration } from 'prosemirror-view'
@@ -7,40 +7,44 @@ export interface FocusOptions {
nested: boolean, nested: boolean,
} }
export default new Extension<FocusOptions>() export default createExtension({
.name('focus') name: 'focus',
.defaults({
defaultOptions: <FocusOptions>{
className: 'has-focus', className: 'has-focus',
nested: false, nested: false,
}) },
.plugins(({ editor, options }) => [
new Plugin({
props: {
decorations: ({ doc, selection }) => {
const { isEditable, isFocused } = editor
const { anchor } = selection
const decorations: Decoration[] = []
if (!isEditable || !isFocused) { addProseMirrorPlugins() {
return DecorationSet.create(doc, []) return [
} new Plugin({
props: {
decorations: ({ doc, selection }) => {
const { isEditable, isFocused } = this.editor
const { anchor } = selection
const decorations: Decoration[] = []
doc.descendants((node, pos) => { if (!isEditable || !isFocused) {
const hasAnchor = anchor >= pos && anchor <= (pos + node.nodeSize) return DecorationSet.create(doc, [])
if (hasAnchor && !node.isText) {
const decoration = Decoration.node(pos, pos + node.nodeSize, {
class: options.className,
})
decorations.push(decoration)
} }
return options.nested doc.descendants((node, pos) => {
}) const hasAnchor = anchor >= pos && anchor <= (pos + node.nodeSize)
return DecorationSet.create(doc, decorations) if (hasAnchor && !node.isText) {
const decoration = Decoration.node(pos, pos + node.nodeSize, {
class: this.options.className,
})
decorations.push(decoration)
}
return this.options.nested
})
return DecorationSet.create(doc, decorations)
},
}, },
}, }),
}), ]
]) },
.create() })

View File

@@ -1,37 +1,50 @@
import { Command, Node } from '@tiptap/core' import { Command, createNode } from '@tiptap/core'
import { chainCommands, exitCode } from 'prosemirror-commands' import { chainCommands, exitCode } from 'prosemirror-commands'
export type HardBreakCommand = () => Command // export type HardBreakCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
hardBreak: HardBreakCommand, // hardBreak: HardBreakCommand,
} // }
} // }
export default new Node() export default createNode({
.name('hardBreak') name: 'hardBreak',
.schema(() => ({
inline: true, inline: true,
group: 'inline',
selectable: false, group: 'inline',
parseDOM: [
selectable: false,
parseHTML() {
return [
{ tag: 'br' }, { tag: 'br' },
], ]
toDOM: () => ['br'], },
}))
.commands(({ type }) => ({ renderHTML({ attributes }) {
hardBreak: () => ({ return ['br', attributes]
tr, state, dispatch, view, },
}) => {
return chainCommands(exitCode, () => { addCommands() {
dispatch(tr.replaceSelectionWith(type.create()).scrollIntoView()) return {
return true hardBreak: () => ({
})(state, dispatch, view) tr, state, dispatch, view,
}, }) => {
})) return chainCommands(exitCode, () => {
.keys(({ editor }) => ({ dispatch(tr.replaceSelectionWith(this.type.create()).scrollIntoView())
'Mod-Enter': () => editor.hardBreak(), return true
'Shift-Enter': () => editor.hardBreak(), })(state, dispatch, view)
})) },
.create() }
},
addKeyboardShortcuts() {
return {
'Mod-Enter': () => this.editor.hardBreak(),
'Shift-Enter': () => this.editor.hardBreak(),
}
},
})

View File

@@ -1,4 +1,4 @@
import { Command, Node } from '@tiptap/core' import { Command, createNode } from '@tiptap/core'
import { textblockTypeInputRule } from 'prosemirror-inputrules' import { textblockTypeInputRule } from 'prosemirror-inputrules'
type Level = 1 | 2 | 3 | 4 | 5 | 6 type Level = 1 | 2 | 3 | 4 | 5 | 6
@@ -7,52 +7,68 @@ export interface HeadingOptions {
levels: Level[], levels: Level[],
} }
export type HeadingCommand = (options: { level: Level }) => Command // export type HeadingCommand = (options: { level: Level }) => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
heading: HeadingCommand, // heading: HeadingCommand,
} // }
} // }
export default new Node<HeadingOptions>() export default createNode({
.name('heading') name: 'heading',
.defaults({
defaultOptions: <HeadingOptions>{
levels: [1, 2, 3, 4, 5, 6], levels: [1, 2, 3, 4, 5, 6],
}) },
.schema(({ options }) => ({
attrs: { content: 'inline*',
group: 'block',
defining: true,
addAttributes() {
return {
level: { level: {
default: 1, default: 1,
rendered: false,
}, },
}, }
content: 'inline*', },
group: 'block',
defining: true, parseHTML() {
draggable: false, return this.options.levels
parseDOM: options.levels
.map((level: Level) => ({ .map((level: Level) => ({
tag: `h${level}`, tag: `h${level}`,
attrs: { level }, attrs: { level },
})), }))
toDOM: node => [`h${node.attrs.level}`, 0], },
}))
.commands(({ name }) => ({ renderHTML({ node, attributes }) {
heading: attrs => ({ commands }) => { return [`h${node.attrs.level}`, attributes, 0]
return commands.toggleBlockType(name, 'paragraph', attrs) },
},
})) addCommands() {
.keys(({ name, options, editor }) => { return {
return options.levels.reduce((items, level) => ({ heading: attrs => ({ commands }) => {
return commands.toggleBlockType('heading', 'paragraph', attrs)
},
}
},
addKeyboardShortcuts() {
return this.options.levels.reduce((items, level) => ({
...items, ...items,
...{ ...{
[`Mod-Alt-${level}`]: () => editor.setBlockType(name, { level }), [`Mod-Alt-${level}`]: () => this.editor.setBlockType('heading', { level }),
}, },
}), {}) }), {})
}) },
.inputRules(({ options, type }) => {
return options.levels.map((level: Level) => { addInputRules() {
return textblockTypeInputRule(new RegExp(`^(#{1,${level}})\\s$`), type, { level }) return this.options.levels.map(level => {
return textblockTypeInputRule(new RegExp(`^(#{1,${level}})\\s$`), this.type, { level })
}) })
}) },
.create() })

View File

@@ -1,42 +1,52 @@
import { Command, Extension } from '@tiptap/core' import { Command, createExtension } from '@tiptap/core'
import { import {
history, history,
undo, undo,
redo, redo,
} from 'prosemirror-history' } from 'prosemirror-history'
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
undo: () => Command, // undo: () => Command,
redo: () => Command, // redo: () => Command,
} // }
} // }
export interface HistoryOptions { export interface HistoryOptions {
depth: number, depth: number,
newGroupDelay: number, newGroupDelay: number,
} }
export default new Extension<HistoryOptions>() export default createExtension({
.name('history') name: 'history',
.defaults({
defaultOptions: <HistoryOptions>{
depth: 100, depth: 100,
newGroupDelay: 500, newGroupDelay: 500,
}) },
.commands(() => ({
undo: () => ({ state, dispatch }) => { addCommands() {
return undo(state, dispatch) return {
}, undo: () => ({ state, dispatch }) => {
redo: () => ({ state, dispatch }) => { return undo(state, dispatch)
return redo(state, dispatch) },
}, redo: () => ({ state, dispatch }) => {
})) return redo(state, dispatch)
.keys(({ editor }) => ({ },
'Mod-z': () => editor.undo(), }
'Mod-y': () => editor.redo(), },
'Shift-Mod-z': () => editor.redo(),
})) addProseMirrorPlugins() {
.plugins(({ options }) => [ return [
history(options), history(this.options),
]) ]
.create() },
addKeyboardShortcuts() {
return {
'Mod-z': () => this.editor.undo(),
'Mod-y': () => this.editor.redo(),
'Shift-Mod-z': () => this.editor.redo(),
}
},
})

View File

@@ -1,28 +1,41 @@
import { Command, Node, nodeInputRule } from '@tiptap/core' import { Command, createNode, nodeInputRule } from '@tiptap/core'
export type HorizontalRuleCommand = () => Command // export type HorizontalRuleCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
horizontalRule: HorizontalRuleCommand, // horizontalRule: HorizontalRuleCommand,
} // }
} // }
export default new Node() export default createNode({
.name('horizontalRule') name: 'horizontalRule',
.schema(() => ({
group: 'block',
parseDOM: [{ tag: 'hr' }],
toDOM: () => ['hr'],
}))
.commands(({ type }) => ({
horizontalRule: () => ({ tr }) => {
tr.replaceSelectionWith(type.create())
return true group: 'block',
},
})) parseHTML() {
.inputRules(({ type }) => [ return [
nodeInputRule(/^(?:---|___\s|\*\*\*\s)$/, type), { tag: 'hr' },
]) ]
.create() },
renderHTML({ attributes }) {
return ['hr', attributes]
},
addCommands() {
return {
horizontalRule: () => ({ tr }) => {
tr.replaceSelectionWith(this.type.create())
return true
},
}
},
addInputRules() {
return [
nodeInputRule(/^(?:---|___\s|\*\*\*\s)$/, this.type),
]
},
})

View File

@@ -1,24 +1,25 @@
import { import {
Command, Mark, markInputRule, markPasteRule, Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core' } from '@tiptap/core'
export type ItalicCommand = () => Command // export type ItalicCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
italic: ItalicCommand, // italic: ItalicCommand,
} // }
} // }
export const starInputRegex = /(?:^|\s)((?:\*)((?:[^*]+))(?:\*))$/gm export const starInputRegex = /(?:^|\s)((?:\*)((?:[^*]+))(?:\*))$/gm
export const starPasteRegex = /(?:^|\s)((?:\*)((?:[^*]+))(?:\*))/gm export const starPasteRegex = /(?:^|\s)((?:\*)((?:[^*]+))(?:\*))/gm
export const underscoreInputRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))$/gm export const underscoreInputRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))$/gm
export const underscorePasteRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))/gm export const underscorePasteRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))/gm
export default new Mark() export default createMark({
.name('italic') name: 'italic',
.schema(() => ({
parseDOM: [ parseHTML() {
return [
{ {
tag: 'em', tag: 'em',
}, },
@@ -29,23 +30,38 @@ export default new Mark()
{ {
style: 'font-style=italic', style: 'font-style=italic',
}, },
], ]
toDOM: () => ['em', 0], },
}))
.commands(({ name }) => ({ renderHTML({ attributes }) {
italic: () => ({ commands }) => { return ['em', attributes, 0]
return commands.toggleMark(name) },
},
})) addCommands() {
.keys(({ editor }) => ({ return {
'Mod-i': () => editor.italic(), italic: () => ({ commands }) => {
})) return commands.toggleMark('italic')
.inputRules(({ type }) => [ },
markInputRule(starInputRegex, type), }
markInputRule(underscoreInputRegex, type), },
])
.pasteRules(({ type }) => [ addKeyboardShortcuts() {
markPasteRule(starPasteRegex, type), return {
markPasteRule(underscorePasteRegex, type), 'Mod-i': () => this.editor.italic(),
]) }
.create() },
addInputRules() {
return [
markInputRule(starInputRegex, this.type),
markInputRule(underscoreInputRegex, this.type),
]
},
addPasteRules() {
return [
markPasteRule(starPasteRegex, this.type),
markPasteRule(underscorePasteRegex, this.type),
]
},
})

View File

@@ -1,5 +1,5 @@
import { import {
Command, Mark, markPasteRule, Command, createMark, markPasteRule,
} from '@tiptap/core' } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state' import { Plugin, PluginKey } from 'prosemirror-state'
@@ -9,34 +9,42 @@ export interface LinkOptions {
rel: string, rel: string,
} }
export type LinkCommand = (options: {href?: string, target?: string}) => Command // export type LinkCommand = (options: {href?: string, target?: string}) => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
link: LinkCommand, // link: LinkCommand,
} // }
} // }
export const pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%_+.~#?&//=]*)/gi export const pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%_+.~#?&//=]*)/gi
export default new Mark<LinkOptions>() export default createMark({
.name('link') name: 'link',
.defaults({
inclusive: false,
defaultOptions: <LinkOptions>{
openOnClick: true, openOnClick: true,
target: '_blank', target: '_blank',
rel: 'noopener noreferrer nofollow', rel: 'noopener noreferrer nofollow',
}) },
.schema(({ options }) => ({
attrs: { addAttributes() {
return {
href: { href: {
default: null, default: null,
rendered: false,
}, },
target: { target: {
default: null, default: null,
rendered: false,
}, },
}, }
inclusive: false, },
parseDOM: [
parseHTML() {
return [
{ {
tag: 'a[href]', tag: 'a[href]',
getAttrs: node => ({ getAttrs: node => ({
@@ -44,27 +52,44 @@ export default new Mark<LinkOptions>()
target: (node as HTMLElement).getAttribute('target'), target: (node as HTMLElement).getAttribute('target'),
}), }),
}, },
], ]
toDOM: node => ['a', { },
...node.attrs,
rel: options.rel,
target: node.attrs.target ? node.attrs.target : options.target,
}, 0],
}))
.commands(({ name }) => ({
link: attributes => ({ commands }) => {
if (!attributes.href) {
return commands.removeMark(name)
}
return commands.updateMark(name, attributes) renderHTML({ mark, attributes }) {
}, return ['a', {
})) ...attributes,
.pasteRules(({ type }) => [ ...mark.attrs,
markPasteRule(pasteRegex, type, (url: string) => ({ href: url })), rel: this.options.rel,
]) target: mark.attrs.target ? mark.attrs.target : this.options.target,
.plugins(({ editor, options, name }) => { }, 0]
if (!options.openOnClick) { },
addCommands() {
return {
link: attributes => ({ commands }) => {
if (!attributes.href) {
return commands.removeMark('link')
}
return commands.updateMark('link', attributes)
},
}
},
addKeyboardShortcuts() {
return {
'Mod-i': () => this.editor.italic(),
}
},
addPasteRules() {
return [
markPasteRule(pasteRegex, this.type, (url: string) => ({ href: url })),
]
},
addProseMirrorPlugins() {
if (!this.options.openOnClick) {
return [] return []
} }
@@ -73,7 +98,7 @@ export default new Mark<LinkOptions>()
key: new PluginKey('handleClick'), key: new PluginKey('handleClick'),
props: { props: {
handleClick: (view, pos, event) => { handleClick: (view, pos, event) => {
const attrs = editor.getMarkAttrs(name) const attrs = this.editor.getMarkAttrs('link')
if (attrs.href && event.target instanceof HTMLAnchorElement) { if (attrs.href && event.target instanceof HTMLAnchorElement) {
window.open(attrs.href, attrs.target) window.open(attrs.href, attrs.target)
@@ -86,5 +111,5 @@ export default new Mark<LinkOptions>()
}, },
}), }),
] ]
}) },
.create() })

View File

@@ -1,17 +1,27 @@
import { Node } from '@tiptap/core' import { createNode } from '@tiptap/core'
export default new Node() export default createNode({
.name('list_item') name: 'list_item',
.schema(() => ({
content: 'paragraph block*', content: 'paragraph block*',
defining: true,
draggable: false, defining: true,
parseDOM: [{ tag: 'li' }],
toDOM: () => ['li', 0], parseHTML() {
})) return [
.keys(({ editor, name }) => ({ { tag: 'li' },
Enter: () => editor.splitListItem(name), ]
Tab: () => editor.sinkListItem(name), },
'Shift-Tab': () => editor.liftListItem(name),
})) renderHTML({ attributes }) {
.create() return ['li', attributes, 0]
},
addKeyboardShortcuts() {
return {
Enter: () => this.editor.splitListItem('list_item'),
Tab: () => this.editor.sinkListItem('list_item'),
'Shift-Tab': () => this.editor.liftListItem('list_item'),
}
},
})

View File

@@ -1,51 +1,71 @@
import { Command, Node } from '@tiptap/core' import { Command, createNode } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules' import { wrappingInputRule } from 'prosemirror-inputrules'
export type OrderedListCommand = () => Command // export type OrderedListCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
orderedList: OrderedListCommand, // orderedList: OrderedListCommand,
} // }
} // }
export default new Node() export default createNode({
.name('ordered_list') name: 'ordered_list',
.schema(() => ({
attrs: { content: 'list_item+',
group: 'block',
addAttributes() {
return {
order: { order: {
default: 1, default: 1,
rendered: false,
}, },
}, }
content: 'list_item+', },
group: 'block',
parseDOM: [{ parseHTML() {
tag: 'ol', return [
getAttrs: node => ({ {
order: (node as HTMLElement).hasAttribute('start') tag: 'ol',
? parseInt((node as HTMLElement).getAttribute('start') || '', 10) getAttrs: node => ({
: 1, order: (node as HTMLElement).hasAttribute('start')
}), ? parseInt((node as HTMLElement).getAttribute('start') || '', 10)
}], : 1,
toDOM: node => (node.attrs.order === 1 }),
? ['ol', 0] },
: ['ol', { start: node.attrs.order }, 0] ]
), },
}))
.commands(({ name }) => ({ renderHTML({ node, attributes }) {
orderedList: () => ({ commands }) => { return node.attrs.order === 1
return commands.toggleList(name, 'list_item') ? ['ol', attributes, 0]
}, : ['ol', { ...attributes, start: node.attrs.order }, 0]
})) },
.keys(({ editor }) => ({
'Shift-Control-9': () => editor.orderedList(), addCommands() {
})) return {
.inputRules(({ type }) => [ orderedList: () => ({ commands }) => {
wrappingInputRule( return commands.toggleList('ordered_list', 'list_item')
/^(\d+)\.\s$/, },
type, }
match => ({ order: +match[1] }), },
(match, node) => node.childCount + node.attrs.order === +match[1],
), addKeyboardShortcuts() {
]) return {
.create() 'Shift-Control-9': () => this.editor.orderedList(),
}
},
addInputRules() {
return [
wrappingInputRule(
/^(\d+)\.\s$/,
this.type,
match => ({ order: +match[1] }),
(match, node) => node.childCount + node.attrs.order === +match[1],
),
]
},
})

View File

@@ -16,35 +16,35 @@ export default createNode({
content: 'inline*', content: 'inline*',
addGlobalAttributes() { // addGlobalAttributes() {
return [ // return [
{ // {
types: ['paragraph'], // types: ['paragraph'],
attributes: { // attributes: {
align: { // align: {
default: 'right', // default: 'right',
renderHTML: attributes => ({ // renderHTML: attributes => ({
class: 'global', // class: 'global',
style: `text-align: ${attributes.align}`, // style: `text-align: ${attributes.align}`,
}), // }),
}, // },
}, // },
}, // },
] // ]
}, // },
addAttributes() { // addAttributes() {
return { // return {
id: { // id: {
default: '123', // default: '123',
rendered: true, // rendered: true,
renderHTML: attributes => ({ // renderHTML: attributes => ({
class: `foo-${attributes.id}`, // class: `foo-${attributes.id}`,
id: 'foo', // id: 'foo',
}), // }),
}, // },
} // }
}, // },
parseHTML() { parseHTML() {
return [ return [

View File

@@ -1,22 +1,23 @@
import { import {
Command, Mark, markInputRule, markPasteRule, Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core' } from '@tiptap/core'
type StrikeCommand = () => Command // type StrikeCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
strike: StrikeCommand, // strike: StrikeCommand,
} // }
} // }
export const inputRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))$/gm export const inputRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))$/gm
export const pasteRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))/gm export const pasteRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))/gm
export default new Mark() export default createMark({
.name('strike') name: 'strike',
.schema(() => ({
parseDOM: [ parseHTML() {
return [
{ {
tag: 's', tag: 's',
}, },
@@ -30,21 +31,36 @@ export default new Mark()
style: 'text-decoration', style: 'text-decoration',
getAttrs: node => (node === 'line-through' ? {} : false), getAttrs: node => (node === 'line-through' ? {} : false),
}, },
], ]
toDOM: () => ['s', 0], },
}))
.commands(({ name }) => ({ renderHTML({ attributes }) {
strike: () => ({ commands }) => { return ['s', attributes, 0]
return commands.toggleMark(name) },
},
})) addCommands() {
.keys(({ editor }) => ({ return {
'Mod-d': () => editor.strike(), strike: () => ({ commands }) => {
})) return commands.toggleMark('strike')
.inputRules(({ type }) => [ },
markInputRule(inputRegex, type), }
]) },
.pasteRules(({ type }) => [
markPasteRule(inputRegex, type), addKeyboardShortcuts() {
]) return {
.create() 'Mod-d': () => this.editor.strike(),
}
},
addInputRules() {
return [
markInputRule(inputRegex, this.type),
]
},
addPasteRules() {
return [
markPasteRule(inputRegex, this.type),
]
},
})

View File

@@ -1,17 +1,18 @@
import { Command, Mark } from '@tiptap/core' import { Command, createMark } from '@tiptap/core'
export type UnderlineCommand = () => Command // export type UnderlineCommand = () => Command
declare module '@tiptap/core/src/Editor' { // declare module '@tiptap/core/src/Editor' {
interface Commands { // interface Commands {
underline: UnderlineCommand, // underline: UnderlineCommand,
} // }
} // }
export default new Mark() export default createMark({
.name('underline') name: 'underline',
.schema(() => ({
parseDOM: [ parseHTML() {
return [
{ {
tag: 'u', tag: 'u',
}, },
@@ -19,15 +20,24 @@ export default new Mark()
style: 'text-decoration', style: 'text-decoration',
getAttrs: node => (node === 'underline' ? {} : false), getAttrs: node => (node === 'underline' ? {} : false),
}, },
], ]
toDOM: () => ['u', 0], },
}))
.commands(({ name }) => ({ renderHTML({ attributes }) {
underline: () => ({ commands }) => { return ['u', attributes, 0]
return commands.toggleMark(name) },
},
})) addCommands() {
.keys(({ editor }) => ({ return {
'Mod-u': () => editor.underline(), underline: () => ({ commands }) => {
})) return commands.toggleMark('underline')
.create() },
}
},
addKeyboardShortcuts() {
return {
'Mod-u': () => this.editor.underline(),
}
},
})

View File

@@ -1,16 +1,7 @@
// import originalDefaultExtensions from '@tiptap/starter-kit' import originalDefaultExtensions from '@tiptap/starter-kit'
import Document from '@tiptap/extension-document'
import Text from '@tiptap/extension-text'
import Paragraph from '@tiptap/extension-paragraph'
export * from '@tiptap/vue' export * from '@tiptap/vue'
export function defaultExtensions() { export function defaultExtensions() {
return [ return originalDefaultExtensions()
Document(),
Text(),
Paragraph(),
]
// return originalDefaultExtensions()
} }