diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/html-attr-class/html-attr-class.tiptap-extension.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/html-attr-class/html-attr-class.tiptap-extension.ts index 3f8bbdee192e..154a68377b82 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/html-attr-class/html-attr-class.tiptap-extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/html-attr-class/html-attr-class.tiptap-extension.ts @@ -47,8 +47,35 @@ export const HtmlClassAttribute = Extension.create { if (!className) return false; const types = type ? [type] : this.options.types; - const existing = types.map((type) => editor.getAttributes(type)?.class as string).filter((x) => x); - return existing.length ? commands.unsetClassName(type) : commands.setClassName(className, type); + + const toggleClasses = className.split(/\s+/).filter((c) => c); + if (toggleClasses.length === 0) { + return true; + } + + return types + .map((t) => { + const existingClass = (editor.getAttributes(t)?.class as string) ?? ''; + const classes = existingClass.split(/\s+/).filter((c) => c); + const hasAllToggleClasses = toggleClasses.every((c) => classes.includes(c)); + + let newClasses: Array; + if (hasAllToggleClasses) { + // All toggle classes present: remove them + newClasses = classes.filter((c) => !toggleClasses.includes(c)); + } else { + // Not all toggle classes present: add missing ones + const toAdd = toggleClasses.filter((c) => !classes.includes(c)); + newClasses = [...classes, ...toAdd]; + } + + if (newClasses.length === 0) { + // No classes left, remove the attribute entirely + return commands.resetAttributes(t, 'class'); + } + return commands.updateAttributes(t, { class: newClasses.join(' ') }); + }) + .every((response) => response); }, unsetClassName: (type) => diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/link/link.tiptap-extension.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/link/link.tiptap-extension.ts index 0384aa3ed66f..f70da6631897 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/link/link.tiptap-extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/link/link.tiptap-extension.ts @@ -33,6 +33,20 @@ export const UmbLink = Link.extend({ addCommands() { return { + // TODO: [v17] Remove the `@ts-expect-error` once Tiptap has resolved the TypeScript definitions. [LK:2025-10-01] + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + ensureUmbLink: (attributes) => { + // TODO: [v17] Remove the `@ts-expect-error` once Tiptap has resolved the TypeScript definitions. [LK:2025-10-01] + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return ({ editor, chain }) => { + if (editor.isActive(this.name)) { + return true; + } + return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run(); + }; + }, // TODO: [v17] Remove the `@ts-expect-error` once Tiptap has resolved the TypeScript definitions. [LK:2025-10-01] // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error @@ -59,6 +73,14 @@ export const UmbLink = Link.extend({ declare module '@tiptap/core' { interface Commands { umbLink: { + ensureUmbLink: (attributes: { + type: string; + href: string; + 'data-anchor'?: string | null; + target?: string | null; + title?: string | null; + }) => ReturnType; + setUmbLink: (attributes: { type: string; href: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-menu/style-menu.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-menu/style-menu.tiptap-toolbar-api.ts index f3b991cb4959..f73fa051eec7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-menu/style-menu.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-menu/style-menu.tiptap-toolbar-api.ts @@ -4,7 +4,7 @@ import type { ChainedCommands, Editor } from '../../externals.js'; type UmbTiptapToolbarStyleMenuCommandType = { type: string; - command: (chain: ChainedCommands) => ChainedCommands; + command?: (chain: ChainedCommands) => ChainedCommands; isActive?: (editor?: Editor) => boolean | undefined; }; @@ -25,6 +25,7 @@ export default class UmbTiptapToolbarStyleMenuApi extends UmbTiptapToolbarElemen h5: this.#headingCommand(5), h6: this.#headingCommand(6), p: { type: 'paragraph', command: (chain) => chain.setParagraph() }, + a: { type: 'umbLink', command: (chain) => chain.ensureUmbLink({ type: 'external', href: '#' }) }, blockquote: { type: 'blockquote', command: (chain) => chain.toggleBlockquote() }, code: { type: 'code', command: (chain) => chain.toggleCode() }, codeBlock: { type: 'codeBlock', command: (chain) => chain.toggleCodeBlock() },