diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-trailing-node.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-trailing-node.extension.ts new file mode 100644 index 0000000000..f76956fa90 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-trailing-node.extension.ts @@ -0,0 +1,71 @@ +/* This Source Code has been derived from Tiptap. + * https://github.com/ueberdosis/tiptap/blob/v2.11.5/demos/src/Experiments/TrailingNode/Vue/trailing-node.ts + * SPDX-License-Identifier: MIT + * Copyright © 2023 Tiptap GmbH. + * Modifications are licensed under the MIT License. + */ + +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +// @ts-ignore +function nodeEqualsType({ types, node }) { + return (Array.isArray(types) && types.includes(node.type)) || node.type === types; +} + +export interface TrailingNodeOptions { + node: string; + notAfter: string[]; +} + +export const TrailingNode = Extension.create({ + name: 'trailingNode', + + addOptions() { + return { + node: 'paragraph', + notAfter: ['paragraph'], + }; + }, + + addProseMirrorPlugins() { + const plugin = new PluginKey(this.name); + const disabledNodes = Object.entries(this.editor.schema.nodes) + .map(([, value]) => value) + .filter((node) => this.options.notAfter.includes(node.name)); + + return [ + new Plugin({ + key: plugin, + appendTransaction: (_, __, state) => { + const { doc, tr, schema } = state; + const shouldInsertNodeAtEnd = plugin.getState(state); + const endPosition = doc.content.size; + const type = schema.nodes[this.options.node]; + + if (!shouldInsertNodeAtEnd) { + return; + } + + return tr.insert(endPosition, type.create()); + }, + state: { + init: (_, state) => { + const lastNode = state.tr.doc.lastChild; + + return !nodeEqualsType({ node: lastNode, types: disabledNodes }); + }, + apply: (tr, value) => { + if (!tr.docChanged) { + return value; + } + + const lastNode = tr.doc.lastChild; + + return !nodeEqualsType({ node: lastNode, types: disabledNodes }); + }, + }, + }), + ]; + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts index 7da80dbf7c..ac8854a021 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts @@ -33,6 +33,7 @@ export * from './extensions/tiptap-figcaption.extension.js'; export * from './extensions/tiptap-figure.extension.js'; export * from './extensions/tiptap-span.extension.js'; export * from './extensions/tiptap-html-global-attributes.extension.js'; +export * from './extensions/tiptap-trailing-node.extension.js'; export * from './extensions/tiptap-umb-embedded-media.extension.js'; export * from './extensions/tiptap-umb-image.extension.js'; export * from './extensions/tiptap-umb-link.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts index b2922ea618..6e0583222a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts @@ -1,6 +1,13 @@ import { UmbTiptapExtensionApiBase } from '../base.js'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; -import { Div, HtmlGlobalAttributes, Placeholder, Span, StarterKit } from '@umbraco-cms/backoffice/external/tiptap'; +import { + Div, + HtmlGlobalAttributes, + Placeholder, + Span, + StarterKit, + TrailingNode, +} from '@umbraco-cms/backoffice/external/tiptap'; export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionApiBase { #localize = new UmbLocalizationController(this); @@ -44,6 +51,7 @@ export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionA 'umbLink', ], }), + TrailingNode, ]; }