Merge branch 'main' into feature/new-highlight-extension

# Conflicts:
#	packages/core/src/commands/toggleMark.ts
This commit is contained in:
Hans Pagel
2020-10-27 20:36:22 +01:00
164 changed files with 4827 additions and 2779 deletions

View File

@@ -3,10 +3,11 @@ import { Editor, Command, CommandsSpec } from './src/Editor'
export default Editor
export { Editor, Command, CommandsSpec }
export { default as ComponentRenderer } from './src/ComponentRenderer'
export { default as Extension } from './src/Extension'
export { default as Node } from './src/Node'
export { default as Mark } from './src/Mark'
export { Extensions } from './src/types'
export * from './src/Extension'
export * from './src/NodeExtension'
export * from './src/MarkExtension'
export * from './src/types'
export { default as nodeInputRule } from './src/inputRules/nodeInputRule'
export { default as markInputRule } from './src/inputRules/markInputRule'
@@ -16,7 +17,5 @@ export { default as capitalize } from './src/utils/capitalize'
export { default as getSchema } from './src/utils/getSchema'
export { default as generateHtml } from './src/utils/generateHtml'
export { default as getHtmlFromFragment } from './src/utils/getHtmlFromFragment'
export { default as getTopNodeFromExtensions } from './src/utils/getTopNodeFromExtensions'
export { default as getNodesFromExtensions } from './src/utils/getNodesFromExtensions'
export { default as getMarksFromExtensions } from './src/utils/getMarksFromExtensions'
export { default as getMarkAttrs } from './src/utils/getMarkAttrs'
export { default as mergeAttributes } from './src/utils/mergeAttributes'

View File

