Merge branch 'main' into feature/plugin-order
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
21
packages/core/src/helpers/getSplittedAttributes.ts
Normal file
21
packages/core/src/helpers/getSplittedAttributes.ts
Normal 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
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const TaskItem = Node.create({
|
|||||||
renderHTML: attributes => ({
|
renderHTML: attributes => ({
|
||||||
'data-checked': attributes.checked,
|
'data-checked': attributes.checked,
|
||||||
}),
|
}),
|
||||||
|
keepOnSplit: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
Reference in New Issue
Block a user