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

View File

@ -1,141 +1,73 @@
// import cloneDeep from 'clone-deep'
// 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 { Plugin } from 'prosemirror-state'
import { Editor } from './Editor'
import { GlobalAttributes } from './types'
export interface ExtensionSpec<Options = {}, Commands = {}> {
/**
* The name of your extension
*/
name: string,
/**
* Default options
*/
defaultOptions?: Options,
/**
* Global attributes
*/
addGlobalAttributes?: (
this: {
options: Options,
},
) => GlobalAttributes,
/**
* Commands
*/
addCommands?: (this: {
options: Options,
editor: Editor,
}) => Commands,
/**
* Keyboard shortcuts
*/
addKeyboardShortcuts?: (this: {
options: Options,
editor: Editor,
}) => {
[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'> & {
type: string,
options: {
@ -143,6 +75,9 @@ export type Extension = Required<Omit<ExtensionSpec, 'defaultOptions'> & {
},
}>
/**
* Default extension
*/
export const defaultExtension: Extension = {
type: 'extension',
name: 'extension',
@ -150,6 +85,9 @@ export const defaultExtension: Extension = {
addGlobalAttributes: () => [],
addCommands: () => ({}),
addKeyboardShortcuts: () => ({}),
addInputRules: () => [],
addPasteRules: () => [],
addProseMirrorPlugins: () => [],
}
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'
export type BlockquoteCommand = () => Command
// export type BlockquoteCommand = () => Command
declare module '@tiptap/core/src/Editor' {
interface Commands {
blockquote: BlockquoteCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// blockquote: BlockquoteCommand,
// }
// }
export const inputRegex = /^\s*>\s$/gm
export default new Node()
.name('blockquote')
.schema(() => ({
content: 'block*',
group: 'block',
defining: true,
draggable: false,
parseDOM: [
export default createNode({
name: 'blockquote',
content: 'block*',
group: 'block',
defining: true,
parseHTML() {
return [
{ tag: 'blockquote' },
],
toDOM: () => ['blockquote', 0],
}))
.commands(({ name }) => ({
[name]: () => ({ commands }) => {
return commands.toggleWrap(name)
},
}))
.keys(({ editor }) => ({
'Shift-Mod-9': () => editor.blockquote(),
}))
.inputRules(({ type }) => [
wrappingInputRule(inputRegex, type),
])
.create()
]
},
renderHTML({ attributes }) {
return ['blockquote', attributes, 0]
},
addCommands() {
return {
blockquote: () => ({ commands }) => {
return commands.toggleWrap('blockquote')
},
}
},
addKeyboardShortcuts() {
return {
'Shift-Mod-9': () => this.editor.blockquote(),
}
},
addInputRules() {
return [
wrappingInputRule(inputRegex, this.type),
]
},
})

View File

@ -1,24 +1,25 @@
import {
Command, Mark, markInputRule, markPasteRule,
Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core'
export type BoldCommand = () => Command
// export type BoldCommand = () => Command
declare module '@tiptap/core/src/Editor' {
interface Commands {
bold: BoldCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// bold: BoldCommand,
// }
// }
export const starInputRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))$/gm
export const starPasteRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))/gm
export const underscoreInputRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))$/gm
export const underscorePasteRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))/gm
export default new Mark()
.name('bold')
.schema(() => ({
parseDOM: [
export default createMark({
name: 'bold',
parseHTML() {
return [
{
tag: 'strong',
},
@ -30,23 +31,38 @@ export default new Mark()
style: 'font-weight',
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null,
},
],
toDOM: () => ['strong', 0],
}))
.commands(({ name }) => ({
bold: () => ({ commands }) => {
return commands.toggleMark(name)
},
}))
.keys(({ editor }) => ({
'Mod-b': () => editor.bold(),
}))
.inputRules(({ type }) => [
markInputRule(starInputRegex, type),
markInputRule(underscoreInputRegex, type),
])
.pasteRules(({ type }) => [
markPasteRule(starPasteRegex, type),
markPasteRule(underscorePasteRegex, type),
])
.create()
]
},
renderHTML({ attributes }) {
return ['strong', attributes, 0]
},
addCommands() {
return {
bold: () => ({ commands }) => {
return commands.toggleMark('bold')
},
}
},
addKeyboardShortcuts() {
return {
'Mod-b': () => this.editor.bold(),
}
},
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'
export type BulletListCommand = () => Command
// export type BulletListCommand = () => Command
declare module '@tiptap/core/src/Editor' {
interface Commands {
bulletList: BulletListCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// bulletList: BulletListCommand,
// }
// }
export default new Node()
.name('bullet_list')
.schema(() => ({
content: 'list_item+',
group: 'block',
parseDOM: [
export default createNode({
name: 'bullet_list',
content: 'list_item+',
group: 'block',
parseHTML() {
return [
{ tag: 'ul' },
],
toDOM: () => ['ul', 0],
}))
.commands(({ name }) => ({
bulletList: () => ({ commands }) => {
return commands.toggleList(name, 'list_item')
},
}))
.keys(({ editor }) => ({
'Shift-Control-8': () => editor.bulletList(),
}))
.inputRules(({ type }) => [
wrappingInputRule(/^\s*([-+*])\s$/, type),
])
.create()
]
},
renderHTML({ attributes }) {
return ['ul', attributes, 0]
},
addCommands() {
return {
bulletList: () => ({ commands }) => {
return commands.toggleList('bullet_list', 'list_item')
},
}
},
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'
export interface CodeBlockOptions {
languageClassPrefix: string,
}
export type CodeBlockCommand = () => Command
// export type CodeBlockCommand = () => Command
declare module '@tiptap/core/src/Editor' {
interface Commands {
codeBlock: CodeBlockCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// codeBlock: CodeBlockCommand,
// }
// }
export const backtickInputRegex = /^```(?<language>[a-z]*)? $/
export const tildeInputRegex = /^~~~(?<language>[a-z]*)? $/
export default new Node<CodeBlockOptions>()
.name('code_block')
.defaults({
export default createNode({
name: 'code_block',
defaultOptions: <CodeBlockOptions>{
languageClassPrefix: 'language-',
})
.schema(({ options }) => ({
attrs: {
},
content: 'text*',
marks: '',
group: 'block',
code: true,
defining: true,
addAttributes() {
return {
language: {
default: null,
rendered: false,
},
},
content: 'text*',
marks: '',
group: 'block',
code: true,
defining: true,
draggable: false,
parseDOM: [
}
},
parseHTML() {
return [
{
tag: 'pre',
preserveWhitespace: 'full',
getAttrs(node) {
getAttrs: node => {
const classAttribute = (node as Element).firstElementChild?.getAttribute('class')
if (!classAttribute) {
return null
}
const regexLanguageClassPrefix = new RegExp(`^(${options.languageClassPrefix})`)
const regexLanguageClassPrefix = new RegExp(`^(${this.options.languageClassPrefix})`)
return { language: classAttribute.replace(regexLanguageClassPrefix, '') }
},
},
],
toDOM: node => ['pre', ['code', {
class: node.attrs.language && options.languageClassPrefix + node.attrs.language,
}, 0]],
}))
.commands(({ name }) => ({
codeBlock: attrs => ({ commands }) => {
return commands.toggleBlockType(name, 'paragraph', attrs)
},
}))
.keys(({ editor }) => ({
'Mod-Shift-c': () => editor.codeBlock(),
}))
.inputRules(({ type }) => [
textblockTypeInputRule(backtickInputRegex, type, ({ groups }: any) => groups),
textblockTypeInputRule(tildeInputRegex, type, ({ groups }: any) => groups),
])
.create()
]
},
renderHTML({ node, attributes }) {
return ['pre', attributes, ['code', {
class: node.attrs.language && this.options.languageClassPrefix + node.attrs.language,
}, 0]]
},
addCommands() {
return {
codeBlock: attrs => ({ commands }) => {
return commands.toggleBlockType('code_block', 'paragraph', attrs)
},
}
},
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 {
Command, Mark, markInputRule, markPasteRule,
Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core'
export type CodeCommand = () => Command
// export type CodeCommand = () => Command
declare module '@tiptap/core/src/Editor' {
interface Commands {
code: CodeCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// code: CodeCommand,
// }
// }
export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/gm
export const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/gm
export default new Mark()
.name('code')
.schema(() => ({
excludes: '_',
parseDOM: [
export default createMark({
name: 'code',
excludes: '_',
parseHTML() {
return [
{ tag: 'code' },
],
toDOM: () => ['code', 0],
}))
.commands(({ name }) => ({
code: () => ({ commands }) => {
return commands.toggleMark(name)
},
}))
.keys(({ editor }) => ({
'Mod-`': () => editor.code(),
}))
.inputRules(({ type }) => [
markInputRule(inputRegex, type),
])
.pasteRules(({ type }) => [
markPasteRule(inputRegex, type),
])
.create()
]
},
renderHTML({ attributes }) {
return ['strong', attributes, 0]
},
addCommands() {
return {
code: () => ({ commands }) => {
return commands.toggleMark('code')
},
}
},
addKeyboardShortcuts() {
return {
'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'
export interface CollaborationCursorOptions {
@ -8,27 +8,21 @@ export interface CollaborationCursorOptions {
render (user: { name: string, color: string }): HTMLElement,
}
export type UserCommand = (attributes: {
name: string,
color: string,
}) => Command
// export type UserCommand = (attributes: {
// name: string,
// color: string,
// }) => Command
declare module '@tiptap/core/src/Editor' {
interface Commands {
user: UserCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// user: UserCommand,
// }
// }
export default new Extension<CollaborationCursorOptions>()
.name('collaboration_cursor')
.commands(({ options }) => ({
user: attributes => () => {
options.provider.awareness.setLocalStateField('user', attributes)
export default createExtension({
name: 'collaboration_cursor',
return true
},
}))
.defaults({
defaultOptions: <CollaborationCursorOptions>{
provider: null,
name: 'Someone',
color: '#cccccc',
@ -45,19 +39,32 @@ export default new Extension<CollaborationCursorOptions>()
return cursor
},
})
.plugins(({ options }) => [
yCursorPlugin((() => {
options.provider.awareness.setLocalStateField('user', {
name: options.name,
color: options.color,
})
},
return options.provider.awareness
})(),
// @ts-ignore
{
cursorBuilder: options.render,
}),
])
.create()
addCommands() {
return {
user: attributes => () => {
this.options.provider.awareness.setLocalStateField('user', attributes)
return true
},
}
},
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 {
redo, undo, ySyncPlugin, yUndoPlugin,
} from 'y-prosemirror'
@ -8,21 +8,26 @@ export interface CollaborationOptions {
type: any,
}
export default new Extension<CollaborationOptions>()
.name('collaboration')
.defaults({
export default createExtension({
name: 'collaboration',
defaultOptions: <CollaborationOptions>{
provider: null,
type: null,
})
.plugins(({ options }) => [
ySyncPlugin(options.type),
yUndoPlugin(),
])
.keys(() => {
},
addProseMirrorPlugins() {
return [
ySyncPlugin(this.options.type),
yUndoPlugin(),
]
},
addKeyboardShortcuts() {
return {
'Mod-z': undo,
'Mod-y': 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 { DecorationSet, Decoration } from 'prosemirror-view'
@ -7,40 +7,44 @@ export interface FocusOptions {
nested: boolean,
}
export default new Extension<FocusOptions>()
.name('focus')
.defaults({
export default createExtension({
name: 'focus',
defaultOptions: <FocusOptions>{
className: 'has-focus',
nested: false,
})
.plugins(({ editor, options }) => [
new Plugin({
props: {
decorations: ({ doc, selection }) => {
const { isEditable, isFocused } = editor
const { anchor } = selection
const decorations: Decoration[] = []
},
if (!isEditable || !isFocused) {
return DecorationSet.create(doc, [])
}
addProseMirrorPlugins() {
return [
new Plugin({
props: {
decorations: ({ doc, selection }) => {
const { isEditable, isFocused } = this.editor
const { anchor } = selection
const decorations: Decoration[] = []
doc.descendants((node, pos) => {
const hasAnchor = anchor >= pos && anchor <= (pos + node.nodeSize)
if (hasAnchor && !node.isText) {
const decoration = Decoration.node(pos, pos + node.nodeSize, {
class: options.className,
})
decorations.push(decoration)
if (!isEditable || !isFocused) {
return DecorationSet.create(doc, [])
}
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'
export type HardBreakCommand = () => Command
// export type HardBreakCommand = () => Command
declare module '@tiptap/core/src/Editor' {
interface Commands {
hardBreak: HardBreakCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// hardBreak: HardBreakCommand,
// }
// }
export default new Node()
.name('hardBreak')
.schema(() => ({
inline: true,
group: 'inline',
selectable: false,
parseDOM: [
export default createNode({
name: 'hardBreak',
inline: true,
group: 'inline',
selectable: false,
parseHTML() {
return [
{ tag: 'br' },
],
toDOM: () => ['br'],
}))
.commands(({ type }) => ({
hardBreak: () => ({
tr, state, dispatch, view,
}) => {
return chainCommands(exitCode, () => {
dispatch(tr.replaceSelectionWith(type.create()).scrollIntoView())
return true
})(state, dispatch, view)
},
}))
.keys(({ editor }) => ({
'Mod-Enter': () => editor.hardBreak(),
'Shift-Enter': () => editor.hardBreak(),
}))
.create()
]
},
renderHTML({ attributes }) {
return ['br', attributes]
},
addCommands() {
return {
hardBreak: () => ({
tr, state, dispatch, view,
}) => {
return chainCommands(exitCode, () => {
dispatch(tr.replaceSelectionWith(this.type.create()).scrollIntoView())
return true
})(state, dispatch, view)
},
}
},
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'
type Level = 1 | 2 | 3 | 4 | 5 | 6
@ -7,52 +7,68 @@ export interface HeadingOptions {
levels: Level[],
}
export type HeadingCommand = (options: { level: Level }) => Command
// export type HeadingCommand = (options: { level: Level }) => Command
declare module '@tiptap/core/src/Editor' {
interface Commands {
heading: HeadingCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// heading: HeadingCommand,
// }
// }
export default new Node<HeadingOptions>()
.name('heading')
.defaults({
export default createNode({
name: 'heading',
defaultOptions: <HeadingOptions>{
levels: [1, 2, 3, 4, 5, 6],
})
.schema(({ options }) => ({
attrs: {
},
content: 'inline*',
group: 'block',
defining: true,
addAttributes() {
return {
level: {
default: 1,
rendered: false,
},
},
content: 'inline*',
group: 'block',
defining: true,
draggable: false,
parseDOM: options.levels
}
},
parseHTML() {
return this.options.levels
.map((level: Level) => ({
tag: `h${level}`,
attrs: { level },
})),
toDOM: node => [`h${node.attrs.level}`, 0],
}))
.commands(({ name }) => ({
heading: attrs => ({ commands }) => {
return commands.toggleBlockType(name, 'paragraph', attrs)
},
}))
.keys(({ name, options, editor }) => {
return options.levels.reduce((items, level) => ({
}))
},
renderHTML({ node, attributes }) {
return [`h${node.attrs.level}`, attributes, 0]
},
addCommands() {
return {
heading: attrs => ({ commands }) => {
return commands.toggleBlockType('heading', 'paragraph', attrs)
},
}
},
addKeyboardShortcuts() {
return this.options.levels.reduce((items, level) => ({
...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) => {
return textblockTypeInputRule(new RegExp(`^(#{1,${level}})\\s$`), type, { level })
},
addInputRules() {
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 {
history,
undo,
redo,
} from 'prosemirror-history'
declare module '@tiptap/core/src/Editor' {
interface Commands {
undo: () => Command,
redo: () => Command,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// undo: () => Command,
// redo: () => Command,
// }
// }
export interface HistoryOptions {
depth: number,
newGroupDelay: number,
}
export default new Extension<HistoryOptions>()
.name('history')
.defaults({
export default createExtension({
name: 'history',
defaultOptions: <HistoryOptions>{
depth: 100,
newGroupDelay: 500,
})
.commands(() => ({
undo: () => ({ state, dispatch }) => {
return undo(state, dispatch)
},
redo: () => ({ state, dispatch }) => {
return redo(state, dispatch)
},
}))
.keys(({ editor }) => ({
'Mod-z': () => editor.undo(),
'Mod-y': () => editor.redo(),
'Shift-Mod-z': () => editor.redo(),
}))
.plugins(({ options }) => [
history(options),
])
.create()
},
addCommands() {
return {
undo: () => ({ state, dispatch }) => {
return undo(state, dispatch)
},
redo: () => ({ state, dispatch }) => {
return redo(state, dispatch)
},
}
},
addProseMirrorPlugins() {
return [
history(this.options),
]
},
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' {
interface Commands {
horizontalRule: HorizontalRuleCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// horizontalRule: HorizontalRuleCommand,
// }
// }
export default new Node()
.name('horizontalRule')
.schema(() => ({
group: 'block',
parseDOM: [{ tag: 'hr' }],
toDOM: () => ['hr'],
}))
.commands(({ type }) => ({
horizontalRule: () => ({ tr }) => {
tr.replaceSelectionWith(type.create())
export default createNode({
name: 'horizontalRule',
return true
},
}))
.inputRules(({ type }) => [
nodeInputRule(/^(?:---|___\s|\*\*\*\s)$/, type),
])
.create()
group: 'block',
parseHTML() {
return [
{ tag: 'hr' },
]
},
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 {
Command, Mark, markInputRule, markPasteRule,
Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core'
export type ItalicCommand = () => Command
// export type ItalicCommand = () => Command
declare module '@tiptap/core/src/Editor' {
interface Commands {
italic: ItalicCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// italic: ItalicCommand,
// }
// }
export const starInputRegex = /(?:^|\s)((?:\*)((?:[^*]+))(?:\*))$/gm
export const starPasteRegex = /(?:^|\s)((?:\*)((?:[^*]+))(?:\*))/gm
export const underscoreInputRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))$/gm
export const underscorePasteRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))/gm
export default new Mark()
.name('italic')
.schema(() => ({
parseDOM: [
export default createMark({
name: 'italic',
parseHTML() {
return [
{
tag: 'em',
},
@ -29,23 +30,38 @@ export default new Mark()
{
style: 'font-style=italic',
},
],
toDOM: () => ['em', 0],
}))
.commands(({ name }) => ({
italic: () => ({ commands }) => {
return commands.toggleMark(name)
},
}))
.keys(({ editor }) => ({
'Mod-i': () => editor.italic(),
}))
.inputRules(({ type }) => [
markInputRule(starInputRegex, type),
markInputRule(underscoreInputRegex, type),
])
.pasteRules(({ type }) => [
markPasteRule(starPasteRegex, type),
markPasteRule(underscorePasteRegex, type),
])
.create()
]
},
renderHTML({ attributes }) {
return ['em', attributes, 0]
},
addCommands() {
return {
italic: () => ({ commands }) => {
return commands.toggleMark('italic')
},
}
},
addKeyboardShortcuts() {
return {
'Mod-i': () => this.editor.italic(),
}
},
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 {
Command, Mark, markPasteRule,
Command, createMark, markPasteRule,
} from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
@ -9,34 +9,42 @@ export interface LinkOptions {
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' {
interface Commands {
link: LinkCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// link: LinkCommand,
// }
// }
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>()
.name('link')
.defaults({
export default createMark({
name: 'link',
inclusive: false,
defaultOptions: <LinkOptions>{
openOnClick: true,
target: '_blank',
rel: 'noopener noreferrer nofollow',
})
.schema(({ options }) => ({
attrs: {
},
addAttributes() {
return {
href: {
default: null,
rendered: false,
},
target: {
default: null,
rendered: false,
},
},
inclusive: false,
parseDOM: [
}
},
parseHTML() {
return [
{
tag: 'a[href]',
getAttrs: node => ({
@ -44,27 +52,44 @@ export default new Mark<LinkOptions>()
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)
},
}))
.pasteRules(({ type }) => [
markPasteRule(pasteRegex, type, (url: string) => ({ href: url })),
])
.plugins(({ editor, options, name }) => {
if (!options.openOnClick) {
renderHTML({ mark, attributes }) {
return ['a', {
...attributes,
...mark.attrs,
rel: this.options.rel,
target: mark.attrs.target ? mark.attrs.target : this.options.target,
}, 0]
},
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 []
}
@ -73,7 +98,7 @@ export default new Mark<LinkOptions>()
key: new PluginKey('handleClick'),
props: {
handleClick: (view, pos, event) => {
const attrs = editor.getMarkAttrs(name)
const attrs = this.editor.getMarkAttrs('link')
if (attrs.href && event.target instanceof HTMLAnchorElement) {
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()
.name('list_item')
.schema(() => ({
content: 'paragraph block*',
defining: true,
draggable: false,
parseDOM: [{ tag: 'li' }],
toDOM: () => ['li', 0],
}))
.keys(({ editor, name }) => ({
Enter: () => editor.splitListItem(name),
Tab: () => editor.sinkListItem(name),
'Shift-Tab': () => editor.liftListItem(name),
}))
.create()
export default createNode({
name: 'list_item',
content: 'paragraph block*',
defining: true,
parseHTML() {
return [
{ tag: 'li' },
]
},
renderHTML({ attributes }) {
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'
export type OrderedListCommand = () => Command
// export type OrderedListCommand = () => Command
declare module '@tiptap/core/src/Editor' {
interface Commands {
orderedList: OrderedListCommand,
}
}
// declare module '@tiptap/core/src/Editor' {
// interface Commands {
// orderedList: OrderedListCommand,
// }
// }
export default new Node()
.name('ordered_list')
.schema(() => ({
attrs: {
export default createNode({
name: 'ordered_list',
content: 'list_item+',
group: 'block',
addAttributes() {
return {
order: {
default: 1,
rendered: false,
},
},
content: 'list_item+',
group: 'block',
parseDOM: [{
tag: 'ol',
getAttrs: node => ({
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 }) => ({
orderedList: () => ({ commands }) => {
return commands.toggleList(name, 'list_item')
},
}))
.keys(({ editor }) => ({
'Shift-Control-9': () => editor.orderedList(),
}))
.inputRules(({ type }) => [
wrappingInputRule(
/^(\d+)\.\s$/,
type,
match => ({ order: +match[1] }),
(match, node) => node.childCount + node.attrs.order === +match[1],
),
])
.create()
}
},
parseHTML() {
return [
{
tag: 'ol',
getAttrs: node => ({
order: (node as HTMLElement).hasAttribute('start')
? parseInt((node as HTMLElement).getAttribute('start') || '', 10)
: 1,
}),
},
]
},
renderHTML({ node, attributes }) {
return node.attrs.order === 1
? ['ol', attributes, 0]
: ['ol', { ...attributes, start: node.attrs.order }, 0]
},
addCommands() {
return {
orderedList: () => ({ commands }) => {
return commands.toggleList('ordered_list', 'list_item')
},
}
},
addKeyboardShortcuts() {
return {
'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*',
addGlobalAttributes() {
return [
{
types: ['paragraph'],
attributes: {
align: {
default: 'right',
renderHTML: attributes => ({
class: 'global',
style: `text-align: ${attributes.align}`,
}),
},
},
},
]
},
// addGlobalAttributes() {
// return [
// {
// types: ['paragraph'],
// attributes: {
// align: {
// default: 'right',
// renderHTML: attributes => ({
// class: 'global',
// style: `text-align: ${attributes.align}`,
// }),
// },
// },
// },
// ]
// },
addAttributes() {
return {
id: {
default: '123',
rendered: true,
renderHTML: attributes => ({
class: `foo-${attributes.id}`,
id: 'foo',
}),
},
}
},
// addAttributes() {
// return {
// id: {
// default: '123',
// rendered: true,
// renderHTML: attributes => ({
// class: `foo-${attributes.id}`,
// id: 'foo',
// }),
// },
// }
// },
parseHTML() {
return [

View File

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

View File

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