Merge branch 'main' into feature/plugin-order

This commit is contained in:
Philipp Kühn
2021-01-29 09:36:00 +01:00
9 changed files with 152 additions and 30 deletions

View File

@@ -96,15 +96,18 @@ export default class ExtensionManager {
.flat() .flat()
} }
get attributes() {
return getAttributesFromExtensions(this.extensions)
}
get nodeViews() { get nodeViews() {
const { editor } = this const { editor } = this
const { nodeExtensions } = splitExtensions(this.extensions) const { nodeExtensions } = splitExtensions(this.extensions)
const allAttributes = getAttributesFromExtensions(this.extensions)
return Object.fromEntries(nodeExtensions return Object.fromEntries(nodeExtensions
.filter(extension => !!extension.config.addNodeView) .filter(extension => !!extension.config.addNodeView)
.map(extension => { .map(extension => {
const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.config.name) const extensionAttributes = this.attributes.filter(attribute => attribute.type === extension.config.name)
const context = { const context = {
options: extension.options, options: extension.options,
editor, editor,

View File

@@ -18,7 +18,7 @@ export const clearNodes = (): Command => ({ state, tr, dispatch }) => {
const targetLiftDepth = liftTarget(nodeRange) const targetLiftDepth = liftTarget(nodeRange)
if (node.type.isTextblock && dispatch) { if (node.type.isTextblock && dispatch) {
tr.setNodeMarkup(nodeRange.start, state.schema.nodes.paragraph) tr.setNodeMarkup(nodeRange.start, state.doc.type.contentMatch.defaultType)
} }
if ((targetLiftDepth || targetLiftDepth === 0) && dispatch) { if ((targetLiftDepth || targetLiftDepth === 0) && dispatch) {

View File

@@ -2,6 +2,7 @@ import { canSplit } from 'prosemirror-transform'
import { ContentMatch, Fragment } from 'prosemirror-model' import { ContentMatch, Fragment } from 'prosemirror-model'
import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state' import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'
import { Command } from '../types' import { Command } from '../types'
import getSplittedAttributes from '../helpers/getSplittedAttributes'
function defaultBlockAt(match: ContentMatch) { function defaultBlockAt(match: ContentMatch) {
for (let i = 0; i < match.edgeCount; i + 1) { for (let i = 0; i < match.edgeCount; i + 1) {
@@ -15,8 +16,7 @@ function defaultBlockAt(match: ContentMatch) {
} }
export interface SplitBlockOptions { export interface SplitBlockOptions {
withAttributes: boolean, keepMarks: boolean,
withMarks: boolean,
} }
function keepMarks(state: EditorState) { function keepMarks(state: EditorState) {
@@ -31,14 +31,24 @@ function keepMarks(state: EditorState) {
/** /**
* Forks a new node from an existing node. * Forks a new node from an existing node.
*/ */
export const splitBlock = (options: Partial<SplitBlockOptions> = {}): Command => ({ tr, state, dispatch }) => { export const splitBlock = (options: Partial<SplitBlockOptions> = {}): Command => ({
tr,
state,
dispatch,
editor,
}) => {
const defaultOptions: SplitBlockOptions = { const defaultOptions: SplitBlockOptions = {
withAttributes: false, keepMarks: true,
withMarks: true,
} }
const config = { ...defaultOptions, ...options } const config = { ...defaultOptions, ...options }
const { selection, doc } = tr const { selection, doc } = tr
const { $from, $to } = selection const { $from, $to } = selection
const extensionAttributes = editor.extensionManager.attributes
const newAttributes = getSplittedAttributes(
extensionAttributes,
$from.node().type.name,
$from.node().attrs,
)
if (selection instanceof NodeSelection && selection.node.isBlock) { if (selection instanceof NodeSelection && selection.node.isBlock) {
if (!$from.parentOffset || !canSplit(doc, $from.pos)) { if (!$from.parentOffset || !canSplit(doc, $from.pos)) {
@@ -46,7 +56,7 @@ export const splitBlock = (options: Partial<SplitBlockOptions> = {}): Command =>
} }
if (dispatch) { if (dispatch) {
if (config.withMarks) { if (config.keepMarks) {
keepMarks(state) keepMarks(state)
} }
@@ -74,9 +84,7 @@ export const splitBlock = (options: Partial<SplitBlockOptions> = {}): Command =>
let types = atEnd && deflt let types = atEnd && deflt
? [{ ? [{
type: deflt, type: deflt,
attrs: config.withAttributes attrs: newAttributes,
? $from.node().attrs
: {},
}] }]
: undefined : undefined
@@ -91,9 +99,7 @@ export const splitBlock = (options: Partial<SplitBlockOptions> = {}): Command =>
types = deflt types = deflt
? [{ ? [{
type: deflt, type: deflt,
attrs: config.withAttributes attrs: newAttributes,
? $from.node().attrs
: {},
}] }]
: undefined : undefined
} }
@@ -111,7 +117,7 @@ export const splitBlock = (options: Partial<SplitBlockOptions> = {}): Command =>
} }
} }
if (config.withMarks) { if (config.keepMarks) {
keepMarks(state) keepMarks(state)
} }

View File

@@ -1,13 +1,112 @@
import { splitListItem as originalSplitListItem } from 'prosemirror-schema-list' import {
import { NodeType } from 'prosemirror-model' NodeType,
Node as ProseMirrorNode,
Fragment,
Slice,
} from 'prosemirror-model'
import { canSplit } from 'prosemirror-transform'
import { TextSelection } from 'prosemirror-state'
import { Command } from '../types' import { Command } from '../types'
import getNodeType from '../helpers/getNodeType' import getNodeType from '../helpers/getNodeType'
import getSplittedAttributes from '../helpers/getSplittedAttributes'
/** /**
* Splits one list item into two list items. * Splits one list item into two list items.
*/ */
export const splitListItem = (typeOrName: string | NodeType): Command => ({ state, dispatch }) => { export const splitListItem = (typeOrName: string | NodeType): Command => ({
tr, state, dispatch, editor,
}) => {
const type = getNodeType(typeOrName, state.schema) const type = getNodeType(typeOrName, state.schema)
const { $from, $to } = state.selection
return originalSplitListItem(type)(state, dispatch) // @ts-ignore
// eslint-disable-next-line
const node: ProseMirrorNode = state.selection.node
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) {
return false
}
const grandParent = $from.node(-1)
if (grandParent.type !== type) {
return false
}
const extensionAttributes = editor.extensionManager.attributes
if ($from.parent.content.size === 0 && $from.node(-1).childCount === $from.indexAfter(-1)) {
// In an empty block. If this is a nested list, the wrapping
// list item should be split. Otherwise, bail out and let next
// command handle lifting.
if (
$from.depth === 2
|| $from.node(-3).type !== type
|| $from.index(-2) !== $from.node(-2).childCount - 1
) {
return false
}
if (dispatch) {
let wrap = Fragment.empty
const keepItem = $from.index(-1) > 0
// Build a fragment containing empty versions of the structure
// from the outer list item to the parent node of the cursor
for (let d = $from.depth - (keepItem ? 1 : 2); d >= $from.depth - 3; d -= 1) {
wrap = Fragment.from($from.node(d).copy(wrap))
}
// Add a second list item with an empty default start node
const newNextTypeAttributes = getSplittedAttributes(
extensionAttributes,
$from.node().type.name,
$from.node().attrs,
)
const nextType = type.contentMatch.defaultType?.createAndFill(newNextTypeAttributes) || undefined
wrap = wrap.append(Fragment.from(type.createAndFill(null, nextType) || undefined))
tr
.replace(
$from.before(keepItem ? undefined : -1),
$from.after(-3),
new Slice(wrap, keepItem ? 3 : 2, 2),
)
.setSelection(TextSelection.near(tr.doc.resolve($from.pos + (keepItem ? 3 : 2))))
.scrollIntoView()
}
return true
}
const nextType = $to.pos === $from.end()
? grandParent.contentMatchAt(0).defaultType
: null
const newTypeAttributes = getSplittedAttributes(
extensionAttributes,
grandParent.type.name,
grandParent.attrs,
)
const newNextTypeAttributes = getSplittedAttributes(
extensionAttributes,
$from.node().type.name,
$from.node().attrs,
)
tr.delete($from.pos, $to.pos)
const types = nextType
? [{ type, attrs: newTypeAttributes }, { type: nextType, attrs: newNextTypeAttributes }]
: [{ type, attrs: newTypeAttributes }]
if (!canSplit(tr.doc, $from.pos, 2)) {
return false
}
if (dispatch) {
tr.split($from.pos, 2, types).scrollIntoView()
}
return true
} }

View File

@@ -20,6 +20,7 @@ export default function getAttributesFromExtensions(extensions: Extensions): Ext
rendered: true, rendered: true,
renderHTML: null, renderHTML: null,
parseHTML: null, parseHTML: null,
keepOnSplit: true,
} }
extensions.forEach(extension => { extensions.forEach(extension => {

View File

@@ -0,0 +1,21 @@
import { AnyObject, ExtensionAttribute } from '../types'
export default function getSplittedAttributes(
extensionAttributes: ExtensionAttribute[],
typeName: string,
attributes: AnyObject,
): AnyObject {
return Object.fromEntries(Object
.entries(attributes)
.filter(([name]) => {
const extensionAttribute = extensionAttributes.find(item => {
return item.type === typeName && item.name === name
})
if (!extensionAttribute) {
return false
}
return extensionAttribute.attribute.keepOnSplit
}))
}

View File

@@ -60,6 +60,7 @@ export type Attribute = {
rendered?: boolean, rendered?: boolean,
renderHTML?: ((attributes: { [key: string]: any }) => { [key: string]: any } | null) | null, renderHTML?: ((attributes: { [key: string]: any }) => { [key: string]: any } | null) | null,
parseHTML?: ((element: HTMLElement) => { [key: string]: any } | null) | null, parseHTML?: ((element: HTMLElement) => { [key: string]: any } | null) | null,
keepOnSplit: boolean,
} }
export type Attributes = { export type Attributes = {

View File

@@ -34,6 +34,7 @@ export const TaskItem = Node.create({
renderHTML: attributes => ({ renderHTML: attributes => ({
'data-checked': attributes.checked, 'data-checked': attributes.checked,
}), }),
keepOnSplit: false,
}, },
} }
}, },

View File

@@ -57,16 +57,6 @@ export const TextAlign = Extension.create({
addKeyboardShortcuts() { addKeyboardShortcuts() {
return { return {
// TODO: re-use only 'textAlign' attribute
// TODO: use custom splitBlock only for `this.options.types`
Enter: () => this.editor.commands.first(({ commands }) => [
() => commands.newlineInCode(),
() => commands.createParagraphNear(),
() => commands.liftEmptyBlock(),
() => commands.splitBlock({
withAttributes: true,
}),
]),
'Mod-Shift-l': () => this.editor.commands.setTextAlign('left'), 'Mod-Shift-l': () => this.editor.commands.setTextAlign('left'),
'Mod-Shift-e': () => this.editor.commands.setTextAlign('center'), 'Mod-Shift-e': () => this.editor.commands.setTextAlign('center'),
'Mod-Shift-r': () => this.editor.commands.setTextAlign('right'), 'Mod-Shift-r': () => this.editor.commands.setTextAlign('right'),