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

# Conflicts:
#	docs/src/docPages/api/extensions.md
#	docs/src/links.yaml
#	packages/core/src/extensions/toggleMark.ts
This commit is contained in:
Philipp Kühn
2020-11-05 21:27:20 +01:00
245 changed files with 4197 additions and 2609 deletions

View File

@@ -1,5 +1,7 @@
import { EditorState, Transaction } from 'prosemirror-state'
import { ChainedCommands, Editor, CommandSpec } from './Editor'
import {
SingleCommands, ChainedCommands, Editor, CommandSpec,
} from './Editor'
import getAllMethodNames from './utils/getAllMethodNames'
export default class CommandManager {
@@ -57,7 +59,7 @@ export default class CommandManager {
}
}
public createChain(startTr?: Transaction) {
public createChain(startTr?: Transaction, shouldDispatch = true) {
const { commands, editor } = this
const { state, view } = editor
const callbacks: boolean[] = []
@@ -71,7 +73,7 @@ export default class CommandManager {
return new Proxy({}, {
get: (_, name: string, proxy) => {
if (name === 'run') {
if (!hasStartTransaction) {
if (!hasStartTransaction && shouldDispatch) {
view.dispatch(tr)
}
@@ -85,7 +87,7 @@ export default class CommandManager {
}
return (...args: any) => {
const props = this.buildProps(tr)
const props = this.buildProps(tr, shouldDispatch)
const callback = command(...args)(props)
callbacks.push(callback)
@@ -95,7 +97,31 @@ export default class CommandManager {
}) as ChainedCommands
}
public buildProps(tr: Transaction) {
public createCan(startTr?: Transaction) {
const { commands, editor } = this
const { state } = editor
const dispatch = false
const hasStartTransaction = !!startTr
const tr = hasStartTransaction ? startTr : state.tr
if (!tr) {
return
}
const props = this.buildProps(tr, dispatch)
const formattedCommands = Object.fromEntries(Object
.entries(commands)
.map(([name, command]) => {
return [name, (...args: any[]) => command(...args)({ ...props, dispatch })]
})) as SingleCommands
return {
...formattedCommands,
chain: () => this.createChain(tr, dispatch),
}
}
public buildProps(tr: Transaction, shouldDispatch = true) {
const { editor, commands } = this
const { state, view } = editor
@@ -104,8 +130,11 @@ export default class CommandManager {
editor,
view,
state: this.chainableState(tr, state),
dispatch: () => false,
dispatch: shouldDispatch
? () => undefined
: undefined,
chain: () => this.createChain(tr),
can: () => this.createCan(tr),
get commands() {
return Object.fromEntries(Object
.entries(commands)

View File

@@ -1,5 +0,0 @@
export default abstract class ComponentRenderer {
static type: string
}

View File

@@ -9,13 +9,12 @@ import getNodeAttrs from './utils/getNodeAttrs'
import getMarkAttrs from './utils/getMarkAttrs'
import removeElement from './utils/removeElement'
import getSchemaTypeNameByName from './utils/getSchemaTypeNameByName'
import getHtmlFromFragment from './utils/getHtmlFromFragment'
import getHTMLFromFragment from './utils/getHTMLFromFragment'
import createStyleTag from './utils/createStyleTag'
import CommandManager from './CommandManager'
import ExtensionManager from './ExtensionManager'
import EventEmitter from './EventEmitter'
import { Extensions, UnionToIntersection, PickValue } from './types'
import defaultPlugins from './plugins'
import * as extensions from './extensions'
import style from './style'
@@ -23,10 +22,11 @@ export type Command = (props: {
editor: Editor,
tr: Transaction,
commands: SingleCommands,
can: () => SingleCommands & { chain: () => ChainedCommands },
chain: () => ChainedCommands,
state: EditorState,
view: EditorView,
dispatch: (args?: any) => any,
dispatch: ((args?: any) => any) | undefined,
}) => boolean
export type CommandSpec = (...args: any[]) => Command
@@ -75,8 +75,6 @@ declare module './Editor' {
@magicMethods
export class Editor extends EventEmitter {
public renderer!: any
private proxy!: Editor
private commandManager!: CommandManager
@@ -116,7 +114,6 @@ export class Editor extends EventEmitter {
this.createExtensionManager()
this.createSchema()
this.createView()
// this.registerCommands(coreCommands)
this.injectCSS()
window.setTimeout(() => this.proxy.focus(this.options.autoFocus), 0)
@@ -139,6 +136,13 @@ export class Editor extends EventEmitter {
return this.commandManager.createChain()
}
/**
* Check if a command or a command chain can be executed. Without executing it.
*/
public can() {
return this.commandManager.createCan()
}
/**
* Inject CSS styles.
*/
@@ -233,7 +237,7 @@ export class Editor extends EventEmitter {
*/
private createExtensionManager() {
const coreExtensions = Object.entries(extensions).map(([, extension]) => extension())
const allExtensions = [...coreExtensions, ...this.options.extensions]
const allExtensions = [...this.options.extensions, ...coreExtensions]
this.extensionManager = new ExtensionManager(allExtensions, this.proxy)
}
@@ -257,18 +261,26 @@ export class Editor extends EventEmitter {
*/
private createView() {
this.view = new EditorView(this.options.element, {
dispatchTransaction: this.dispatchTransaction.bind(this),
state: EditorState.create({
doc: this.createDocument(this.options.content),
plugins: [
...this.extensionManager.plugins,
...defaultPlugins.map(plugin => plugin(this.proxy)),
],
}),
dispatchTransaction: this.dispatchTransaction.bind(this),
})
// `editor.view` is not yet available at this time.
// Therefore we will add all plugins and node views directly afterwards.
const newState = this.state.reconfigure({
plugins: this.extensionManager.plugins,
})
this.view.updateState(newState)
this.view.setProps({
nodeViews: this.extensionManager.nodeViews,
})
// store editor in dom element for better testing
// Lets store the editor instance in the DOM element.
// So well have access to it for tests.
const dom = this.view.dom as HTMLElement
dom.editor = this.proxy
}
@@ -384,7 +396,7 @@ export class Editor extends EventEmitter {
* Get the document as HTML.
*/
public getHTML() {
return getHtmlFromFragment(this.state.doc, this.schema)
return getHTMLFromFragment(this.state.doc, this.schema)
}
/**

View File

@@ -1,14 +1,15 @@
import { Plugin } from 'prosemirror-state'
import { keymap } from 'prosemirror-keymap'
// import { Schema, Node as ProsemirrorNode } 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 { EditorView, Decoration } from 'prosemirror-view'
import { Editor } from './Editor'
// import capitalize from './utils/capitalize'
import { Extensions } from './types'
import { Extensions, NodeViewRenderer } from './types'
import getSchema from './utils/getSchema'
import getSchemaTypeByName from './utils/getSchemaTypeByName'
import splitExtensions from './utils/splitExtensions'
import getAttributesFromExtensions from './utils/getAttributesFromExtensions'
import getRenderedAttributes from './utils/getRenderedAttributes'
export default class ExtensionManager {
@@ -98,36 +99,42 @@ export default class ExtensionManager {
}
get nodeViews() {
// const { renderer: Renderer } = this.editor
const { editor } = this
const { nodeExtensions } = splitExtensions(this.extensions)
const allAttributes = getAttributesFromExtensions(this.extensions)
// if (!Renderer || !Renderer.type) {
// return {}
// }
return Object.fromEntries(nodeExtensions
.filter(extension => !!extension.addNodeView)
.map(extension => {
const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name)
const context = {
options: extension.options,
editor,
type: getSchemaTypeByName(extension.name, this.schema),
}
// const prop = `to${capitalize(Renderer.type)}`
// @ts-ignore
const renderer = extension.addNodeView?.bind(context)?.() as NodeViewRenderer
// return collect(this.extensions)
// .where('extensionType', 'node')
// .filter((extension: any) => extension.schema()[prop])
// .map((extension: any) => {
// return (
// node: ProsemirrorNode,
// view: EditorView,
// getPos: (() => number) | boolean,
// decorations: Decoration[],
// ) => {
// return new Renderer(extension.schema()[prop], {
// extension,
// editor: this.editor,
// node,
// getPos,
// decorations,
// })
// }
// })
// .all()
const nodeview = (
node: ProsemirrorNode,
view: EditorView,
getPos: (() => number) | boolean,
decorations: Decoration[],
) => {
const attributes = getRenderedAttributes(node, extensionAttributes)
return {}
return renderer({
editor,
node,
getPos,
decorations,
attributes,
})
}
return [extension.name, nodeview]
}))
}
}

View File

@@ -10,22 +10,22 @@ export interface MarkExtensionSpec<Options = {}, Commands = {}> extends Overwrit
/**
* Inclusive
*/
inclusive?: MarkSpec['inclusive'],
inclusive?: MarkSpec['inclusive'] | ((this: { options: Options }) => MarkSpec['inclusive']),
/**
* Excludes
*/
excludes?: MarkSpec['excludes'],
excludes?: MarkSpec['excludes'] | ((this: { options: Options }) => MarkSpec['excludes']),
/**
* Group
*/
group?: MarkSpec['group'],
group?: MarkSpec['group'] | ((this: { options: Options }) => MarkSpec['group']),
/**
* Spanning
*/
spanning?: MarkSpec['spanning'],
spanning?: MarkSpec['spanning'] | ((this: { options: Options }) => MarkSpec['spanning']),
/**
* Parse HTML

View File

@@ -3,7 +3,7 @@ import {
} from 'prosemirror-model'
import { Plugin } from 'prosemirror-state'
import { ExtensionSpec, defaultExtension } from './Extension'
import { Attributes, Overwrite } from './types'
import { Attributes, NodeViewRenderer, Overwrite } from './types'
import { Editor } from './Editor'
export interface NodeExtensionSpec<Options = {}, Commands = {}> extends Overwrite<ExtensionSpec<Options, Commands>, {
@@ -15,52 +15,52 @@ export interface NodeExtensionSpec<Options = {}, Commands = {}> extends Overwrit
/**
* Content
*/
content?: NodeSpec['content'],
content?: NodeSpec['content'] | ((this: { options: Options }) => NodeSpec['content']),
/**
* Marks
*/
marks?: NodeSpec['marks'],
marks?: NodeSpec['marks'] | ((this: { options: Options }) => NodeSpec['marks']),
/**
* Group
*/
group?: NodeSpec['group'],
group?: NodeSpec['group'] | ((this: { options: Options }) => NodeSpec['group']),
/**
* Inline
*/
inline?: NodeSpec['inline'],
inline?: NodeSpec['inline'] | ((this: { options: Options }) => NodeSpec['inline']),
/**
* Atom
*/
atom?: NodeSpec['atom'],
atom?: NodeSpec['atom'] | ((this: { options: Options }) => NodeSpec['atom']),
/**
* Selectable
*/
selectable?: NodeSpec['selectable'],
selectable?: NodeSpec['selectable'] | ((this: { options: Options }) => NodeSpec['selectable']),
/**
* Draggable
*/
draggable?: NodeSpec['draggable'],
draggable?: NodeSpec['draggable'] | ((this: { options: Options }) => NodeSpec['draggable']),
/**
* Code
*/
code?: NodeSpec['code'],
code?: NodeSpec['code'] | ((this: { options: Options }) => NodeSpec['code']),
/**
* Defining
*/
defining?: NodeSpec['defining'],
defining?: NodeSpec['defining'] | ((this: { options: Options }) => NodeSpec['defining']),
/**
* Isolating
*/
isolating?: NodeSpec['isolating'],
isolating?: NodeSpec['isolating'] | ((this: { options: Options }) => NodeSpec['isolating']),
/**
* Parse HTML
@@ -139,6 +139,15 @@ export interface NodeExtensionSpec<Options = {}, Commands = {}> extends Overwrit
editor: Editor,
type: NodeType,
}) => Plugin[],
/**
* Node View
*/
addNodeView?: ((this: {
options: Options,
editor: Editor,
type: NodeType,
}) => NodeViewRenderer) | null,
}> {}
export type NodeExtension = Required<Omit<NodeExtensionSpec, 'defaultOptions'> & {
@@ -166,6 +175,7 @@ const defaultNode: NodeExtension = {
parseHTML: () => null,
renderHTML: null,
addAttributes: () => ({}),
addNodeView: null,
}
export function createNode<Options extends {}, Commands extends {}>(config: NodeExtensionSpec<Options, Commands>) {

View File

@@ -0,0 +1,9 @@
import { Command } from '../Editor'
export default (): Command => ({ view }) => {
const element = view.dom as HTMLElement
element.blur()
return true
}

View File

@@ -0,0 +1,5 @@
import { Command } from '../Editor'
export default (emitUpdate: Boolean = false): Command => ({ commands }) => {
return commands.setContent('', emitUpdate)
}

View File

@@ -0,0 +1,29 @@
import { liftTarget } from 'prosemirror-transform'
import { Command } from '../Editor'
export default (): Command => ({ state, tr, dispatch }) => {
const { selection } = tr
const { from, to } = selection
state.doc.nodesBetween(from, to, (node, pos) => {
if (!node.type.isText) {
const fromPos = tr.doc.resolve(tr.mapping.map(pos + 1))
const toPos = tr.doc.resolve(tr.mapping.map(pos + node.nodeSize - 1))
const nodeRange = fromPos.blockRange(toPos)
if (nodeRange) {
const targetLiftDepth = liftTarget(nodeRange)
if (node.type.isTextblock && dispatch) {
tr.setNodeMarkup(nodeRange.start, state.schema.nodes.paragraph)
}
if ((targetLiftDepth || targetLiftDepth === 0) && dispatch) {
tr.lift(nodeRange, targetLiftDepth)
}
}
}
})
return true
}

View File

@@ -0,0 +1,6 @@
import { deleteSelection } from 'prosemirror-commands'
import { Command } from '../Editor'
export default (): Command => ({ state, dispatch }) => {
return deleteSelection(state, dispatch)
}

View File

@@ -0,0 +1,58 @@
import { TextSelection } from 'prosemirror-state'
import { Editor, Command } from '../Editor'
import minMax from '../utils/minMax'
type Position = 'start' | 'end' | number | boolean | null
interface ResolvedSelection {
from: number,
to: number,
}
function resolveSelection(editor: Editor, position: Position = null): ResolvedSelection {
if (position === null) {
return editor.selection
}
if (position === 'start' || position === true) {
return {
from: 0,
to: 0,
}
}
if (position === 'end') {
const { size } = editor.state.doc.content
return {
from: size,
to: size - 1, // TODO: -1 only for nodes with content
}
}
return {
from: position as number,
to: position as number,
}
}
export default (position: Position = null): Command => ({
editor, view, tr, dispatch,
}) => {
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)
if (dispatch) {
tr.setSelection(selection)
view.focus()
}
return true
}

View File

@@ -3,7 +3,6 @@ import { Selection, Transaction } from 'prosemirror-state'
import { ReplaceStep, ReplaceAroundStep } from 'prosemirror-transform'
import elementFromString from '../utils/elementFromString'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
// TODO: move to utils
// https://github.com/ProseMirror/prosemirror-state/blob/master/src/selection.js#L466
@@ -18,25 +17,15 @@ function selectionToInsertionEnd(tr: Transaction, startLen: number, bias: number
tr.setSelection(Selection.near(tr.doc.resolve(end as unknown as number), bias))
}
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)
export default (value: string): Command => ({ tr, state, dispatch }) => {
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)
return true
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
InsertHTML: typeof InsertHTML,
if (dispatch) {
tr.insert(selection.anchor, slice.content)
selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
}
return true
}

View File

@@ -0,0 +1,9 @@
import { Command } from '../Editor'
export default (value: string): Command => ({ tr, dispatch }) => {
if (dispatch) {
tr.insertText(value)
}
return true
}

View File

@@ -0,0 +1,10 @@
import { liftListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return liftListItem(type)(state, dispatch)
}

View File

@@ -0,0 +1,26 @@
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import getMarkType from '../utils/getMarkType'
import getMarkRange from '../utils/getMarkRange'
export default (typeOrName: string | MarkType): Command => ({ tr, state, dispatch }) => {
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
}
}
if (dispatch) {
tr.removeMark(from, to, type)
}
return true
}

View File

@@ -0,0 +1,20 @@
import { Command } from '../Editor'
export default (): Command => ({ tr, state, dispatch }) => {
const { selection } = tr
const { from, to, empty } = selection
if (empty) {
return true
}
if (dispatch) {
Object
.entries(state.schema.marks)
.forEach(([, mark]) => {
tr.removeMark(from, to, mark as any)
})
}
return true
}

View File

@@ -0,0 +1,24 @@
import { Command } from '../Editor'
export default (attributeNames: string[] = []): Command => ({ tr, state, dispatch }) => {
const { selection } = tr
const { from, to } = selection
state.doc.nodesBetween(from, to, (node, pos) => {
if (!node.type.isText) {
attributeNames.forEach(name => {
const attribute = node.type.spec.attrs?.[name]
const defaultValue = attribute?.default
if (attribute && defaultValue !== undefined && dispatch) {
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
[name]: defaultValue,
})
}
})
}
})
return true
}

View File

@@ -0,0 +1,9 @@
import { Command } from '../Editor'
export default (): Command => ({ tr, dispatch }) => {
if (dispatch) {
tr.scrollIntoView()
}
return true
}

View File

@@ -0,0 +1,6 @@
import { selectAll } from 'prosemirror-commands'
import { Command } from '../Editor'
export default (): Command => ({ state, dispatch }) => {
return selectAll(state, dispatch)
}

View File

@@ -0,0 +1,6 @@
import { selectParentNode } from 'prosemirror-commands'
import { Command } from '../Editor'
export default (): Command => ({ state, dispatch }) => {
return selectParentNode(state, dispatch)
}

View File

@@ -0,0 +1,10 @@
import { NodeType } from 'prosemirror-model'
import { setBlockType } from 'prosemirror-commands'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType, attrs = {}): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return setBlockType(type, attrs)(state, dispatch)
}

View File

@@ -0,0 +1,17 @@
import { TextSelection } from 'prosemirror-state'
import { Command } from '../Editor'
export default (content: string, emitUpdate: Boolean = false, parseOptions = {}): Command => ({ tr, editor, dispatch }) => {
const { createDocument } = editor
const { doc } = tr
const document = createDocument(content, parseOptions)
const selection = TextSelection.create(doc, 0, doc.content.size)
if (dispatch) {
tr.setSelection(selection)
.replaceSelectionWith(document, false)
.setMeta('preventUpdate', !emitUpdate)
}
return true
}

View File

@@ -0,0 +1,17 @@
import { Command } from '../Editor'
export default (attributes: {}): Command => ({ tr, state, dispatch }) => {
const { selection } = tr
const { from, to } = selection
state.doc.nodesBetween(from, to, (node, pos) => {
if (!node.type.isText && dispatch) {
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
...attributes,
})
}
})
return true
}

View File

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

View File

@@ -0,0 +1,117 @@
import { canSplit } from 'prosemirror-transform'
import { ContentMatch, Fragment } from 'prosemirror-model'
import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'
import { Command } from '../Editor'
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 interface SplitBlockOptions {
withAttributes: boolean,
withMarks: boolean,
}
function keepMarks(state: EditorState) {
const marks = state.storedMarks
|| (state.selection.$to.parentOffset && state.selection.$from.marks())
if (marks) {
state.tr.ensureMarks(marks)
}
}
export default (options: Partial<SplitBlockOptions> = {}): Command => ({ tr, state, dispatch }) => {
const defaultOptions: SplitBlockOptions = {
withAttributes: false,
withMarks: true,
}
const config = { ...defaultOptions, ...options }
const { selection, doc } = tr
const { $from, $to } = selection
if (selection instanceof NodeSelection && selection.node.isBlock) {
if (!$from.parentOffset || !canSplit(doc, $from.pos)) {
return false
}
if (dispatch) {
if (config.withMarks) {
keepMarks(state)
}
tr.split($from.pos).scrollIntoView()
}
return true
}
if (!$from.parent.isBlock) {
return false
}
if (dispatch) {
const atEnd = $to.parentOffset === $to.parent.content.size
if (selection instanceof TextSelection) {
tr.deleteSelection()
}
const deflt = $from.depth === 0
? undefined
: defaultBlockAt($from.node(-1).contentMatchAt($from.indexAfter(-1)))
let types = atEnd && deflt
? [{
type: deflt,
attrs: config.withAttributes
? $from.node().attrs
: {},
}]
: undefined
let can = canSplit(tr.doc, tr.mapping.map($from.pos), 1, types)
if (
!types
&& !can
&& canSplit(tr.doc, tr.mapping.map($from.pos), 1, deflt ? [{ type: deflt }] : undefined)
) {
can = true
types = deflt
? [{
type: deflt,
attrs: config.withAttributes
? $from.node().attrs
: {},
}]
: undefined
}
if (can) {
tr.split(tr.mapping.map($from.pos), 1, types)
if (
!atEnd
&& !$from.parentOffset
&& $from.parent.type !== deflt
&& $from.node(-1).canReplace($from.index(-1), $from.indexAfter(-1), Fragment.from(deflt?.create()))
) {
tr.setNodeMarkup(tr.mapping.map($from.before()), deflt || undefined)
}
}
if (config.withMarks) {
keepMarks(state)
}
tr.scrollIntoView()
}
return true
}

View File

@@ -0,0 +1,10 @@
import { splitListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return splitListItem(type)(state, dispatch)
}

View File

@@ -0,0 +1,16 @@
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import nodeIsActive from '../utils/nodeIsActive'
import getNodeType from '../utils/getNodeType'
export default (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)
}

View File

@@ -0,0 +1,52 @@
import { findParentNode } from 'prosemirror-utils'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
import isList from '../utils/isList'
export default (listTypeOrName: string | NodeType, itemTypeOrName: string | NodeType): Command => ({
editor, tr, state, dispatch, chain, commands, can,
}) => {
const { extensions } = editor.options
const listType = getNodeType(listTypeOrName, state.schema)
const itemType = getNodeType(itemTypeOrName, state.schema)
const { selection } = state
const { $from, $to } = selection
const range = $from.blockRange($to)
if (!range) {
return false
}
const parentList = findParentNode(node => isList(node.type.name, extensions))(selection)
if (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
// remove list
if (parentList.node.type === listType) {
return commands.liftListItem(itemType)
}
// change list type
if (
isList(parentList.node.type.name, extensions)
&& listType.validContent(parentList.node.content)
&& dispatch
) {
tr.setNodeMarkup(parentList.pos, listType)
return true
}
}
const canWrapInList = can().wrapInList(listType)
// try to convert node to paragraph if needed
if (!canWrapInList) {
return chain()
.clearNodes()
.wrapInList(listType)
.run()
}
return commands.wrapInList(listType)
}

View File

@@ -0,0 +1,19 @@
import { toggleMark } from 'prosemirror-commands'
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import getMarkType from '../utils/getMarkType'
import markIsActive from '../utils/markIsActive'
export default (typeOrName: string | MarkType, attrs?: {}): Command => ({ state, dispatch, commands }) => {
const type = getMarkType(typeOrName, state.schema)
const hasMarkWithDifferentAttributes = attrs
&& markIsActive(state, type)
&& !markIsActive(state, type, attrs)
if (attrs && hasMarkWithDifferentAttributes) {
return commands.updateMark(type, attrs)
}
return toggleMark(type)(state, dispatch)
}

View File

@@ -0,0 +1,16 @@
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'
export default (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)
}

View File

@@ -0,0 +1,15 @@
import { Command } from '../Editor'
export default (commands: Command[] | ((props: Parameters<Command>[0]) => Command[])): Command => props => {
const items = typeof commands === 'function'
? commands(props)
: commands
for (let i = 0; i < items.length; i += 1) {
if (items[i](props)) {
return true
}
}
return false
}

View File

@@ -0,0 +1,32 @@
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import getMarkType from '../utils/getMarkType'
import getMarkRange from '../utils/getMarkRange'
export default (typeOrName: string | MarkType, attrs: {}): Command => ({ tr, state, dispatch }) => {
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 && dispatch) {
tr.removeMark(from, to, type)
}
if (dispatch) {
tr.addMark(from, to, type.create(attrs))
}
return true
}

View File

@@ -0,0 +1,10 @@
import { wrapInList } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType, attrs?: {}): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return wrapInList(type, attrs)(state, dispatch)
}

View File

@@ -1,22 +0,0 @@
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

@@ -1,18 +0,0 @@
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,68 @@
import { createExtension } from '../Extension'
import blur from '../commands/blur'
import clearContent from '../commands/clearContent'
import clearNodes from '../commands/clearNodes'
import deleteSelection from '../commands/deleteSelection'
import focus from '../commands/focus'
import insertHTML from '../commands/insertHTML'
import insertText from '../commands/insertText'
import liftListItem from '../commands/liftListItem'
import removeMark from '../commands/removeMark'
import removeMarks from '../commands/removeMarks'
import resetNodeAttributes from '../commands/resetNodeAttributes'
import scrollIntoView from '../commands/scrollIntoView'
import selectAll from '../commands/selectAll'
import selectParentNode from '../commands/selectParentNode'
import setBlockType from '../commands/setBlockType'
import setContent from '../commands/setContent'
import setNodeAttributes from '../commands/setNodeAttributes'
import sinkListItem from '../commands/sinkListItem'
import splitBlock from '../commands/splitBlock'
import splitListItem from '../commands/splitListItem'
import toggleBlockType from '../commands/toggleBlockType'
import toggleList from '../commands/toggleList'
import toggleMark from '../commands/toggleMark'
import toggleWrap from '../commands/toggleWrap'
import tryCommand from '../commands/try'
import updateMark from '../commands/updateMark'
import wrapInList from '../commands/wrapInList'
export const Commands = createExtension({
addCommands() {
return {
blur,
clearContent,
clearNodes,
deleteSelection,
focus,
insertHTML,
insertText,
liftListItem,
removeMark,
removeMarks,
resetNodeAttributes,
scrollIntoView,
selectAll,
selectParentNode,
setBlockType,
setContent,
setNodeAttributes,
sinkListItem,
splitBlock,
splitListItem,
toggleBlockType,
toggleList,
toggleMark,
toggleWrap,
try: tryCommand,
updateMark,
wrapInList,
}
},
})
declare module '../Editor' {
interface AllExtensions {
Commands: typeof Commands,
}
}

View File

@@ -1,19 +0,0 @@
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

@@ -0,0 +1,22 @@
import { Plugin, PluginKey } from 'prosemirror-state'
import { createExtension } from '../Extension'
export const Editable = createExtension({
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('editable'),
props: {
editable: () => this.editor.options.editable,
},
}),
]
},
})
// TODO: Editable circularly references itself!?
// declare module '../Editor' {
// interface AllExtensions {
// Editable: typeof Editable,
// }
// }

