Merge branch 'main' into feature/new-highlight-extension
# Conflicts: # packages/core/src/commands/toggleMark.ts
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
151
packages/core/src/MarkExtension.ts
Normal file
151
packages/core/src/MarkExtension.ts
Normal 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 })
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
193
packages/core/src/NodeExtension.ts
Normal file
193
packages/core/src/NodeExtension.ts
Normal 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 })
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
22
packages/core/src/extensions/blur.ts
Normal file
22
packages/core/src/extensions/blur.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
18
packages/core/src/extensions/clearContent.ts
Normal file
18
packages/core/src/extensions/clearContent.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
19
packages/core/src/extensions/deleteSelection.ts
Normal file
19
packages/core/src/extensions/deleteSelection.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
23
packages/core/src/extensions/index.ts
Normal file
23
packages/core/src/extensions/index.ts
Normal 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'
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
20
packages/core/src/extensions/insertText.ts
Normal file
20
packages/core/src/extensions/insertText.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
23
packages/core/src/extensions/liftListItem.ts
Normal file
23
packages/core/src/extensions/liftListItem.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
37
packages/core/src/extensions/removeMark.ts
Normal file
37
packages/core/src/extensions/removeMark.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
31
packages/core/src/extensions/removeMarks.ts
Normal file
31
packages/core/src/extensions/removeMarks.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
20
packages/core/src/extensions/scrollIntoView.ts
Normal file
20
packages/core/src/extensions/scrollIntoView.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
19
packages/core/src/extensions/selectAll.ts
Normal file
19
packages/core/src/extensions/selectAll.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
19
packages/core/src/extensions/selectParentNode.ts
Normal file
19
packages/core/src/extensions/selectParentNode.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
23
packages/core/src/extensions/setBlockType.ts
Normal file
23
packages/core/src/extensions/setBlockType.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
28
packages/core/src/extensions/setContent.ts
Normal file
28
packages/core/src/extensions/setContent.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
27
packages/core/src/extensions/setNodeAttributes.ts
Normal file
27
packages/core/src/extensions/setNodeAttributes.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
23
packages/core/src/extensions/sinkListItem.ts
Normal file
23
packages/core/src/extensions/sinkListItem.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
71
packages/core/src/extensions/splitBlock.ts
Normal file
71
packages/core/src/extensions/splitBlock.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
23
packages/core/src/extensions/splitListItem.ts
Normal file
23
packages/core/src/extensions/splitListItem.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
29
packages/core/src/extensions/toggleBlockType.ts
Normal file
29
packages/core/src/extensions/toggleBlockType.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
52
packages/core/src/extensions/toggleList.ts
Normal file
52
packages/core/src/extensions/toggleList.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
23
packages/core/src/extensions/toggleMark.ts
Normal file
23
packages/core/src/extensions/toggleMark.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
29
packages/core/src/extensions/toggleWrap.ts
Normal file
29
packages/core/src/extensions/toggleWrap.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
43
packages/core/src/extensions/updateMark.ts
Normal file
43
packages/core/src/extensions/updateMark.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
72
packages/core/src/utils/getAttributesFromExtensions.ts
Normal file
72
packages/core/src/utils/getAttributesFromExtensions.ts
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
20
packages/core/src/utils/getRenderedAttributes.ts
Normal file
20
packages/core/src/utils/getRenderedAttributes.ts
Normal 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)
|
||||
}, {})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
13
packages/core/src/utils/getSchemaTypeNameByName.ts
Normal file
13
packages/core/src/utils/getSchemaTypeNameByName.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
},
|
||||
}
|
||||
}
|
||||
3
packages/core/src/utils/isEmptyObject.ts
Normal file
3
packages/core/src/utils/isEmptyObject.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function isEmptyObject(object = {}) {
|
||||
return Object.keys(object).length === 0 && object.constructor === Object
|
||||
}
|
||||
25
packages/core/src/utils/mergeAttributes.ts
Normal file
25
packages/core/src/utils/mergeAttributes.ts
Normal 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
|
||||
}, {})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
16
packages/core/src/utils/splitExtensions.ts
Normal file
16
packages/core/src/utils/splitExtensions.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
125
packages/extension-image/index.ts
Normal file
125
packages/extension-image/index.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
17
packages/extension-image/package.json
Normal file
17
packages/extension-image/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
63
packages/extension-text-align/index.ts
Normal file
63
packages/extension-text-align/index.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
17
packages/extension-text-align/package.json
Normal file
17
packages/extension-text-align/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user