add basic schema extender

This commit is contained in:
Philipp Kühn
2021-02-19 09:54:39 +01:00
parent 85eceb32b8
commit 6f9557294e
7 changed files with 88 additions and 13 deletions

View File

@@ -2,6 +2,7 @@ import { Plugin, Transaction } from 'prosemirror-state'
import { Command as ProseMirrorCommand } from 'prosemirror-commands'
import { InputRule } from 'prosemirror-inputrules'
import { Editor } from './Editor'
import { Node } from './Node'
import mergeDeep from './utilities/mergeDeep'
import { GlobalAttributes, RawCommands } from './types'
@@ -65,6 +66,30 @@ export interface ExtensionConfig<Options = any> {
editor: Editor,
}) => Plugin[],
/**
* Extend Node Schema
*/
extendNodeSchema?: ((
this: {
options: Options,
},
extension: Node,
) => {
[key: string]: any,
}) | null,
/**
* Extend Mark Schema
*/
extendMarkSchema?: ((
this: {
options: Options,
},
extension: Node,
) => {
[key: string]: any,
}) | null,
/**
* The editor is ready.
*/
@@ -149,6 +174,8 @@ export class Extension<Options = any> {
addInputRules: () => [],
addPasteRules: () => [],
addProseMirrorPlugins: () => [],
extendNodeSchema: null,
extendMarkSchema: null,
onCreate: null,
onUpdate: null,
onSelection: null,

View File

@@ -209,6 +209,8 @@ export class Mark<Options = any> {
parseHTML: () => null,
renderHTML: null,
addAttributes: () => ({}),
extendNodeSchema: null,
extendMarkSchema: null,
onCreate: null,
onUpdate: null,
onSelection: null,

View File

@@ -70,12 +70,6 @@ export interface NodeConfig<Options = any> extends Overwrite<ExtensionConfig<Opt
*/
isolating?: NodeSpec['isolating'] | ((this: { options: Options }) => NodeSpec['isolating']),
// TODO: extend via extension-table
/**
* Table Role
*/
tableRole?: NodeSpec['tableRole'] | ((this: { options: Options }) => NodeSpec['tableRole']),
/**
* Parse HTML
*/
@@ -284,6 +278,8 @@ export class Node<Options = any> {
renderText: null,
addAttributes: () => ({}),
addNodeView: null,
extendNodeSchema: null,
extendMarkSchema: null,
onCreate: null,
onUpdate: null,
onSelection: null,
@@ -291,8 +287,6 @@ export class Node<Options = any> {
onFocus: null,
onBlur: null,
onDestroy: null,
// TODO: remove,
tableRole: null,
}
options!: Options

View File

@@ -1,5 +1,6 @@
import { NodeSpec, MarkSpec, Schema } from 'prosemirror-model'
import { Extensions } from '../types'
import { ExtensionConfig } from '../Extension'
import splitExtensions from './splitExtensions'
import getAttributesFromExtensions from './getAttributesFromExtensions'
import getRenderedAttributes from './getRenderedAttributes'
@@ -21,11 +22,34 @@ export default function getSchema(extensions: Extensions): Schema {
const allAttributes = getAttributesFromExtensions(extensions)
const { nodeExtensions, markExtensions } = splitExtensions(extensions)
const topNode = nodeExtensions.find(extension => extension.config.topNode)?.config.name
const nodeSchemaExtenders: ExtensionConfig['extendNodeSchema'][] = []
const markSchemaExtenders: ExtensionConfig['extendMarkSchema'][] = []
extensions.forEach(extension => {
if (typeof extension.config.extendNodeSchema === 'function') {
nodeSchemaExtenders.push(extension.config.extendNodeSchema)
}
if (typeof extension.config.extendMarkSchema === 'function') {
markSchemaExtenders.push(extension.config.extendMarkSchema)
}
})
const nodes = Object.fromEntries(nodeExtensions.map(extension => {
const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.config.name)
const context = { options: extension.options }
const extraNodeFields = nodeSchemaExtenders.reduce((fields, nodeSchemaExtender) => {
const extraFields = callOrReturn(nodeSchemaExtender, context, extension)
return {
...fields,
...extraFields,
}
}, {})
const schema: NodeSpec = cleanUpSchemaItem({
...extraNodeFields,
content: callOrReturn(extension.config.content, context),
marks: callOrReturn(extension.config.marks, context),
group: callOrReturn(extension.config.group, context),
@@ -36,7 +60,6 @@ export default function getSchema(extensions: Extensions): Schema {
code: callOrReturn(extension.config.code, context),
defining: callOrReturn(extension.config.defining, context),
isolating: callOrReturn(extension.config.isolating, context),
tableRole: callOrReturn(extension.config.tableRole, context),
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
})),
@@ -61,7 +84,18 @@ export default function getSchema(extensions: Extensions): Schema {
const marks = Object.fromEntries(markExtensions.map(extension => {
const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.config.name)
const context = { options: extension.options }
const extraMarkFields = markSchemaExtenders.reduce((fields, markSchemaExtender) => {
const extraFields = callOrReturn(markSchemaExtender, context, extension)
return {
...fields,
...extraFields,
}
}, {})
const schema: MarkSpec = cleanUpSchemaItem({
...extraMarkFields,
inclusive: callOrReturn(extension.config.inclusive, context),
excludes: callOrReturn(extension.config.excludes, context),
group: callOrReturn(extension.config.group, context),

View File

@@ -8,11 +8,12 @@ export { default as nodeInputRule } from './inputRules/nodeInputRule'
export { default as markInputRule } from './inputRules/markInputRule'
export { default as markPasteRule } from './pasteRules/markPasteRule'
export { default as callOrReturn } from './utilities/callOrReturn'
export { default as mergeAttributes } from './utilities/mergeAttributes'
export { default as generateHTML } from './helpers/generateHTML'
export { default as getSchema } from './helpers/getSchema'
export { default as getHTMLFromFragment } from './helpers/getHTMLFromFragment'
export { default as getMarkAttributes } from './helpers/getMarkAttributes'
export { default as mergeAttributes } from './utilities/mergeAttributes'
export { default as isActive } from './helpers/isActive'
export { default as isMarkActive } from './helpers/isMarkActive'
export { default as isNodeActive } from './helpers/isNodeActive'

View File

@@ -3,14 +3,15 @@
* Otherwise it is returned directly.
* @param value Function or any value.
* @param context Optional context to bind to function.
* @param props Optional props to pass to function.
*/
export default function callOrReturn(value: any, context?: any): any {
export default function callOrReturn(value: any, context: any = undefined, ...props: any[]): any {
if (typeof value === 'function') {
if (context) {
return value.bind(context)()
return value.bind(context)(...props)
}
return value()
return value(...props)
}
return value

View File

@@ -4,6 +4,7 @@ import {
mergeAttributes,
isCellSelection,
findParentNodeClosestToPos,
callOrReturn,
} from '@tiptap/core'
import {
tableEditing,
@@ -64,6 +65,13 @@ declare module '@tiptap/core' {
fixTables: () => Command,
}
}
interface NodeConfig<Options> {
/**
* Table Role
*/
tableRole?: string | ((this: { options: Options }) => string),
}
}
export const Table = Node.create<TableOptions>({
@@ -81,6 +89,14 @@ export const Table = Node.create<TableOptions>({
allowTableNodeSelection: false,
},
extendNodeSchema(extension) {
const context = { options: extension.options }
return {
tableRole: callOrReturn(extension.config.tableRole, context),
}
},
content: 'tableRow+',
tableRole: 'table',