View File

@@ -1,67 +0,0 @@
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
interface ResolvedSelection {
from: number,
to: number,
}
function resolveSelection(editor: Editor, position: Position = null): ResolvedSelection {
if (position === null) {
return editor.selection
}
if (position === 'start' || position === true) {
return {
from: 0,
to: 0,
}
}
if (position === 'end') {
const { size } = editor.state.doc.content
return {
from: size,
to: size - 1, // TODO: -1 only for nodes with content
}
}
return {
from: position as number,
to: position as number,
}
}
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,
}
}

View File

@@ -0,0 +1,42 @@
import { Plugin } from 'prosemirror-state'
import { createExtension } from '../Extension'
export const FocusEvents = createExtension({
addProseMirrorPlugins() {
const { editor } = this
return [
new Plugin({
props: {
attributes: {
tabindex: '0',
},
handleDOMEvents: {
focus: () => {
editor.isFocused = true
const transaction = editor.state.tr.setMeta('focused', true)
editor.view.dispatch(transaction)
return true
},
blur: () => {
editor.isFocused = false
const transaction = editor.state.tr.setMeta('focused', false)
editor.view.dispatch(transaction)
return true
},
},
},
}),
]
},
})
declare module '../Editor' {
interface AllExtensions {
FocusEvents: typeof FocusEvents,
}
}

