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:
@@ -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)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export default abstract class ComponentRenderer {
|
||||
|
||||
static type: string
|
||||
|
||||
}
|
||||
@@ -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
|
||||
// Let’s store the editor instance in the DOM element.
|
||||
// So we’ll 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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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]
|
||||
}))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
9
packages/core/src/commands/blur.ts
Normal file
9
packages/core/src/commands/blur.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Command } from '../Editor'
|
||||
|
||||
export default (): Command => ({ view }) => {
|
||||
const element = view.dom as HTMLElement
|
||||
|
||||
element.blur()
|
||||
|
||||
return true
|
||||
}
|
||||
5
packages/core/src/commands/clearContent.ts
Normal file
5
packages/core/src/commands/clearContent.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Command } from '../Editor'
|
||||
|
||||
export default (emitUpdate: Boolean = false): Command => ({ commands }) => {
|
||||
return commands.setContent('', emitUpdate)
|
||||
}
|
||||
29
packages/core/src/commands/clearNodes.ts
Normal file
29
packages/core/src/commands/clearNodes.ts
Normal 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
|
||||
}
|
||||
6
packages/core/src/commands/deleteSelection.ts
Normal file
6
packages/core/src/commands/deleteSelection.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { deleteSelection } from 'prosemirror-commands'
|
||||
import { Command } from '../Editor'
|
||||
|
||||
export default (): Command => ({ state, dispatch }) => {
|
||||
return deleteSelection(state, dispatch)
|
||||
}
|
||||
58
packages/core/src/commands/focus.ts
Normal file
58
packages/core/src/commands/focus.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
9
packages/core/src/commands/insertText.ts
Normal file
9
packages/core/src/commands/insertText.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Command } from '../Editor'
|
||||
|
||||
export default (value: string): Command => ({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
tr.insertText(value)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
10
packages/core/src/commands/liftListItem.ts
Normal file
10
packages/core/src/commands/liftListItem.ts
Normal 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)
|
||||
}
|
||||
26
packages/core/src/commands/removeMark.ts
Normal file
26
packages/core/src/commands/removeMark.ts
Normal 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
|
||||
}
|
||||
20
packages/core/src/commands/removeMarks.ts
Normal file
20
packages/core/src/commands/removeMarks.ts
Normal 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
|
||||
}
|
||||
24
packages/core/src/commands/resetNodeAttributes.ts
Normal file
24
packages/core/src/commands/resetNodeAttributes.ts
Normal 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
|
||||
}
|
||||
9
packages/core/src/commands/scrollIntoView.ts
Normal file
9
packages/core/src/commands/scrollIntoView.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Command } from '../Editor'
|
||||
|
||||
export default (): Command => ({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
tr.scrollIntoView()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
6
packages/core/src/commands/selectAll.ts
Normal file
6
packages/core/src/commands/selectAll.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { selectAll } from 'prosemirror-commands'
|
||||
import { Command } from '../Editor'
|
||||
|
||||
export default (): Command => ({ state, dispatch }) => {
|
||||
return selectAll(state, dispatch)
|
||||
}
|
||||
6
packages/core/src/commands/selectParentNode.ts
Normal file
6
packages/core/src/commands/selectParentNode.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { selectParentNode } from 'prosemirror-commands'
|
||||
import { Command } from '../Editor'
|
||||
|
||||
export default (): Command => ({ state, dispatch }) => {
|
||||
return selectParentNode(state, dispatch)
|
||||
}
|
||||
10
packages/core/src/commands/setBlockType.ts
Normal file
10
packages/core/src/commands/setBlockType.ts
Normal 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)
|
||||
}
|
||||
17
packages/core/src/commands/setContent.ts
Normal file
17
packages/core/src/commands/setContent.ts
Normal 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
|
||||
}
|
||||
17
packages/core/src/commands/setNodeAttributes.ts
Normal file
17
packages/core/src/commands/setNodeAttributes.ts
Normal 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
|
||||
}
|
||||
10
packages/core/src/commands/sinkListItem.ts
Normal file
10
packages/core/src/commands/sinkListItem.ts
Normal 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)
|
||||
}
|
||||
117
packages/core/src/commands/splitBlock.ts
Normal file
117
packages/core/src/commands/splitBlock.ts
Normal 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
|
||||
}
|
||||
10
packages/core/src/commands/splitListItem.ts
Normal file
10
packages/core/src/commands/splitListItem.ts
Normal 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)
|
||||
}
|
||||
16
packages/core/src/commands/toggleBlockType.ts
Normal file
16
packages/core/src/commands/toggleBlockType.ts
Normal 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)
|
||||
}
|
||||
52
packages/core/src/commands/toggleList.ts
Normal file
52
packages/core/src/commands/toggleList.ts
Normal 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)
|
||||
}
|
||||
19
packages/core/src/commands/toggleMark.ts
Normal file
19
packages/core/src/commands/toggleMark.ts
Normal 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)
|
||||
}
|
||||
16
packages/core/src/commands/toggleWrap.ts
Normal file
16
packages/core/src/commands/toggleWrap.ts
Normal 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)
|
||||
}
|
||||
15
packages/core/src/commands/try.ts
Normal file
15
packages/core/src/commands/try.ts
Normal 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
|
||||
}
|
||||
32
packages/core/src/commands/updateMark.ts
Normal file
32
packages/core/src/commands/updateMark.ts
Normal 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
|
||||
}
|
||||
10
packages/core/src/commands/wrapInList.ts
Normal file
10
packages/core/src/commands/wrapInList.ts
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
68
packages/core/src/extensions/commands.ts
Normal file
68
packages/core/src/extensions/commands.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
22
packages/core/src/extensions/editable.ts
Normal file
22
packages/core/src/extensions/editable.ts
Normal 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,
|
||||
// }
|
||||
// }
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
42
packages/core/src/extensions/focusEvents.ts
Normal file
42
packages/core/src/extensions/focusEvents.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
52
packages/core/src/extensions/keymap.ts
Normal file
52
packages/core/src/extensions/keymap.ts
Normal 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 don’t need a custom `selectAll` for now
|
||||
// 'Mod-a': () => this.editor.selectAll(),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
declare module '../Editor' {
|
||||
interface AllExtensions {
|
||||
Keymap: typeof Keymap,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
]
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
17
packages/core/src/utils/callOrReturn.ts
Normal file
17
packages/core/src/utils/callOrReturn.ts
Normal 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
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function capitalize(value = ''): string {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1)
|
||||
}
|
||||
19
packages/core/src/utils/fromString.ts
Normal file
19
packages/core/src/utils/fromString.ts
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 }]
|
||||
})),
|
||||
|
||||
@@ -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)
|
||||
|
||||
20
packages/core/src/utils/isList.ts
Normal file
20
packages/core/src/utils/isList.ts
Normal 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')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user