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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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