View File

@@ -1,23 +1,4 @@
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'
export { Commands } from './commands'
export { Editable } from './editable'
export { FocusEvents } from './focusEvents'
export { Keymap } from './keymap'

View File

@@ -1,20 +0,0 @@
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,52 @@
import {
newlineInCode,
createParagraphNear,
liftEmptyBlock,
exitCode,
deleteSelection,
joinForward,
joinBackward,
selectNodeForward,
selectNodeBackward,
} from 'prosemirror-commands'
import { undoInputRule } from 'prosemirror-inputrules'
import { createExtension } from '../Extension'
export const Keymap = createExtension({
addKeyboardShortcuts() {
const handleBackspace = () => this.editor.try(({ state, dispatch }) => [
() => undoInputRule(state, dispatch),
() => deleteSelection(state, dispatch),
() => joinBackward(state, dispatch),
() => selectNodeBackward(state, dispatch),
])
const handleDelete = () => this.editor.try(({ state, dispatch }) => [
() => deleteSelection(state, dispatch),
() => joinForward(state, dispatch),
() => selectNodeForward(state, dispatch),
])
return {
Enter: () => this.editor.try(({ commands, state, dispatch }) => [
() => newlineInCode(state, dispatch),
() => createParagraphNear(state, dispatch),
() => liftEmptyBlock(state, dispatch),
() => commands.splitBlock(),
]),
'Mod-Enter': exitCode,
Backspace: () => handleBackspace(),
'Mod-Backspace': () => handleBackspace(),
Delete: () => handleDelete(),
'Mod-Delete': () => handleDelete(),
// we dont need a custom `selectAll` for now
// 'Mod-a': () => this.editor.selectAll(),
}
},
})
declare module '../Editor' {
interface AllExtensions {
Keymap: typeof Keymap,
}
}