@@ -12,24 +12,20 @@
"dist"
],
"dependencies": {
"@types/clone-deep": "^4.0.1",
"@types/prosemirror-dropcursor": "^1.0.0",
"@types/prosemirror-gapcursor": "^1.0.1",
"@types/prosemirror-schema-list": "^1.0.1",
"clone-deep": "^4.0.1",
"collect.js": "^4.28.2",
"deepmerge": "^4.2.2",
"prosemirror-commands": "^1.1.3",
"prosemirror-dropcursor": "^1.3.2",
"prosemirror-gapcursor": "^1.1.5",
"prosemirror-inputrules": "^1.1.3",
"prosemirror-keymap": "^1.1.3",
"prosemirror-model": "^1.11.2",
"prosemirror-model": "^1.12.0",
"prosemirror-schema-list": "^1.1.4",
"prosemirror-state": "^1.3.3",
"prosemirror-tables": "^1.1.1",
"prosemirror-utils": "^0.9.6",
"prosemirror-view": "^1.16.0"
"prosemirror-utils": "^1.0.0-0",
"prosemirror-view": "^1.16.1"
},
"scripts": {
"build": "microbundle"

View File

@@ -8,17 +8,15 @@ import markIsActive from './utils/markIsActive'
import getNodeAttrs from './utils/getNodeAttrs'
import getMarkAttrs from './utils/getMarkAttrs'
import removeElement from './utils/removeElement'
import getSchemaTypeByName from './utils/getSchemaTypeByName'
import getSchemaTypeNameByName from './utils/getSchemaTypeNameByName'
import getHtmlFromFragment from './utils/getHtmlFromFragment'
import createStyleTag from './utils/createStyleTag'
import CommandManager from './CommandManager'
import ExtensionManager from './ExtensionManager'
import EventEmitter from './EventEmitter'
import Extension from './Extension'
import Node from './Node'
import Mark from './Mark'
import { Extensions, UnionToIntersection, PickValue } from './types'
import defaultPlugins from './plugins'
import * as coreCommands from './commands'
import * as extensions from './extensions'
import style from './style'
export type Command = (props: {
@@ -37,19 +35,19 @@ export interface CommandsSpec {
[key: string]: CommandSpec
}
export interface Commands {}
export interface AllExtensions {}
export type CommandNames = Extract<keyof Commands, string>
export type AllCommands = UnionToIntersection<ReturnType<PickValue<ReturnType<AllExtensions[keyof AllExtensions]>, 'addCommands'>>>
export type SingleCommands = {
[Item in keyof Commands]: Commands[Item] extends (...args: any[]) => any
? (...args: Parameters<Commands[Item]>) => boolean
[Item in keyof AllCommands]: AllCommands[Item] extends (...args: any[]) => any
? (...args: Parameters<AllCommands[Item]>) => boolean
: never
}
export type ChainedCommands = {
[Item in keyof Commands]: Commands[Item] extends (...args: any[]) => any
? (...args: Parameters<Commands[Item]>) => ChainedCommands
[Item in keyof AllCommands]: AllCommands[Item] extends (...args: any[]) => any
? (...args: Parameters<AllCommands[Item]>) => ChainedCommands
: never
} & {
run: () => boolean
@@ -64,7 +62,7 @@ interface HTMLElement {
interface EditorOptions {
element: Element,
content: EditorContent,
extensions: (Extension | Node | Mark)[],
extensions: Extensions,
injectCSS: boolean,
autoFocus: 'start' | 'end' | number | boolean | null,
editable: boolean,
@@ -117,12 +115,11 @@ export class Editor extends EventEmitter {
this.createCommandManager()
this.createExtensionManager()
this.createSchema()
this.extensionManager.resolveConfigs()
this.createView()
this.registerCommands(coreCommands)
// this.registerCommands(coreCommands)
this.injectCSS()
this.proxy.focus(this.options.autoFocus)
window.setTimeout(() => this.proxy.focus(this.options.autoFocus), 0)
}
/**
@@ -235,7 +232,10 @@ export class Editor extends EventEmitter {
* Creates an extension manager.
*/
private createExtensionManager() {
this.extensionManager = new ExtensionManager(this.options.extensions, this.proxy)
const coreExtensions = Object.entries(extensions).map(([, extension]) => extension())
const allExtensions = [...coreExtensions, ...this.options.extensions]
this.extensionManager = new ExtensionManager(allExtensions, this.proxy)
}
/**
@@ -346,7 +346,7 @@ export class Editor extends EventEmitter {
* @param attrs Attributes of the node or mark
*/
public isActive(name: string, attrs = {}) {
const schemaType = getSchemaTypeByName(name, this.schema)
const schemaType = getSchemaTypeNameByName(name, this.schema)
if (schemaType === 'node') {
return nodeIsActive(this.state, this.schema.nodes[name], attrs)
@@ -376,17 +376,24 @@ export class Editor extends EventEmitter {
/**
* Get the document as JSON.
*/
public json() {
public getJSON() {
return this.state.doc.toJSON()
}
/**
* Get the document as HTML.
*/
public html() {
public getHTML() {
return getHtmlFromFragment(this.state.doc, this.schema)
}
/**
* Check if there is no content.
*/
public isEmpty() {
return !this.state.doc.textContent.length
}
/**
* Destroy the editor.
*/

View File

@@ -1,114 +1,113 @@
import cloneDeep from 'clone-deep'
import { Plugin } from 'prosemirror-state'
import { Editor, CommandsSpec } from './Editor'
import { Editor } from './Editor'
import { GlobalAttributes } from './types'
type AnyObject = {
[key: string]: any
export interface ExtensionSpec<Options = {}, Commands = {}> {
/**
* Name
*/
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[],
}
type NoInfer<T> = [T][T extends any ? 0 : never]
/**
* Extension interface for internal usage
*/
export type Extension = Required<Omit<ExtensionSpec, 'defaultOptions'> & {
type: string,
options: {
[key: string]: any
},
}>
type MergeStrategy = 'extend' | 'overwrite'
type Configs = {
[key: string]: {
stategy: MergeStrategy
value: any
}[]
/**
* Default extension
*/
export const defaultExtension: Extension = {
name: 'extension',
type: 'extension',
options: {},
addGlobalAttributes: () => [],
addCommands: () => ({}),
addKeyboardShortcuts: () => ({}),
addInputRules: () => [],
addPasteRules: () => [],
addProseMirrorPlugins: () => [],
}
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
export function createExtension<Options extends {}, Commands extends {}>(config: ExtensionSpec<Options, Commands>) {
const extend = <ExtendedOptions = Options, ExtendedCommands = Commands>(extendedConfig: Partial<ExtensionSpec<ExtendedOptions, ExtendedCommands>>) => {
return createExtension({
...config,
...extendedConfig,
} as ExtensionSpec<ExtendedOptions, ExtendedCommands>)
}
plugins: (params: Props) => Plugin[]
}
export default class Extension<
Options = {},
Props = ExtensionProps<Options>,
Methods extends ExtensionMethods<Props, Options> = ExtensionMethods<Props, Options>,
> {
type = 'extension'
const setOptions = (options?: Partial<Options>) => {
const { defaultOptions, ...rest } = config
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]
return {
...defaultExtension,
...rest,
options: {
...defaultOptions,
...options,
} as Options,
}
}
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)
}
}
return Object.assign(setOptions, { config, extend })
}

View File

@@ -1,86 +1,53 @@
import collect from 'collect.js'
import { Plugin } from 'prosemirror-state'
import { keymap } from 'prosemirror-keymap'
import { Schema } from 'prosemirror-model'
// import { Schema, Node as ProsemirrorNode } from 'prosemirror-model'
import { inputRules } from 'prosemirror-inputrules'
// import { EditorView, Decoration } from 'prosemirror-view'
import { Schema } from 'prosemirror-model'
import { Editor } from './Editor'
// import capitalize from './utils/capitalize'
import { Extensions } from './types'
import getTopNodeFromExtensions from './utils/getTopNodeFromExtensions'
import getNodesFromExtensions from './utils/getNodesFromExtensions'
import getMarksFromExtensions from './utils/getMarksFromExtensions'
import resolveExtensionConfig from './utils/resolveExtensionConfig'
import getSchema from './utils/getSchema'
import getSchemaTypeByName from './utils/getSchemaTypeByName'
export default class ExtensionManager {
editor: Editor
schema: Schema
extensions: Extensions
constructor(extensions: Extensions, editor: Editor) {
this.editor = editor
this.extensions = extensions
}
this.schema = getSchema(this.extensions)
resolveConfigs() {
this.extensions.forEach(extension => {
const { editor } = this
const { name } = extension.config
const options = {
...extension.config.defaults,
...extension.options,
const context = {
options: extension.options,
editor: this.editor,
type: getSchemaTypeByName(extension.name, this.schema),
}
const type = extension.type === 'node'
? editor.schema.nodes[name]
: editor.schema.marks[name]
resolveExtensionConfig(extension, 'commands', {
name, options, editor, type,
})
resolveExtensionConfig(extension, 'inputRules', {
name, options, editor, type,
})
resolveExtensionConfig(extension, 'pasteRules', {
name, options, editor, type,
})
resolveExtensionConfig(extension, 'keys', {
name, options, editor, type,
})
resolveExtensionConfig(extension, 'plugins', {
name, options, editor, type,
})
const commands = extension.addCommands.bind(context)()
if (extension.config.commands) {
editor.registerCommands(extension.config.commands)
}
editor.registerCommands(commands)
})
}
get schema(): Schema {
return getSchema(this.extensions)
}
get topNode(): any {
return getTopNodeFromExtensions(this.extensions)
}
get nodes(): any {
return getNodesFromExtensions(this.extensions)
}
get marks(): any {
return getMarksFromExtensions(this.extensions)
}
get plugins(): Plugin[] {
const plugins = collect(this.extensions)
.flatMap(extension => extension.config.plugins)
.filter(plugin => plugin)
.toArray()
const plugins = this.extensions
.map(extension => {
const context = {
options: extension.options,
editor: this.editor,
type: getSchemaTypeByName(extension.name, this.schema),
}
return extension.addProseMirrorPlugins.bind(context)()
})
.flat()
return [
...plugins,
@@ -91,25 +58,43 @@ export default class ExtensionManager {
}
get inputRules(): any {
return collect(this.extensions)
.flatMap(extension => extension.config.inputRules)
.filter(plugin => plugin)
.toArray()
return this.extensions
.map(extension => {
const context = {
options: extension.options,
editor: this.editor,
type: getSchemaTypeByName(extension.name, this.schema),
}
return extension.addInputRules.bind(context)()
})
.flat()
}
get pasteRules(): any {
return collect(this.extensions)
.flatMap(extension => extension.config.pasteRules)
.filter(plugin => plugin)
.toArray()
return this.extensions
.map(extension => {
const context = {
options: extension.options,
editor: this.editor,
type: getSchemaTypeByName(extension.name, this.schema),
}
return extension.addPasteRules.bind(context)()
})
.flat()
}
get keymaps() {
return collect(this.extensions)
.map(extension => extension.config.keys)
.filter(keys => keys)
.map(keys => keymap(keys))
.toArray()
return this.extensions.map(extension => {
const context = {
options: extension.options,
editor: this.editor,
type: getSchemaTypeByName(extension.name, this.schema),
}
return keymap(extension.addKeyboardShortcuts.bind(context)())
})
}
get nodeViews() {

View File

@@ -1,28 +0,0 @@
import { MarkSpec, MarkType } from 'prosemirror-model'
import Extension, { ExtensionMethods } from './Extension'
import { Editor } from './Editor'
export interface MarkProps<Options> {
name: string
editor: Editor
options: Options
type: MarkType
}
export interface MarkMethods<Props, Options> extends ExtensionMethods<Props, Options> {
topMark: boolean
schema: (params: Omit<Props, 'type' | 'editor'>) => MarkSpec
}
export default class Mark<
Options = {},
Props = MarkProps<Options>,
Methods extends MarkMethods<Props, Options> = MarkMethods<Props, Options>,
> extends Extension<Options, Props, Methods> {
type = 'mark'
public schema(value: Methods['schema']) {
this.storeConfig('schema', value, 'overwrite')
return this
}
}

View File

@@ -0,0 +1,151 @@
import {
DOMOutputSpec, MarkSpec, Mark, MarkType,
} from 'prosemirror-model'
import { Plugin } from 'prosemirror-state'
import { ExtensionSpec, defaultExtension } from './Extension'
import { Attributes, Overwrite } from './types'
import { Editor } from './Editor'
export interface MarkExtensionSpec<Options = {}, Commands = {}> extends Overwrite<ExtensionSpec<Options, Commands>, {
/**
* Inclusive
*/
inclusive?: MarkSpec['inclusive'],
/**
* Excludes
*/
excludes?: MarkSpec['excludes'],
/**
* Group
*/
group?: MarkSpec['group'],
/**
* Spanning
*/
spanning?: MarkSpec['spanning'],
/**
* Parse HTML
*/
parseHTML?: (
this: {
options: Options,
},
) => MarkSpec['parseDOM'],
/**
* Render HTML
*/
renderHTML?: ((
this: {
options: Options,
},
props: {
mark: Mark,
attributes: { [key: string]: any },
}
) => DOMOutputSpec) | null,
/**
* Attributes
*/
addAttributes?: (
this: {
options: Options,
},
) => Attributes,
/**
* Commands
*/
addCommands?: (this: {
options: Options,
editor: Editor,
type: MarkType,
}) => Commands,
/**
* Keyboard shortcuts
*/
addKeyboardShortcuts?: (this: {
options: Options,
editor: Editor,
type: MarkType,
}) => {
[key: string]: any
},
/**
* Input rules
*/
addInputRules?: (this: {
options: Options,
editor: Editor,
type: MarkType,
}) => any[],
/**
* Paste rules
*/
addPasteRules?: (this: {
options: Options,
editor: Editor,
type: MarkType,
}) => any[],
/**
* ProseMirror plugins
*/
addProseMirrorPlugins?: (this: {
options: Options,
editor: Editor,
type: MarkType,
}) => Plugin[],
}> {}
export type MarkExtension = Required<Omit<MarkExtensionSpec, 'defaultOptions'> & {
type: string,
options: {
[key: string]: any
},
}>
const defaultMark: MarkExtension = {
...defaultExtension,
type: 'mark',
name: 'mark',
inclusive: null,
excludes: null,
group: null,
spanning: null,
parseHTML: () => null,
renderHTML: null,
addAttributes: () => ({}),
}
export function createMark<Options extends {}, Commands extends {}>(config: MarkExtensionSpec<Options, Commands>) {
const extend = <ExtendedOptions = Options, ExtendedCommands = Commands>(extendedConfig: Partial<MarkExtensionSpec<ExtendedOptions, ExtendedCommands>>) => {
return createMark({
...config,
...extendedConfig,
} as MarkExtensionSpec<ExtendedOptions, ExtendedCommands>)
}
const setOptions = (options?: Partial<Options>) => {
const { defaultOptions, ...rest } = config
return {
...defaultMark,
...rest,
options: {
...defaultOptions,
...options,
} as Options,
}
}
return Object.assign(setOptions, { config, extend })
}

View File

@@ -1,33 +0,0 @@
import { NodeSpec, NodeType } from 'prosemirror-model'
import Extension, { ExtensionMethods } from './Extension'
import { Editor } from './Editor'
export interface NodeProps<Options> {
name: string
editor: Editor
options: Options
type: NodeType
}
export interface NodeMethods<Props, Options> extends ExtensionMethods<Props, Options> {
topNode: boolean
schema: (params: Omit<Props, 'type' | 'editor'>) => NodeSpec
}
export default class Node<
Options = {},
Props = NodeProps<Options>,
Methods extends NodeMethods<Props, Options> = NodeMethods<Props, Options>,
> extends Extension<Options, Props, Methods> {
type = 'node'
public topNode(value: Methods['topNode'] = true) {
this.storeConfig('topNode', value, 'overwrite')
return this
}
public schema(value: Methods['schema']) {
this.storeConfig('schema', value, 'overwrite')
return this
}
}

View File

@@ -0,0 +1,193 @@
import {
DOMOutputSpec, NodeSpec, Node, NodeType,
} from 'prosemirror-model'
import { Plugin } from 'prosemirror-state'
import { ExtensionSpec, defaultExtension } from './Extension'
import { Attributes, Overwrite } from './types'
import { Editor } from './Editor'
export interface NodeExtensionSpec<Options = {}, Commands = {}> extends Overwrite<ExtensionSpec<Options, Commands>, {
/**
* TopNode
*/
topNode?: boolean,
/**
* Content
*/
content?: NodeSpec['content'],
/**
* Marks
*/
marks?: NodeSpec['marks'],
/**
* Group
*/
group?: NodeSpec['group'],
/**
* Inline
*/
inline?: NodeSpec['inline'],
/**
* Atom
*/
atom?: NodeSpec['atom'],
/**
* Selectable
*/
selectable?: NodeSpec['selectable'],
/**
* Draggable
*/
draggable?: NodeSpec['draggable'],
/**
* Code
*/
code?: NodeSpec['code'],
/**
* Defining
*/
defining?: NodeSpec['defining'],
/**
* Isolating
*/
isolating?: NodeSpec['isolating'],
/**
* Parse HTML
*/
parseHTML?: (
this: {
options: Options,
},
) => NodeSpec['parseDOM'],
/**
* Render HTML
*/
renderHTML?: ((
this: {
options: Options,
},
props: {
node: Node,
attributes: { [key: string]: any },
}
) => DOMOutputSpec) | null,
/**
* Add Attributes
*/
addAttributes?: (
this: {
options: Options,
},
) => Attributes,
/**
* Commands
*/
addCommands?: (this: {
options: Options,
editor: Editor,
type: NodeType,
}) => Commands,
/**
* Keyboard shortcuts
*/
addKeyboardShortcuts?: (this: {
options: Options,
editor: Editor,
type: NodeType,
}) => {
[key: string]: any
},
/**
* Input rules
*/
addInputRules?: (this: {
options: Options,
editor: Editor,
type: NodeType,
}) => any[],
/**
* Paste rules
*/
addPasteRules?: (this: {
options: Options,
editor: Editor,
type: NodeType,
}) => any[],
/**
* ProseMirror plugins
*/
addProseMirrorPlugins?: (this: {
options: Options,
editor: Editor,
type: NodeType,
}) => Plugin[],
}> {}
export type NodeExtension = Required<Omit<NodeExtensionSpec, 'defaultOptions'> & {
type: string,
options: {
[key: string]: any
},
}>
const defaultNode: NodeExtension = {
...defaultExtension,
type: 'node',
name: 'node',
topNode: false,
content: null,
marks: null,
group: null,
inline: null,
atom: null,
selectable: null,
draggable: null,
code: null,
defining: null,
isolating: null,
parseHTML: () => null,
renderHTML: null,
addAttributes: () => ({}),
}
export function createNode<Options extends {}, Commands extends {}>(config: NodeExtensionSpec<Options, Commands>) {
const extend = <ExtendedOptions = Options, ExtendedCommands = Commands>(extendedConfig: Partial<NodeExtensionSpec<ExtendedOptions, ExtendedCommands>>) => {
return createNode({
...config,
...extendedConfig,
} as NodeExtensionSpec<ExtendedOptions, ExtendedCommands>)
}
const setOptions = (options?: Partial<Options>) => {
const { defaultOptions, ...rest } = config
return {
...defaultNode,
...rest,
options: {
...defaultOptions,
...options,
} as Options,
}
}
return Object.assign(setOptions, { config, extend })
}

View File

@@ -1,17 +0,0 @@
import { Command } from '../Editor'
type BlurCommand = () => Command
declare module '../Editor' {
interface Commands {
blur: BlurCommand,
}
}
export const blur: BlurCommand = () => ({ view }) => {
const element = view.dom as HTMLElement
element.blur()
return true
}

View File

@@ -1,13 +0,0 @@
import { Command } from '../Editor'
type ClearContentCommand = (emitUpdate?: Boolean) => Command
declare module '../Editor' {
interface Commands {
clearContent: ClearContentCommand,
}
}
export const clearContent: ClearContentCommand = (emitUpdate = false) => ({ commands }) => {
return commands.setContent('', emitUpdate)
}

View File

@@ -1,14 +0,0 @@
import { deleteSelection as originalDeleteSelection } from 'prosemirror-commands'
import { Command } from '../Editor'
type DeleteSelectionCommand = () => Command
declare module '../Editor' {
interface Commands {
deleteSelection: DeleteSelectionCommand,
}
}
export const deleteSelection: DeleteSelectionCommand = () => ({ state, dispatch }) => {
return originalDeleteSelection(state, dispatch)
}

View File

@@ -1,21 +0,0 @@
export { blur } from './blur'
export { clearContent } from './clearContent'
export { deleteSelection } from './deleteSelection'
export { focus } from './focus'
export { insertHTML } from './insertHTML'
export { insertText } from './insertText'
export { liftListItem } from './liftListItem'
export { removeMark } from './removeMark'
export { removeMarks } from './removeMarks'
export { scrollIntoView } from './scrollIntoView'
export { selectAll } from './selectAll'
export { selectParentNode } from './selectParentNode'
export { setBlockType } from './setBlockType'
export { setContent } from './setContent'
export { sinkListItem } from './sinkListItem'
export { splitListItem } from './splitListItem'
export { toggleBlockType } from './toggleBlockType'
export { toggleList } from './toggleList'
export { toggleMark } from './toggleMark'
export { updateMark } from './updateMark'
export { toggleWrap } from './toggleWrap'

View File

@@ -1,15 +0,0 @@
import { Command } from '../Editor'
type InsertTextCommand = (value: string) => Command
declare module '../Editor' {
interface Commands {
insertText: InsertTextCommand,
}
}
export const insertText: InsertTextCommand = value => ({ tr }) => {
tr.insertText(value)
return true
}

View File

@@ -1,18 +0,0 @@
import { liftListItem as originalLiftListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
type LiftListItem = (typeOrName: string | NodeType) => Command
declare module '../Editor' {
interface Commands {
liftListItem: LiftListItem,
}
}
export const liftListItem: LiftListItem = typeOrName => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return originalLiftListItem(type)(state, dispatch)
}

View File

@@ -1,32 +0,0 @@
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import getMarkType from '../utils/getMarkType'
import getMarkRange from '../utils/getMarkRange'
type RemoveMarkCommand = (typeOrName: string | MarkType) => Command
declare module '../Editor' {
interface Commands {
removeMark: RemoveMarkCommand,
}
}
export const removeMark: RemoveMarkCommand = typeOrName => ({ tr, state }) => {
const { selection } = tr
const type = getMarkType(typeOrName, state.schema)
let { from, to } = selection
const { $from, empty } = selection
if (empty) {
const range = getMarkRange($from, type)
if (range) {
from = range.from
to = range.to
}
}
tr.removeMark(from, to, type)
return true
}

View File

@@ -1,26 +0,0 @@
import { Command } from '../Editor'
type RemoveMarksCommand = () => Command
declare module '../Editor' {
interface Commands {
removeMarks: RemoveMarksCommand,
}
}
export const removeMarks: RemoveMarksCommand = () => ({ tr, state }) => {
const { selection } = tr
const { from, to, empty } = selection
if (empty) {
return true
}
Object
.entries(state.schema.marks)
.forEach(([, mark]) => {
tr.removeMark(from, to, mark as any)
})
return true
}

View File

@@ -1,15 +0,0 @@
import { Command } from '../Editor'
type ScrollIntoViewCommand = () => Command
declare module '../Editor' {
interface Commands {
scrollIntoView: ScrollIntoViewCommand,
}
}
export const scrollIntoView: ScrollIntoViewCommand = () => ({ tr }) => {
tr.scrollIntoView()
return true
}

View File

@@ -1,14 +0,0 @@
import { selectAll as originalSelectAll } from 'prosemirror-commands'
import { Command } from '../Editor'
type SelectAllCommand = () => Command
declare module '../Editor' {
interface Commands {
selectAll: SelectAllCommand,
}
}
export const selectAll: SelectAllCommand = () => ({ state, dispatch }) => {
return originalSelectAll(state, dispatch)
}

View File

@@ -1,14 +0,0 @@
import { selectParentNode as originalSelectParentNode } from 'prosemirror-commands'
import { Command } from '../Editor'
type SelectParentNodeCommand = () => Command
declare module '../Editor' {
interface Commands {
selectParentNode: SelectParentNodeCommand,
}
}
export const selectParentNode: SelectParentNodeCommand = () => ({ state, dispatch }) => {
return originalSelectParentNode(state, dispatch)
}

View File

@@ -1,21 +0,0 @@
import { NodeType } from 'prosemirror-model'
import { setBlockType as originalSetBlockType } from 'prosemirror-commands'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
type SetBlockTypeCommand = (
typeOrName: string | NodeType,
attrs?: {},
) => Command
declare module '../Editor' {
interface Commands {
setBlockType: SetBlockTypeCommand,
}
}
export const setBlockType: SetBlockTypeCommand = (typeOrName, attrs = {}) => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return originalSetBlockType(type, attrs)(state, dispatch)
}

View File

@@ -1,27 +0,0 @@
import { TextSelection } from 'prosemirror-state'
import { Command } from '../Editor'
type SetContentCommand = (
content: string,
emitUpdate?: Boolean,
parseOptions?: any,
) => Command
declare module '../Editor' {
interface Commands {
setContent: SetContentCommand,
}
}
export const setContent: SetContentCommand = (content = '', emitUpdate = false, parseOptions = {}) => ({ tr, editor }) => {
const { createDocument } = editor
const { doc } = tr
const document = createDocument(content, parseOptions)
const selection = TextSelection.create(doc, 0, doc.content.size)
tr.setSelection(selection)
.replaceSelectionWith(document, false)
.setMeta('preventUpdate', !emitUpdate)
return true
}

View File

@@ -1,18 +0,0 @@
import { sinkListItem as originalSinkListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
type SinkListItem = (typeOrName: string | NodeType) => Command
declare module '../Editor' {
interface Commands {
sinkListItem: SinkListItem,
}
}
export const sinkListItem: SinkListItem = typeOrName => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return originalSinkListItem(type)(state, dispatch)
}

View File

@@ -1,18 +0,0 @@
import { splitListItem as originalSplitListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
type SplitListItem = (typeOrName: string | NodeType) => Command
declare module '../Editor' {
interface Commands {
splitListItem: SplitListItem,
}
}
export const splitListItem: SplitListItem = typeOrName => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return originalSplitListItem(type)(state, dispatch)
}

View File

@@ -1,28 +0,0 @@
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import nodeIsActive from '../utils/nodeIsActive'
import getNodeType from '../utils/getNodeType'
type ToggleBlockTypeCommand = (
typeOrName: string | NodeType,
toggleType: string | NodeType,
attrs?: {}
) => Command
declare module '../Editor' {
interface Commands {
toggleBlockType: ToggleBlockTypeCommand,
}
}
export const toggleBlockType: ToggleBlockTypeCommand = (typeOrName, toggleTypeOrName, attrs = {}) => ({ state, commands }) => {
const type = getNodeType(typeOrName, state.schema)
const toggleType = getNodeType(toggleTypeOrName, state.schema)
const isActive = nodeIsActive(state, type, attrs)
if (isActive) {
return commands.setBlockType(toggleType)
}
return commands.setBlockType(type, attrs)
}

View File

@@ -1,50 +0,0 @@
import { wrapInList, liftListItem } from 'prosemirror-schema-list'
import { findParentNode } from 'prosemirror-utils'
import { Node, NodeType, Schema } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
type ToggleListCommand = (
listType: string | NodeType,
itemType: string | NodeType,
) => Command
declare module '../Editor' {
interface Commands {
toggleList: ToggleListCommand,
}
}
function isList(node: Node, schema: Schema) {
return (node.type === schema.nodes.bullet_list
|| node.type === schema.nodes.ordered_list
|| node.type === schema.nodes.todo_list)
}
export const toggleList: ToggleListCommand = (listTypeOrName, itemTypeOrName) => ({ tr, state, dispatch }) => {
const listType = getNodeType(listTypeOrName, state.schema)
const itemType = getNodeType(itemTypeOrName, state.schema)
const { schema, selection } = state
const { $from, $to } = selection
const range = $from.blockRange($to)
if (!range) {
return false
}
const parentList = findParentNode(node => isList(node, schema))(selection)
if (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
if (parentList.node.type === listType) {
return liftListItem(itemType)(state, dispatch)
}
if (isList(parentList.node, schema) && listType.validContent(parentList.node.content)) {
tr.setNodeMarkup(parentList.pos, listType)
return false
}
}
return wrapInList(listType)(state, dispatch)
}

View File

@@ -1,26 +0,0 @@
import { wrapIn, lift } from 'prosemirror-commands'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import nodeIsActive from '../utils/nodeIsActive'
import getNodeType from '../utils/getNodeType'
type ToggleWrapCommand = (typeOrName: string | NodeType, attrs?: {}) => Command
declare module '../Editor' {
interface Commands {
toggleWrap: ToggleWrapCommand,
}
}
export const toggleWrap: ToggleWrapCommand = (typeOrName, attrs) => ({
state, dispatch,
}) => {
const type = getNodeType(typeOrName, state.schema)
const isActive = nodeIsActive(state, type, attrs)
if (isActive) {
return lift(state, dispatch)
}
return wrapIn(type, attrs)(state, dispatch)
}

View File

@@ -1,41 +0,0 @@
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import getMarkType from '../utils/getMarkType'
import getMarkRange from '../utils/getMarkRange'
type UpdateMarkCommand = (
typeOrName: string | MarkType,
attrs: {},
) => Command
declare module '../Editor' {
interface Commands {
updateMark: UpdateMarkCommand,
}
}
export const updateMark: UpdateMarkCommand = (typeOrName, attrs = {}) => ({ tr, state }) => {
const { selection, doc } = tr
let { from, to } = selection
const { $from, empty } = selection
const type = getMarkType(typeOrName, state.schema)
if (empty) {
const range = getMarkRange($from, type)
if (range) {
from = range.from
to = range.to
}
}
const hasMark = doc.rangeHasMark(from, to, type)
if (hasMark) {
tr.removeMark(from, to, type)
}
tr.addMark(from, to, type.create(attrs))
return true
}

View File

@@ -0,0 +1,22 @@
import { Command } from '../Editor'
import { createExtension } from '../Extension'
export const Blur = createExtension({
addCommands() {
return {
blur: (): Command => ({ view }) => {
const element = view.dom as HTMLElement
element.blur()
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
Blur: typeof Blur,
}
}

View File

@@ -0,0 +1,18 @@
import { Command } from '../Editor'
import { createExtension } from '../Extension'
export const ClearContent = createExtension({
addCommands() {
return {
clearContent: (emitUpdate: Boolean = false): Command => ({ commands }) => {
return commands.setContent('', emitUpdate)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
ClearContent: typeof ClearContent,
}
}

View File

@@ -0,0 +1,19 @@
import { deleteSelection } from 'prosemirror-commands'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
export const DeleteSelection = createExtension({
addCommands() {
return {
deleteSelection: (): Command => ({ state, dispatch }) => {
return deleteSelection(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
DeleteSelection: typeof DeleteSelection,
}
}

View File

@@ -1,15 +1,9 @@
import { TextSelection } from 'prosemirror-state'
import { Editor, Command } from '../Editor'
import { createExtension } from '../Extension'
import minMax from '../utils/minMax'
type Position = 'start' | 'end' | number | boolean | null
type FocusCommand = (position?: Position) => Command
declare module '../Editor' {
interface Commands {
focus: FocusCommand
}
}
interface ResolvedSelection {
from: number,
@@ -43,19 +37,31 @@ function resolveSelection(editor: Editor, position: Position = null): ResolvedSe
}
}
export const focus: FocusCommand = (position = null) => ({ editor, view, tr }) => {
if ((view.hasFocus() && position === null) || position === false) {
return true
export const Focus = createExtension({
addCommands() {
return {
focus: (position: Position = null): Command => ({ editor, view, tr }) => {
if ((view.hasFocus() && position === null) || position === false) {
return true
}
const { from, to } = resolveSelection(editor, position)
const { doc } = tr
const resolvedFrom = minMax(from, 0, doc.content.size)
const resolvedEnd = minMax(to, 0, doc.content.size)
const selection = TextSelection.create(doc, resolvedFrom, resolvedEnd)
tr.setSelection(selection)
view.focus()
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
Focus: typeof Focus,
}
const { from, to } = resolveSelection(editor, position)
const { doc } = tr
const resolvedFrom = minMax(from, 0, doc.content.size)
const resolvedEnd = minMax(to, 0, doc.content.size)
const selection = TextSelection.create(doc, resolvedFrom, resolvedEnd)
tr.setSelection(selection)
view.focus()
return true
}

View File

@@ -0,0 +1,23 @@
export { Blur } from './blur'
export { ClearContent } from './clearContent'
export { DeleteSelection } from './deleteSelection'
export { Focus } from './focus'
export { InsertHTML } from './insertHTML'
export { InsertText } from './insertText'
export { LiftListItem } from './liftListItem'
export { RemoveMark } from './removeMark'
export { RemoveMarks } from './removeMarks'
export { ScrollIntoView } from './scrollIntoView'
export { SelectAll } from './selectAll'
export { SelectParentNode } from './selectParentNode'
export { SetNodeAttributes } from './setNodeAttributes'
export { SetBlockType } from './setBlockType'
export { SetContent } from './setContent'
export { SinkListItem } from './sinkListItem'
export { SplitBlock } from './splitBlock'
export { SplitListItem } from './splitListItem'
export { ToggleBlockType } from './toggleBlockType'
export { ToggleList } from './toggleList'
export { ToggleMark } from './toggleMark'
export { UpdateMark } from './updateMark'
export { ToggleWrap } from './toggleWrap'

View File

@@ -1,16 +1,9 @@
import { DOMParser } from 'prosemirror-model'
import { Selection, Transaction } from 'prosemirror-state'
import { ReplaceStep, ReplaceAroundStep } from 'prosemirror-transform'
import { Command } from '../Editor'
import elementFromString from '../utils/elementFromString'
type InsertHTMLCommand = (value: string) => Command
declare module '../Editor' {
interface Commands {
insertHTML: InsertHTMLCommand,
}
}
import { Command } from '../Editor'
import { createExtension } from '../Extension'
// TODO: move to utils
// https://github.com/ProseMirror/prosemirror-state/blob/master/src/selection.js#L466
@@ -25,13 +18,25 @@ function selectionToInsertionEnd(tr: Transaction, startLen: number, bias: number
tr.setSelection(Selection.near(tr.doc.resolve(end as unknown as number), bias))
}
export const insertHTML: InsertHTMLCommand = value => ({ tr, state }) => {
const { selection } = tr
const element = elementFromString(value)
const slice = DOMParser.fromSchema(state.schema).parseSlice(element)
export const InsertHTML = createExtension({
addCommands() {
return {
insertHTML: (value: string): Command => ({ tr, state }) => {
const { selection } = tr
const element = elementFromString(value)
const slice = DOMParser.fromSchema(state.schema).parseSlice(element)
tr.insert(selection.anchor, slice.content)
selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
tr.insert(selection.anchor, slice.content)
selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
return true
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
InsertHTML: typeof InsertHTML,
}
}

View File

@@ -0,0 +1,20 @@
import { Command } from '../Editor'
import { createExtension } from '../Extension'
export const InsertText = createExtension({
addCommands() {
return {
insertText: (value: string): Command => ({ tr }) => {
tr.insertText(value)
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
InsertText: typeof InsertText,
}
}

View File

@@ -0,0 +1,23 @@
import { liftListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import getNodeType from '../utils/getNodeType'
export const LiftListItem = createExtension({
addCommands() {
return {
liftListItem: (typeOrName: string | NodeType): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return liftListItem(type)(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
LiftListItem: typeof LiftListItem,
}
}

View File

@@ -0,0 +1,37 @@
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import getMarkType from '../utils/getMarkType'
import getMarkRange from '../utils/getMarkRange'
export const RemoveMark = createExtension({
addCommands() {
return {
removeMark: (typeOrName: string | MarkType): Command => ({ tr, state }) => {
const { selection } = tr
const type = getMarkType(typeOrName, state.schema)
let { from, to } = selection
const { $from, empty } = selection
if (empty) {
const range = getMarkRange($from, type)
if (range) {
from = range.from
to = range.to
}
}
tr.removeMark(from, to, type)
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
RemoveMark: typeof RemoveMark,
}
}

View File

@@ -0,0 +1,31 @@
import { Command } from '../Editor'
import { createExtension } from '../Extension'
export const RemoveMarks = createExtension({
addCommands() {
return {
removeMarks: (): Command => ({ tr, state }) => {
const { selection } = tr
const { from, to, empty } = selection
if (empty) {
return true
}
Object
.entries(state.schema.marks)
.forEach(([, mark]) => {
tr.removeMark(from, to, mark as any)
})
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
RemoveMarks: typeof RemoveMarks,
}
}

View File

@@ -0,0 +1,20 @@
import { Command } from '../Editor'
import { createExtension } from '../Extension'
export const ScrollIntoView = createExtension({
addCommands() {
return {
scrollIntoView: (): Command => ({ tr }) => {
tr.scrollIntoView()
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
ScrollIntoView: typeof ScrollIntoView,
}
}

View File

@@ -0,0 +1,19 @@
import { selectAll } from 'prosemirror-commands'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
export const SelectAll = createExtension({
addCommands() {
return {
selectAll: (): Command => ({ state, dispatch }) => {
return selectAll(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
SelectAll: typeof SelectAll,
}
}

View File

@@ -0,0 +1,19 @@
import { selectParentNode } from 'prosemirror-commands'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
export const SelectParentNode = createExtension({
addCommands() {
return {
selectParentNode: (): Command => ({ state, dispatch }) => {
return selectParentNode(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
SelectParentNode: typeof SelectParentNode,
}
}

View File

@@ -0,0 +1,23 @@
import { NodeType } from 'prosemirror-model'
import { setBlockType } from 'prosemirror-commands'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import getNodeType from '../utils/getNodeType'
export const SetBlockType = createExtension({
addCommands() {
return {
setBlockType: (typeOrName: string | NodeType, attrs = {}): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return setBlockType(type, attrs)(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
SetBlockType: typeof SetBlockType,
}
}

View File

@@ -0,0 +1,28 @@
import { TextSelection } from 'prosemirror-state'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
export const SetContent = createExtension({
addCommands() {
return {
setContent: (content: string, emitUpdate: Boolean = false, parseOptions = {}): Command => ({ tr, editor }) => {
const { createDocument } = editor
const { doc } = tr
const document = createDocument(content, parseOptions)
const selection = TextSelection.create(doc, 0, doc.content.size)
tr.setSelection(selection)
.replaceSelectionWith(document, false)
.setMeta('preventUpdate', !emitUpdate)
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
SetContent: typeof SetContent,
}
}

View File

@@ -0,0 +1,27 @@
import { Command } from '../Editor'
import { createExtension } from '../Extension'
export const SetNodeAttributes = createExtension({
addCommands() {
return {
setNodeAttributes: (attributes: {}): Command => ({ tr, state }) => {
const { selection } = tr
const { from, to } = selection
state.doc.nodesBetween(from, to, (node, pos) => {
if (!node.type.isText) {
tr.setNodeMarkup(pos, undefined, attributes)
}
})
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
SetNodeAttributes: typeof SetNodeAttributes,
}
}

View File

@@ -0,0 +1,23 @@
import { sinkListItem as originalSinkListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import getNodeType from '../utils/getNodeType'
export const SinkListItem = createExtension({
addCommands() {
return {
sinkListItem: (typeOrName: string | NodeType): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return originalSinkListItem(type)(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
SinkListItem: typeof SinkListItem,
}
}

View File

@@ -0,0 +1,71 @@
// import {
// baseKeymap, chainCommands, newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock,
// } from 'prosemirror-commands'
import { canSplit } from 'prosemirror-transform'
import { ContentMatch, Fragment } from 'prosemirror-model'
import { NodeSelection, TextSelection } from 'prosemirror-state'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
// import getNodeType from '../utils/getNodeType'
function defaultBlockAt(match: ContentMatch) {
for (let i = 0; i < match.edgeCount; i + 1) {
const { type } = match.edge(i)
// @ts-ignore
if (type.isTextblock && !type.hasRequiredAttrs()) return type
}
return null
}
export const SplitBlock = createExtension({
addCommands() {
return {
splitBlock: (copyAttributes = false): Command => ({ state, dispatch }) => {
// const type = getNodeType(typeOrName, state.schema)
const { $from, $to } = state.selection
if (state.selection instanceof NodeSelection && state.selection.node.isBlock) {
if (!$from.parentOffset || !canSplit(state.doc, $from.pos)) return false
if (dispatch) dispatch(state.tr.split($from.pos).scrollIntoView())
return true
}
if (!$from.parent.isBlock) return false
if (dispatch) {
const atEnd = $to.parentOffset === $to.parent.content.size
const { tr } = state
if (state.selection instanceof TextSelection) tr.deleteSelection()
const deflt = $from.depth === 0 ? null : defaultBlockAt($from.node(-1).contentMatchAt($from.indexAfter(-1)))
let types = atEnd && deflt ? [{ type: deflt, attrs: copyAttributes ? $from.node().attrs : {} }] : null
// let types = atEnd && deflt ? [{ type: deflt }] : null
// @ts-ignore
let can = canSplit(tr.doc, tr.mapping.map($from.pos), 1, types)
// @ts-ignore
if (!types && !can && canSplit(tr.doc, tr.mapping.map($from.pos), 1, deflt && [{ type: deflt }])) {
// @ts-ignore
types = [{ type: deflt, attrs: copyAttributes ? $from.node().attrs : {} }]
// types = [{ type: deflt }]
can = true
}
if (can) {
// @ts-ignore
tr.split(tr.mapping.map($from.pos), 1, types)
if (!atEnd && !$from.parentOffset && $from.parent.type !== deflt
// @ts-ignore
&& $from.node(-1).canReplace($from.index(-1), $from.indexAfter(-1), Fragment.from(deflt.create(), $from.parent))) { tr.setNodeMarkup(tr.mapping.map($from.before()), deflt) }
}
dispatch(tr.scrollIntoView())
}
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
SplitBlock: typeof SplitBlock,
}
}

View File

@@ -0,0 +1,23 @@
import { splitListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import getNodeType from '../utils/getNodeType'
export const SplitListItem = createExtension({
addCommands() {
return {
splitListItem: (typeOrName: string | NodeType): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return splitListItem(type)(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
SplitListItem: typeof SplitListItem,
}
}

View File

@@ -0,0 +1,29 @@
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import nodeIsActive from '../utils/nodeIsActive'
import getNodeType from '../utils/getNodeType'
export const ToggleBlockType = createExtension({
addCommands() {
return {
toggleBlockType: (typeOrName: string | NodeType, toggleTypeOrName: string | NodeType, attrs = {}): Command => ({ state, commands }) => {
const type = getNodeType(typeOrName, state.schema)
const toggleType = getNodeType(toggleTypeOrName, state.schema)
const isActive = nodeIsActive(state, type, attrs)
if (isActive) {
return commands.setBlockType(toggleType)
}
return commands.setBlockType(type, attrs)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
ToggleBlockType: typeof ToggleBlockType,
}
}

View File

@@ -0,0 +1,52 @@
import { wrapInList, liftListItem } from 'prosemirror-schema-list'
import { findParentNode } from 'prosemirror-utils'
import { Node, NodeType, Schema } from 'prosemirror-model'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import getNodeType from '../utils/getNodeType'
function isList(node: Node, schema: Schema) {
return (node.type === schema.nodes.bullet_list
|| node.type === schema.nodes.ordered_list
|| node.type === schema.nodes.todo_list)
}
export const ToggleList = createExtension({
addCommands() {
return {
toggleList: (listTypeOrName: string | NodeType, itemTypeOrName: string | NodeType): Command => ({ tr, state, dispatch }) => {
const listType = getNodeType(listTypeOrName, state.schema)
const itemType = getNodeType(itemTypeOrName, state.schema)
const { schema, selection } = state
const { $from, $to } = selection
const range = $from.blockRange($to)
if (!range) {
return false
}
const parentList = findParentNode(node => isList(node, schema))(selection)
if (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
if (parentList.node.type === listType) {
return liftListItem(itemType)(state, dispatch)
}
if (isList(parentList.node, schema) && listType.validContent(parentList.node.content)) {
tr.setNodeMarkup(parentList.pos, listType)
return false
}
}
return wrapInList(listType)(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
ToggleList: typeof ToggleList,
}
}

View File

@@ -0,0 +1,23 @@
import { toggleMark as originalToggleMark } from 'prosemirror-commands'
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import getMarkType from '../utils/getMarkType'
export const ToggleMark = createExtension({
addCommands() {
return {
toggleMark: (typeOrName: string | MarkType): Command => ({ state, dispatch }) => {
const type = getMarkType(typeOrName, state.schema)
return originalToggleMark(type)(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
ToggleMark: typeof ToggleMark,
}
}

View File

@@ -0,0 +1,29 @@
import { wrapIn, lift } from 'prosemirror-commands'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import nodeIsActive from '../utils/nodeIsActive'
import getNodeType from '../utils/getNodeType'
export const ToggleWrap = createExtension({
addCommands() {
return {
toggleWrap: (typeOrName: string | NodeType, attrs = {}): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
const isActive = nodeIsActive(state, type, attrs)
if (isActive) {
return lift(state, dispatch)
}
return wrapIn(type, attrs)(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
ToggleWrap: typeof ToggleWrap,
}
}

View File

@@ -0,0 +1,43 @@
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import getMarkType from '../utils/getMarkType'
import getMarkRange from '../utils/getMarkRange'
export const UpdateMark = createExtension({
addCommands() {
return {
updateMark: (typeOrName: string | MarkType, attrs: {}): Command => ({ tr, state }) => {
const { selection, doc } = tr
let { from, to } = selection
const { $from, empty } = selection
const type = getMarkType(typeOrName, state.schema)
if (empty) {
const range = getMarkRange($from, type)
if (range) {
from = range.from
to = range.to
}
}
const hasMark = doc.rangeHasMark(from, to, type)
if (hasMark) {
tr.removeMark(from, to, type)
}
tr.addMark(from, to, type.create(attrs))
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
UpdateMark: typeof UpdateMark,
}
}

View File

@@ -26,7 +26,7 @@ export default function (regexp: RegExp, markType: MarkType, getAttrs?: Function
if (match[m]) {
const matchStart = start + match[0].indexOf(match[m - 1])
const matchEnd = matchStart + match[m - 1].length
const matchEnd = matchStart + match[m - 1].length - 1
const textStart = matchStart + match[m - 1].lastIndexOf(match[m])
const textEnd = textStart + match[m].length

View File

@@ -1,5 +1,42 @@
import Extension from './Extension'
import Node from './Node'
import Mark from './Mark'
import { Extension } from './Extension'
import { NodeExtension } from './NodeExtension'
import { MarkExtension } from './MarkExtension'
export type Extensions = (Extension | Node | Mark)[]
export type Extensions = (Extension | NodeExtension | MarkExtension)[]
export type Attribute = {
default: any,
rendered?: boolean,
renderHTML?: ((attributes: { [key: string]: any }) => { [key: string]: any } | null) | null,
parseHTML?: ((element: HTMLElement) => { [key: string]: any } | null) | null,
}
export type Attributes = {
[key: string]: Attribute,
}
export type ExtensionAttribute = {
type: string,
name: string,
attribute: Required<Attribute>,
}
export type GlobalAttributes = {
types: string[],
attributes: Attributes,
}[]
export type PickValue<T, K extends keyof T> = T[K]
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I)=>void)
? I
: never
export type Diff<T extends keyof any, U extends keyof any> =
({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T]
export type Overwrite<T, U> = Pick<T, Diff<keyof T, keyof U>> & U;
export type AnyObject = {
[key: string]: any
}

View File

@@ -0,0 +1,72 @@
import splitExtensions from './splitExtensions'
import {
Extensions,
GlobalAttributes,
Attributes,
Attribute,
ExtensionAttribute,
} from '../types'
/**
* Get a list of all extension attributes defined in `addAttribute` and `addGlobalAttribute`.
* @param extensions List of extensions
*/
export default function getAttributesFromExtensions(extensions: Extensions) {
const extensionAttributes: ExtensionAttribute[] = []
const { nodeExtensions, markExtensions } = splitExtensions(extensions)
const nodeAndMarkExtensions = [...nodeExtensions, ...markExtensions]
const defaultAttribute: Required<Attribute> = {
default: null,
rendered: true,
renderHTML: null,
parseHTML: null,
}
extensions.forEach(extension => {
const context = {
options: extension.options,
}
const globalAttributes = extension.addGlobalAttributes.bind(context)() as GlobalAttributes
globalAttributes.forEach(globalAttribute => {
globalAttribute.types.forEach(type => {
Object
.entries(globalAttribute.attributes)
.forEach(([name, attribute]) => {
extensionAttributes.push({
type,
name,
attribute: {
...defaultAttribute,
...attribute,
},
})
})
})
})
})
nodeAndMarkExtensions.forEach(extension => {
const context = {
options: extension.options,
}
const attributes = extension.addAttributes.bind(context)() as Attributes
Object
.entries(attributes)
.forEach(([name, attribute]) => {
extensionAttributes.push({
type: extension.name,
name,
attribute: {
...defaultAttribute,
...attribute,
},
})
})
})
return extensionAttributes
}

View File

@@ -1,10 +0,0 @@
import collect from 'collect.js'
import Mark from '../Mark'
import { Extensions } from '../types'
export default function getMarksFromExtensions(extensions: Extensions): any {
return collect(extensions)
.where('type', 'mark')
.mapWithKeys((extension: Mark) => [extension.config.name, extension.config.schema])
.all()
}

View File

@@ -1,10 +0,0 @@
import collect from 'collect.js'
import Node from '../Node'
import { Extensions } from '../types'
export default function getNodesFromExtensions(extensions: Extensions): any {
return collect(extensions)
.where('type', 'node')
.mapWithKeys((extension: Node) => [extension.config.name, extension.config.schema])
.all()
}

View File

@@ -0,0 +1,20 @@
import { Node, Mark } from 'prosemirror-model'
import { ExtensionAttribute } from '../types'
import mergeAttributes from './mergeAttributes'
export default function getRenderedAttributes(nodeOrMark: Node | Mark, extensionAttributes: ExtensionAttribute[]): { [key: string]: any } {
return extensionAttributes
.filter(item => item.attribute.rendered)
.map(item => {
if (!item.attribute.renderHTML) {
return {
[item.name]: nodeOrMark.attrs[item.name],
}
}
return item.attribute.renderHTML(nodeOrMark.attrs) || {}
})
.reduce((attributes, attribute) => {
return mergeAttributes(attributes, attribute)
}, {})
}

View File

@@ -1,26 +1,93 @@
import deepmerge from 'deepmerge'
import { Schema } from 'prosemirror-model'
import { NodeSpec, MarkSpec, Schema } from 'prosemirror-model'
import { Extensions } from '../types'
import getTopNodeFromExtensions from './getTopNodeFromExtensions'
import getNodesFromExtensions from './getNodesFromExtensions'
import getMarksFromExtensions from './getMarksFromExtensions'
import resolveExtensionConfig from './resolveExtensionConfig'
import splitExtensions from './splitExtensions'
import getAttributesFromExtensions from './getAttributesFromExtensions'
import getRenderedAttributes from './getRenderedAttributes'
import isEmptyObject from './isEmptyObject'
import injectExtensionAttributesToParseRule from './injectExtensionAttributesToParseRule'
function cleanUpSchemaItem<T>(data: T) {
return Object.fromEntries(Object.entries(data).filter(([key, value]) => {
if (key === 'attrs' && isEmptyObject(value)) {
return false
}
return value !== null && value !== undefined
})) as T
}
export default function getSchema(extensions: Extensions): Schema {
extensions.forEach(extension => {
resolveExtensionConfig(extension, 'name')
resolveExtensionConfig(extension, 'defaults')
resolveExtensionConfig(extension, 'topNode')
const allAttributes = getAttributesFromExtensions(extensions)
const { nodeExtensions, markExtensions } = splitExtensions(extensions)
const topNode = nodeExtensions.find(extension => extension.topNode)?.name
const { name } = extension.config
const options = deepmerge(extension.config.defaults, extension.options)
const nodes = Object.fromEntries(nodeExtensions.map(extension => {
const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name)
const context = { options: extension.options }
const schema: NodeSpec = cleanUpSchemaItem({
content: extension.content,
marks: extension.marks,
group: extension.group,
inline: extension.inline,
atom: extension.atom,
selectable: extension.selectable,
draggable: extension.draggable,
code: extension.code,
defining: extension.defining,
isolating: extension.isolating,
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
})),
})
resolveExtensionConfig(extension, 'schema', { name, options })
})
if (extension.parseHTML) {
schema.parseDOM = extension.parseHTML
.bind(context)()
?.map(parseRule => injectExtensionAttributesToParseRule(parseRule, extensionAttributes))
}
if (extension.renderHTML) {
schema.toDOM = node => (extension.renderHTML as Function)?.bind(context)({
node,
attributes: getRenderedAttributes(node, extensionAttributes),
})
}
return [extension.name, schema]
}))
const marks = Object.fromEntries(markExtensions.map(extension => {
const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name)
const context = { options: extension.options }
const schema: MarkSpec = cleanUpSchemaItem({
inclusive: extension.inclusive,
excludes: extension.excludes,
group: extension.group,
spanning: extension.spanning,
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
})),
})
if (extension.parseHTML) {
schema.parseDOM = extension.parseHTML
.bind(context)()
?.map(parseRule => injectExtensionAttributesToParseRule(parseRule, extensionAttributes))
}
if (extension.renderHTML) {
schema.toDOM = mark => (extension.renderHTML as Function)?.bind(context)({
mark,
attributes: getRenderedAttributes(mark, extensionAttributes),
})
}
return [extension.name, schema]
}))
return new Schema({
topNode: getTopNodeFromExtensions(extensions),
nodes: getNodesFromExtensions(extensions),
marks: getMarksFromExtensions(extensions),
topNode,
nodes,
marks,
})
}

View File

@@ -2,11 +2,11 @@ import { Schema } from 'prosemirror-model'
export default function getSchemaTypeByName(name: string, schema: Schema) {
if (schema.nodes[name]) {
return 'node'
return schema.nodes[name]
}
if (schema.marks[name]) {
return 'mark'
return schema.marks[name]
}
return null

View File

@@ -0,0 +1,13 @@
import { Schema } from 'prosemirror-model'
export default function getSchemaTypeNameByName(name: string, schema: Schema) {
if (schema.nodes[name]) {
return 'node'
}
if (schema.marks[name]) {
return 'mark'
}
return null
}

View File

@@ -1,10 +0,0 @@
import collect from 'collect.js'
import { Extensions } from '../types'
export default function getTopNodeFromExtensions(extensions: Extensions): any {
const topNode = collect(extensions).firstWhere('config.topNode', true)
if (topNode) {
return topNode.config.name
}
}

View File

@@ -0,0 +1,47 @@
import { ParseRule } from 'prosemirror-model'
import { ExtensionAttribute } from '../types'
/**
* This function merges extension attributes into parserule attributes (`attrs` or `getAttrs`).
* Cancels when `getAttrs` returned `false`.
* @param parseRule ProseMirror ParseRule
* @param extensionAttributes List of attributes to inject
*/
export default function injectExtensionAttributesToParseRule(parseRule: ParseRule, extensionAttributes: ExtensionAttribute[]): ParseRule {
if (parseRule.style) {
return parseRule
}
return {
...parseRule,
getAttrs: node => {
const oldAttributes = parseRule.getAttrs
? parseRule.getAttrs(node)
: parseRule.attrs
if (oldAttributes === false) {
return false
}
const newAttributes = extensionAttributes
.filter(item => item.attribute.rendered)
.reduce((items, item) => {
const attributes = item.attribute.parseHTML
? item.attribute.parseHTML(node as HTMLElement) || {}
: {
[item.name]: (node as HTMLElement).getAttribute(item.name),
}
const filteredAttributes = Object.fromEntries(Object.entries(attributes)
.filter(([, value]) => value !== undefined && value !== null))
return {
...items,
...filteredAttributes,
}
}, {})
return { ...oldAttributes, ...newAttributes }
},
}
}

View File

@@ -0,0 +1,3 @@
export default function isEmptyObject(object = {}) {
return Object.keys(object).length === 0 && object.constructor === Object
}

View File

@@ -0,0 +1,25 @@
import { AnyObject } from '../types'
export default function mergeAttributes(...object: AnyObject[]) {
return object.reduce((items, item) => {
const mergedAttributes = { ...items }
Object.entries(item).forEach(([key, value]) => {
if (!mergedAttributes[key]) {
mergedAttributes[key] = value
return
}
if (key === 'class') {
mergedAttributes[key] = [mergedAttributes[key], value].join(' ')
return
}
if (key === 'style') {
mergedAttributes[key] = [mergedAttributes[key], value].join('; ')
}
})
return mergedAttributes
}, {})
}

View File

@@ -1,35 +0,0 @@
import deepmerge from 'deepmerge'
import Extension from '../Extension'
import Node from '../Node'
import Mark from '../Mark'
export default function resolveExtensionConfig(
extension: Extension | Node | Mark,
name: string,
props = {},
): void {
if (!extension.configs[name]) {
return
}
extension.config[name] = extension.configs[name]
.reduce((accumulator, { stategy, value: rawValue }) => {
const value = typeof rawValue === 'function'
? rawValue(props)
: rawValue
if (accumulator === undefined) {
return value
}
if (stategy === 'overwrite') {
return value
}
if (stategy === 'extend') {
return deepmerge(accumulator, value)
}
return accumulator
}, undefined)
}

View File

@@ -0,0 +1,16 @@
import { Extensions } from '../types'
import { Extension } from '../Extension'
import { NodeExtension } from '../NodeExtension'
import { MarkExtension } from '../MarkExtension'
export default function splitExtensions(extensions: Extensions) {
const baseExtensions = extensions.filter(extension => extension.type === 'extension') as Extension[]
const nodeExtensions = extensions.filter(extension => extension.type === 'node') as NodeExtension[]
const markExtensions = extensions.filter(extension => extension.type === 'mark') as MarkExtension[]
return {
baseExtensions,
nodeExtensions,
markExtensions,
}
}

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
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: [
const Blockquote = 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: (): Command => ({ commands }) => {
return commands.toggleWrap('blockquote')
},
}
},
addKeyboardShortcuts() {
return {
'Shift-Mod-9': () => this.editor.blockquote(),
}
},
addInputRules() {
return [
wrappingInputRule(inputRegex, this.type),
]
},
})
export default Blockquote
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Blockquote: typeof Blockquote,
}
}

View File

@@ -1,24 +1,17 @@
import {
Command, Mark, markInputRule, markPasteRule,
Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core'
export type BoldCommand = () => Command
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: [
const Bold = createMark({
name: 'bold',
parseHTML() {
return [
{
tag: 'strong',
},
@@ -30,23 +23,49 @@ 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 command
*/
bold: (): Command => ({ 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),
]
},
})
export default Bold
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Bold: typeof Bold,
}
}

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
const BulletList = createNode({
name: 'bullet_list',
content: 'list_item+',
group: 'block',
parseHTML() {
return [
{ tag: 'ul' },
]
},
renderHTML({ attributes }) {
return ['ul', attributes, 0]
},
addCommands() {
return {
bulletList: (): Command => ({ commands }) => {
return commands.toggleList('bullet_list', 'list_item')
},
}
},
addKeyboardShortcuts() {
return {
'Shift-Control-8': () => this.editor.bulletList(),
}
},
addInputRules() {
return [
wrappingInputRule(/^\s*([-+*])\s$/, this.type),
]
},
})
export default BulletList
declare module '@tiptap/core/src/Editor' {
interface Commands {
bulletList: BulletListCommand,
interface AllExtensions {
BulletList: typeof BulletList,
}
}
export default new Node()
.name('bullet_list')
.schema(() => ({
content: 'list_item+',
group: 'block',
parseDOM: [
{ 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()

View File

@@ -1,67 +1,99 @@
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 const backtickInputRegex = /^```(?<language>[a-z]*)? $/
export const tildeInputRegex = /^~~~(?<language>[a-z]*)? $/
declare module '@tiptap/core/src/Editor' {
interface Commands {
codeBlock: CodeBlockCommand,
}
}
const CodeBlock = createNode({
name: 'code_block',
export const inputRegex = /^```(?<language>[a-z]*)? $/
export default new Node<CodeBlockOptions>()
.name('code_block')
.defaults({
defaultOptions: <CodeBlockOptions>{
languageClassPrefix: 'language-',
})
.schema(({ options }) => ({
attrs: {
},
content: 'text*',
marks: '',
group: 'block',
code: true,
defining: true,
addAttributes() {
return {
language: {
default: null,
},
},
content: 'text*',
marks: '',
group: 'block',
code: true,
defining: true,
draggable: false,
parseDOM: [
{
tag: 'pre',
preserveWhitespace: 'full',
getAttrs(node) {
const classAttribute = (node as Element).firstElementChild?.getAttribute('class')
parseHTML: element => {
const classAttribute = 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, '') }
return {
language: classAttribute.replace(regexLanguageClassPrefix, ''),
}
},
renderHTML: attributes => {
if (!attributes.language) {
return null
}
return {
class: this.options.languageClassPrefix + attributes.language,
}
},
},
],
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 }) => ({
'Shift-Control-\\': () => editor.codeBlock(),
}))
.inputRules(({ type }) => [
textblockTypeInputRule(inputRegex, type, ({ groups }: any) => groups),
])
.create()
}
},
parseHTML() {
return [
{
tag: 'pre',
preserveWhitespace: 'full',
},
]
},
renderHTML({ attributes }) {
return ['pre', ['code', attributes, 0]]
},
addCommands() {
return {
codeBlock: (attrs?: CodeBlockOptions): Command => ({ 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),
]
},
})
export default CodeBlock
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
CodeBlock: typeof CodeBlock,
}
}

View File

@@ -1,39 +1,56 @@
import {
Command, Mark, markInputRule, markPasteRule,
Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core'
export type CodeCommand = () => Command
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: [
const Code = 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 ['code', attributes, 0]
},
addCommands() {
return {
code: (): Command => ({ commands }) => {
return commands.toggleMark('code')
},
}
},
addKeyboardShortcuts() {
return {
'Mod-`': () => this.editor.code(),
}
},
addInputRules() {
return [
markInputRule(inputRegex, this.type),
]
},
addPasteRules() {
return [
markPasteRule(inputRegex, this.type),
]
},
})
export default Code
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Code: typeof Code,
}
}

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,8 @@ export interface CollaborationCursorOptions {
render (user: { name: string, color: string }): HTMLElement,
}
export type UserCommand = (attributes: {
name: string,
color: string,
}) => Command
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)
return true
},
}))
.defaults({
const CollaborationCursor = createExtension({
defaultOptions: <CollaborationCursorOptions>{
provider: null,
name: 'Someone',
color: '#cccccc',
@@ -45,19 +26,43 @@ 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: {
name: string,
color: string,
}): Command => () => {
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,
}),
]
},
})
export default CollaborationCursor
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
CollaborationCursor: typeof CollaborationCursor,
}
}

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,32 @@ export interface CollaborationOptions {
type: any,
}
export default new Extension<CollaborationOptions>()
.name('collaboration')
.defaults({
const Collaboration = createExtension({
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()
},
})
export default Collaboration
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Collaboration: typeof Collaboration,
}
}

View File

@@ -1,9 +1,15 @@
import { Node } from '@tiptap/core'
import { createNode } from '@tiptap/core'
export default new Node()
.name('document')
.topNode()
.schema(() => ({
content: 'block+',
}))
.create()
const Document = createNode({
name: 'document',
topNode: true,
content: 'block+',
})
export default Document
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Document: typeof Document,
}
}

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,50 @@ export interface FocusOptions {
nested: boolean,
}
export default new Extension<FocusOptions>()
.name('focus')
.defaults({
const FocusClasses = createExtension({
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()
}),
]
},
})
export default FocusClasses
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
FocusClasses: typeof FocusClasses,
}
}

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
const HardBreak = createNode({
name: 'hardBreak',
inline: true,
group: 'inline',
selectable: false,
parseHTML() {
return [
{ tag: 'br' },
]
},
renderHTML({ attributes }) {
return ['br', attributes]
},
addCommands() {
return {
hardBreak: (): Command => ({
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(),
}
},
})
export default HardBreak
declare module '@tiptap/core/src/Editor' {
interface Commands {
hardBreak: HardBreakCommand,
interface AllExtensions {
HardBreak: typeof HardBreak,
}
}
export default new Node()
.name('hardBreak')
.schema(() => ({
inline: true,
group: 'inline',
selectable: false,
parseDOM: [
{ 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()

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,71 @@ export interface HeadingOptions {
levels: Level[],
}
export type HeadingCommand = (options: { level: Level }) => Command
const Heading = createNode({
name: 'heading',
declare module '@tiptap/core/src/Editor' {
interface Commands {
heading: HeadingCommand,
}
}
export default new Node<HeadingOptions>()
.name('heading')
.defaults({
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 command
*/
heading: (options: { level: Level }): Command => ({ commands }) => {
return commands.toggleBlockType('heading', 'paragraph', options)
},
}
},
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()
},
})
export default Heading
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Heading: typeof Heading,
}
}

View File

@@ -1,42 +1,47 @@
import { Command, Extension } from '@tiptap/core'
import {
history,
undo,
redo,
} from 'prosemirror-history'
declare module '@tiptap/core/src/Editor' {
interface Commands {
undo: () => Command,
redo: () => Command,
}
}
import { Command, createExtension } from '@tiptap/core'
import { history, undo, redo } from 'prosemirror-history'
export interface HistoryOptions {
depth: number,
newGroupDelay: number,
}
export default new Extension<HistoryOptions>()
.name('history')
.defaults({
const History = createExtension({
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: (): Command => ({ state, dispatch }) => {
return undo(state, dispatch)
},
redo: (): Command => ({ 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(),
}
},
})
export default History
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
History: typeof History,
}
}

View File

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

View File

@@ -0,0 +1,125 @@
import { Command, createNode, nodeInputRule } from '@tiptap/core'
import { Plugin } from 'prosemirror-state'
const IMAGE_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
const Image = createNode({
name: 'image',
inline: true,
group: 'inline',
addAttributes() {
return {
src: {
default: null,
},
alt: {
default: null,
},
title: {
default: null,
},
}
},
parseHTML() {
return [
{
tag: 'img[src]',
},
]
},
renderHTML({ attributes }) {
return ['img', attributes]
},
addCommands() {
return {
image: (attrs: any): Command => ({ tr }) => {
const { selection } = tr
console.log({ selection })
// const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
const position = selection.$anchor ? selection.$anchor.pos : selection.$to.pos
const node = this.type.create(attrs)
tr.insert(position, node)
return true
},
}
},
addInputRules() {
return [
nodeInputRule(IMAGE_INPUT_REGEX, this.type, match => {
const [, alt, src, title] = match
return {
src,
alt,
title,
}
}),
]
},
addProseMirrorPlugins() {
return [
new Plugin({
props: {
handleDOMEvents: {
drop(view, event) {
const hasFiles = event.dataTransfer
&& event.dataTransfer.files
&& event.dataTransfer.files.length
if (!hasFiles) {
return false
}
const images = Array
// @ts-ignore
.from(event.dataTransfer.files)
.filter(file => (/image/i).test(file.type))
if (images.length === 0) {
return false
}
event.preventDefault()
const { schema } = view.state
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
images.forEach(image => {
const reader = new FileReader()
reader.onload = readerEvent => {
const node = schema.nodes.image.create({
// @ts-ignore
src: readerEvent.target.result,
})
// @ts-ignore
const transaction = view.state.tr.insert(coordinates.pos, node)
view.dispatch(transaction)
}
reader.readAsDataURL(image)
})
return true
},
},
},
}),
]
},
})
export default Image
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Image: typeof Image,
}
}

View File

@@ -0,0 +1,17 @@
{
"name": "@tiptap/extension-image",
"version": "1.0.0",
"source": "index.ts",
"main": "dist/tiptap-extension-image.js",
"umd:main": "dist/tiptap-extension-image.umd.js",
"module": "dist/tiptap-extension-image.mjs",
"unpkg": "dist/tiptap-extension-image.js",
"jsdelivr": "dist/tiptap-extension-image.js",
"files": [
"src",
"dist"
],
"peerDependencies": {
"@tiptap/core": "2.x"
}
}

View File

@@ -1,44 +1,67 @@
import {
Command, Mark, markInputRule, markPasteRule,
Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core'
export type ItalicCommand = () => Command
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: [
{ tag: 'i' },
{ tag: 'em' },
{ 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()
const Italic = createMark({
name: 'italic',
parseHTML() {
return [
{
tag: 'em',
},
{
tag: 'i',
getAttrs: node => (node as HTMLElement).style.fontStyle !== 'normal' && null,
},
{
style: 'font-style=italic',
},
]
},
renderHTML({ attributes }) {
return ['em', attributes, 0]
},
addCommands() {
return {
italic: (): Command => ({ 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),
]
},
})
export default Italic
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Italic: typeof Italic,
}
}

View File

@@ -1,5 +1,5 @@
import {
Command, Mark, markPasteRule,
Command, createMark, markPasteRule, mergeAttributes,
} from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
@@ -9,62 +9,60 @@ export interface LinkOptions {
rel: string,
}
export type LinkCommand = (options: {href?: string, target?: string}) => Command
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({
const Link = createMark({
name: 'link',
inclusive: false,
defaultOptions: <LinkOptions>{
openOnClick: true,
target: '_blank',
rel: 'noopener noreferrer nofollow',
})
.schema(({ options }) => ({
attrs: {
},
addAttributes() {
return {
href: {
default: null,
},
target: {
default: null,
default: this.options.target,
},
},
inclusive: false,
parseDOM: [
{
tag: 'a[href]',
getAttrs: node => ({
href: (node as HTMLElement).getAttribute('href'),
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) {
parseHTML() {
return [
{ tag: 'a[href]' },
]
},
renderHTML({ attributes }) {
return ['a', mergeAttributes(attributes, { rel: this.options.rel }), 0]
},
addCommands() {
return {
link: (options: { href?: string, target?: string } = {}): Command => ({ commands }) => {
if (!options.href) {
return commands.removeMark('link')
}
return commands.updateMark('link', options)
},
}
},
addPasteRules() {
return [
markPasteRule(pasteRegex, this.type, (url: string) => ({ href: url })),
]
},
addProseMirrorPlugins() {
if (!this.options.openOnClick) {
return []
}
@@ -73,7 +71,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 +84,13 @@ export default new Mark<LinkOptions>()
},
}),
]
})
.create()
},
})
export default Link
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Link: typeof Link,
}
}

View File

@@ -1,17 +1,35 @@
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()
const ListItem = 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'),
}
},
})
export default ListItem
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
ListItem: typeof ListItem,
}
}

View File

@@ -1,51 +1,72 @@
import { Command, Node } from '@tiptap/core'
import { Command, createNode } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules'
export type OrderedListCommand = () => Command
const OrderedList = createNode({
name: 'ordered_list',
content: 'list_item+',
group: 'block',
addAttributes() {
return {
start: {
default: 1,
parseHTML: element => ({
start: element.hasAttribute('start')
? parseInt(element.getAttribute('start') || '', 10)
: 1,
}),
},
}
},
parseHTML() {
return [
{
tag: 'ol',
},
]
},
renderHTML({ attributes }) {
const { start, ...attributesWithoutStart } = attributes
return start === 1
? ['ol', attributesWithoutStart, 0]
: ['ol', attributes, 0]
},
addCommands() {
return {
orderedList: (): Command => ({ 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],
),
]
},
})
export default OrderedList
declare module '@tiptap/core/src/Editor' {
interface Commands {
orderedList: OrderedListCommand,
interface AllExtensions {
OrderedList: typeof OrderedList,
}
}
export default new Node()
.name('ordered_list')
.schema(() => ({
attrs: {
order: {
default: 1,
},
},
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()

View File

@@ -1,29 +1,42 @@
import { Command, Node } from '@tiptap/core'
import { Command, createNode } from '@tiptap/core'
// import ParagraphComponent from './paragraph.vue'
export type ParagraphCommand = () => Command
const Paragraph = createNode({
name: 'paragraph',
group: 'block',
content: 'inline*',
parseHTML() {
return [
{ tag: 'p' },
]
},
renderHTML({ attributes }) {
return ['p', attributes, 0]
},
addCommands() {
return {
paragraph: (): Command => ({ commands }) => {
return commands.toggleBlockType('paragraph', 'paragraph')
},
}
},
addKeyboardShortcuts() {
return {
'Mod-Alt-0': () => this.editor.paragraph(),
}
},
})
export default Paragraph
declare module '@tiptap/core/src/Editor' {
interface Commands {
paragraph: ParagraphCommand,
interface AllExtensions {
Paragraph: typeof Paragraph,
}
}
export default new Node()
.name('paragraph')
.schema(() => ({
content: 'inline*',
group: 'block',
parseDOM: [{ tag: 'p' }],
toDOM: () => ['p', 0],
// toVue: ParagraphComponent,
}))
.commands(({ name }) => ({
[name]: () => ({ commands }) => {
return commands.toggleBlockType(name, 'paragraph')
},
}))
.keys(({ editor }) => ({
'Mod-Alt-0': () => editor.paragraph(),
}))
.create()

View File

@@ -1,22 +1,15 @@
import {
Command, Mark, markInputRule, markPasteRule,
Command, createMark, markInputRule, markPasteRule,
} from '@tiptap/core'
type StrikeCommand = () => Command
export const inputRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))$/gm
export const pasteRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))/gm
declare module '@tiptap/core/src/Editor' {
interface Commands {
strike: StrikeCommand,
}
}
const Strike = createMark({
name: 'strike',
export const inputRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))$/gm
export const pasteRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))/gm
export default new Mark()
.name('strike')
.schema(() => ({
parseDOM: [
parseHTML() {
return [
{
tag: 's',
},
@@ -30,21 +23,44 @@ 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: (): Command => ({ commands }) => {
return commands.toggleMark('strike')
},
}
},
addKeyboardShortcuts() {
return {
'Mod-d': () => this.editor.strike(),
}
},
addInputRules() {
return [
markInputRule(inputRegex, this.type),
]
},
addPasteRules() {
return [
markPasteRule(inputRegex, this.type),
]
},
})
export default Strike
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Strike: typeof Strike,
}
}

View File

@@ -0,0 +1,63 @@
import { Command, createExtension } from '@tiptap/core'
type TextAlignOptions = {
types: string[],
alignments: string[],
defaultAlignment: string,
}
const TextAlign = createExtension({
defaultOptions: <TextAlignOptions>{
types: ['heading', 'paragraph'],
alignments: ['left', 'center', 'right'],
defaultAlignment: 'left',
},
addGlobalAttributes() {
return [
{
types: this.options.types,
attributes: {
textAlign: {
default: this.options.defaultAlignment,
renderHTML: attributes => ({
style: `text-align: ${attributes.textAlign}`,
}),
parseHTML: element => ({
textAlign: element.style.textAlign || this.options.defaultAlignment,
}),
},
},
},
]
},
addCommands() {
return {
textAlign: (alignment: string): Command => ({ commands }) => {
if (!this.options.alignments.includes(alignment)) {
return false
}
return commands.setNodeAttributes({ textAlign: alignment })
},
}
},
addKeyboardShortcuts() {
return {
// TODO: re-use only 'textAlign' attribute
// TODO: use custom splitBlock only for `this.options.types`
// TODO: use complete default enter handler (chainCommand) with custom splitBlock
Enter: () => this.editor.splitBlock(true),
}
},
})
export default TextAlign
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
TextAlign: typeof TextAlign,
}
}

View File

@@ -0,0 +1,17 @@
{
"name": "@tiptap/extension-text-align",
"version": "1.0.0",
"source": "index.ts",
"main": "dist/tiptap-extension-text-align.js",
"umd:main": "dist/tiptap-extension-text-align.umd.js",
"module": "dist/tiptap-extension-text-align.mjs",
"unpkg": "dist/tiptap-extension-text-align.js",
"jsdelivr": "dist/tiptap-extension-text-align.js",
"files": [
"src",
"dist"
],
"peerDependencies": {
"@tiptap/core": "2.x"
}
}

View File

@@ -1,8 +1,14 @@
import { Node } from '@tiptap/core'
import { createNode } from '@tiptap/core'
export default new Node()
.name('text')
.schema(() => ({
group: 'inline',
}))
.create()
const Text = createNode({
name: 'text',
group: 'inline',
})
export default Text
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Text: typeof Text,
}
}

View File

@@ -1,17 +1,10 @@
import { Command, Mark } from '@tiptap/core'
import { Command, createMark } from '@tiptap/core'
export type UnderlineCommand = () => Command
const Underline = createMark({
name: 'underline',
declare module '@tiptap/core/src/Editor' {
interface Commands {
underline: UnderlineCommand,
}
}
export default new Mark()
.name('underline')
.schema(() => ({
parseDOM: [
parseHTML() {
return [
{
tag: 'u',
},
@@ -19,15 +12,32 @@ 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: (): Command => ({ commands }) => {
return commands.toggleMark('underline')
},
}
},
addKeyboardShortcuts() {
return {
'Mod-u': () => this.editor.underline(),
}
},
})
export default Underline
declare module '@tiptap/core/src/Editor' {
interface AllExtensions {
Underline: typeof Underline,
}
}

View File

@@ -10,6 +10,6 @@
"@tiptap/core": "2.x",
"@types/jsdom": "^16.2.4",
"jsdom": "^16.4.0",
"prosemirror-model": "^1.11.2"
"prosemirror-model": "^1.12.0"
}
}