View File

@@ -1,23 +0,0 @@
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

@@ -1,37 +0,0 @@
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

@@ -1,31 +0,0 @@
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

@@ -1,20 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,27 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -1,71 +0,0 @@
// 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

@@ -1,23 +0,0 @@
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

@@ -1,29 +0,0 @@
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

@@ -1,52 +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 { 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

@@ -1,32 +0,0 @@
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'
import markIsActive from '../utils/markIsActive'
export const ToggleMark = createExtension({
addCommands() {
return {
toggleMark: (typeOrName: string | MarkType, attrs?: {}): Command => ({ state, dispatch, commands }) => {
const type = getMarkType(typeOrName, state.schema)
const hasMarkWithDifferentAttributes = attrs
&& markIsActive(state, type)
&& !markIsActive(state, type, attrs)
if (attrs && hasMarkWithDifferentAttributes) {
return commands.updateMark(type, attrs)
}
return originalToggleMark(type)(state, dispatch)
},
}
},
})
declare module '../Editor' {
interface AllExtensions {
ToggleMark: typeof ToggleMark,
}
}

View File

@@ -1,29 +0,0 @@
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

@@ -1,43 +0,0 @@
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

@@ -16,45 +16,47 @@ function getMarksBetween(start: number, end: number, state: EditorState) {
return marks
}
export default function (regexp: RegExp, markType: MarkType, getAttrs?: Function) {
export default function (regexp: RegExp, markType: MarkType, getAttributes?: Function) {
return new InputRule(regexp, (state, match, start, end) => {
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
const attributes = getAttributes instanceof Function
? getAttributes(match)
: getAttributes
const { tr } = state
const m = match.length - 1
const captureGroup = match[match.length - 1]
const fullMatch = match[0]
let markEnd = end
let markStart = start
if (match[m]) {
const matchStart = start + match[0].indexOf(match[m - 1])
const matchEnd = matchStart + match[m - 1].length - 1
const textStart = matchStart + match[m - 1].lastIndexOf(match[m])
const textEnd = textStart + match[m].length
if (captureGroup) {
const startSpaces = fullMatch.search(/\S/)
const textStart = start + fullMatch.indexOf(captureGroup)
const textEnd = textStart + captureGroup.length
const excludedMarks = getMarksBetween(start, end, state)
.filter(item => {
const { excluded } = item.mark.type
return excluded.find((type: MarkType) => type.name === markType.name)
})
.filter(item => item.end > matchStart)
.filter(item => item.end > textStart)
if (excludedMarks.length) {
return null
}
if (textEnd < matchEnd) {
tr.delete(textEnd, matchEnd)
if (textEnd < end) {
tr.delete(textEnd, end)
}
if (textStart > matchStart) {
tr.delete(matchStart, textStart)
if (textStart > start) {
tr.delete(start + startSpaces, textStart)
}
markStart = matchStart
markEnd = markStart + match[m].length
markEnd = start + startSpaces + captureGroup.length
}
tr.addMark(markStart, markEnd, markType.create(attrs))
tr.addMark(start, markEnd, markType.create(attributes))
tr.removeStoredMark(markType)
return tr
})
}

View File

@@ -1,13 +1,15 @@
import { InputRule } from 'prosemirror-inputrules'
import { NodeType } from 'prosemirror-model'
export default function (regexp: RegExp, type: NodeType, getAttrs?: (match: any) => any): InputRule {
export default function (regexp: RegExp, type: NodeType, getAttributes?: (match: any) => any): InputRule {
return new InputRule(regexp, (state, match, start, end) => {
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
const attributes = getAttributes instanceof Function
? getAttributes(match)
: getAttributes
const { tr } = state
if (match[0]) {
tr.replaceWith(start - 1, end, type.create(attrs))
tr.replaceWith(start - 1, end, type.create(attributes))
}
return tr

View File

@@ -2,7 +2,6 @@ import { Plugin } from 'prosemirror-state'
import { Slice, Fragment, MarkType } from 'prosemirror-model'
export default function (regexp: RegExp, type: MarkType, getAttrs?: (match: any) => any): Plugin {
const handler = (fragment: Fragment, parent?: any) => {
const nodes: any[] = []
@@ -33,7 +32,6 @@ export default function (regexp: RegExp, type: MarkType, getAttrs?: (match: any)
// adding the markdown part to nodes
nodes.push(child
.cut(textStart, textEnd)
// @ts-ignore
.mark(type.create(attrs).addToSet(child.marks)))
pos = matchEnd
@@ -59,5 +57,4 @@ export default function (regexp: RegExp, type: MarkType, getAttrs?: (match: any)
},
},
})
}

View File

@@ -1,9 +0,0 @@
import { Plugin, PluginKey } from 'prosemirror-state'
import Editor from '../..'
export default (editor: Editor) => new Plugin({
key: new PluginKey('editable'),
props: {
editable: () => editor.options.editable,
},
})

View File

@@ -1,28 +0,0 @@
import { Plugin } from 'prosemirror-state'
import Editor from '../..'
export default (editor: Editor) => new Plugin({
props: {
attributes: {
tabindex: '0',
},
handleDOMEvents: {
focus: () => {
editor.isFocused = true
const transaction = editor.state.tr.setMeta('focused', true)
editor.view.dispatch(transaction)
return true
},
blur: () => {
editor.isFocused = false
const transaction = editor.state.tr.setMeta('focused', false)
editor.view.dispatch(transaction)
return true
},
},
},
})

View File

@@ -1,16 +0,0 @@
import { keymap } from 'prosemirror-keymap'
import { baseKeymap } from 'prosemirror-commands'
import { dropCursor } from 'prosemirror-dropcursor'
import { gapCursor } from 'prosemirror-gapcursor'
import { undoInputRule } from 'prosemirror-inputrules'
import editable from './editable'
import focus from './focus'
export default [
() => dropCursor(),
() => gapCursor(),
() => keymap({ Backspace: undoInputRule }),
() => keymap(baseKeymap),
editable,
focus,
]

View File

@@ -9,6 +9,14 @@ const style = `.ProseMirror {
font-variant-ligatures: none;
}
.ProseMirror [contenteditable="false"] {
white-space: normal;
}
.ProseMirror [contenteditable="false"] [contenteditable="true"] {
white-space: pre-wrap;
}
.ProseMirror pre {
white-space: pre-wrap;
}

View File

@@ -1,6 +1,9 @@
import { Node } from 'prosemirror-model'
import { Decoration, NodeView } from 'prosemirror-view'
import { Extension } from './Extension'
import { NodeExtension } from './NodeExtension'
import { MarkExtension } from './MarkExtension'
import Editor from '..'
export type Extensions = (Extension | NodeExtension | MarkExtension)[]
@@ -40,3 +43,13 @@ export type Overwrite<T, U> = Pick<T, Diff<keyof T, keyof U>> & U;
export type AnyObject = {
[key: string]: any
}
export type NodeViewRendererProps = {
editor: Editor,
node: Node,
getPos: (() => number) | boolean,
decorations: Decoration[],
attributes: AnyObject,
}
export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView

View File

@@ -0,0 +1,17 @@
/**
* Optionally calls `value` as a function.
* Otherwise it is returned directly.
* @param value Function or any value.
* @param context Optional context to bind to function.
*/
export default function callOrReturn(value: any, context?: any) {
if (typeof value === 'function') {
if (context) {
return value.bind(context)()
}
return value()
}
return value
}

View File

@@ -1,3 +0,0 @@
export default function capitalize(value = ''): string {
return value.charAt(0).toUpperCase() + value.slice(1)
}

View File

@@ -0,0 +1,19 @@
export default function fromString(value: any) {
if (typeof value !== 'string') {
return value
}
if (value.match(/^\d*(\.\d+)?$/)) {
return Number(value)
}
if (value === 'true') {
return true
}
if (value === 'false') {
return false
}
return value
}

View File

@@ -1,11 +1,11 @@
import { Node } from 'prosemirror-model'
import getSchema from './getSchema'
import getHtmlFromFragment from './getHtmlFromFragment'
import getHTMLFromFragment from './getHTMLFromFragment'
import { Extensions } from '../types'
export default function generateHtml(doc: object, extensions: Extensions): string {
export default function generateHTML(doc: object, extensions: Extensions): string {
const schema = getSchema(extensions)
const contentNode = Node.fromJSON(schema, doc)
return getHtmlFromFragment(contentNode, schema)
return getHTMLFromFragment(contentNode, schema)
}

View File

@@ -1,6 +1,6 @@
import { Node, DOMSerializer, Schema } from 'prosemirror-model'
export default function getHtmlFromFragment(doc: Node, schema: Schema): string {
export default function getHTMLFromFragment(doc: Node, schema: Schema): string {
const fragment = DOMSerializer
.fromSchema(schema)
.serializeFragment(doc.content)

View File

@@ -1,8 +1,8 @@
import { Node, Mark } from 'prosemirror-model'
import { ExtensionAttribute } from '../types'
import { ExtensionAttribute, AnyObject } from '../types'
import mergeAttributes from './mergeAttributes'
export default function getRenderedAttributes(nodeOrMark: Node | Mark, extensionAttributes: ExtensionAttribute[]): { [key: string]: any } {
export default function getRenderedAttributes(nodeOrMark: Node | Mark, extensionAttributes: ExtensionAttribute[]): AnyObject {
return extensionAttributes
.filter(item => item.attribute.rendered)
.map(item => {

View File

@@ -5,6 +5,7 @@ import getAttributesFromExtensions from './getAttributesFromExtensions'
import getRenderedAttributes from './getRenderedAttributes'
import isEmptyObject from './isEmptyObject'
import injectExtensionAttributesToParseRule from './injectExtensionAttributesToParseRule'
import callOrReturn from './callOrReturn'
function cleanUpSchemaItem<T>(data: T) {
return Object.fromEntries(Object.entries(data).filter(([key, value]) => {
@@ -25,16 +26,16 @@ export default function getSchema(extensions: Extensions): Schema {
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,
content: callOrReturn(extension.content, context),
marks: callOrReturn(extension.marks, context),
group: callOrReturn(extension.group, context),
inline: callOrReturn(extension.inline, context),
atom: callOrReturn(extension.atom, context),
selectable: callOrReturn(extension.selectable, context),
draggable: callOrReturn(extension.draggable, context),
code: callOrReturn(extension.code, context),
defining: callOrReturn(extension.defining, context),
isolating: callOrReturn(extension.isolating, context),
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
})),
@@ -60,10 +61,10 @@ export default function getSchema(extensions: Extensions): Schema {
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,
inclusive: callOrReturn(extension.inclusive, context),
excludes: callOrReturn(extension.excludes, context),
group: callOrReturn(extension.group, context),
spanning: callOrReturn(extension.spanning, context),
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
})),

View File

@@ -1,5 +1,6 @@
import { ParseRule } from 'prosemirror-model'
import { ExtensionAttribute } from '../types'
import fromString from './fromString'
/**
* This function merges extension attributes into parserule attributes (`attrs` or `getAttrs`).
@@ -29,7 +30,7 @@ export default function injectExtensionAttributesToParseRule(parseRule: ParseRul
const attributes = item.attribute.parseHTML
? item.attribute.parseHTML(node as HTMLElement) || {}
: {
[item.name]: (node as HTMLElement).getAttribute(item.name),
[item.name]: fromString((node as HTMLElement).getAttribute(item.name)),
}
const filteredAttributes = Object.fromEntries(Object.entries(attributes)

View File

@@ -0,0 +1,20 @@
import { Extensions } from '../types'
import splitExtensions from './splitExtensions'
import callOrReturn from './callOrReturn'
export default function isList(name: string, extensions: Extensions) {
const { nodeExtensions } = splitExtensions(extensions)
const extension = nodeExtensions.find(item => item.name === name)
if (!extension) {
return false
}
const groups = callOrReturn(extension.group, { options: extension.options })
if (typeof groups !== 'string') {
return false
}
return groups.split(' ').includes('list')
}

View File

@@ -5,18 +5,19 @@ export default function mergeAttributes(...object: AnyObject[]) {
const mergedAttributes = { ...items }
Object.entries(item).forEach(([key, value]) => {
if (!mergedAttributes[key]) {
const exists = mergedAttributes[key]
if (!exists) {
mergedAttributes[key] = value
return
}
if (key === 'class') {
mergedAttributes[key] = [mergedAttributes[key], value].join(' ')
return
}
if (key === 'style') {
} else if (key === 'style') {
mergedAttributes[key] = [mergedAttributes[key], value].join('; ')
} else {
mergedAttributes[key] = value
}
})