diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts index 011a8209c6..69c0cbdd1f 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts @@ -64,6 +64,14 @@ export const HtmlGlobalAttributes = Extension.create commands.updateAttributes(type, { class: className })) .every((response) => response); }, + toggleClassName: + (className, type) => + ({ commands, editor }) => { + 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); + }, unsetClassName: (type) => ({ commands }) => { @@ -77,12 +85,41 @@ export const HtmlGlobalAttributes = Extension.create commands.updateAttributes(type, { id })).every((response) => response); }, + toggleId: + (id, type) => + ({ commands, editor }) => { + if (!id) return false; + const types = type ? [type] : this.options.types; + const existing = types.map((type) => editor.getAttributes(type)?.id as string).filter((x) => x); + return existing.length ? commands.unsetId(type) : commands.setId(id, type); + }, unsetId: (type) => ({ commands }) => { const types = type ? [type] : this.options.types; return types.map((type) => commands.resetAttributes(type, 'id')).every((response) => response); }, + setStyles: + (style, type) => + ({ commands }) => { + if (!style) return false; + const types = type ? [type] : this.options.types; + return types.map((type) => commands.updateAttributes(type, { style })).every((response) => response); + }, + toggleStyles: + (style, type) => + ({ commands, editor }) => { + if (!style) return false; + const types = type ? [type] : this.options.types; + const existing = types.map((type) => editor.getAttributes(type)?.style as string).filter((x) => x); + return existing.length ? commands.unsetStyles(type) : commands.setStyles(style, type); + }, + unsetStyles: + (type) => + ({ commands }) => { + const types = type ? [type] : this.options.types; + return types.map((type) => commands.resetAttributes(type, 'style')).every((response) => response); + }, }; }, }); @@ -91,9 +128,14 @@ declare module '@tiptap/core' { interface Commands { htmlGlobalAttributes: { setClassName: (className?: string, type?: string) => ReturnType; + toggleClassName: (className?: string, type?: string) => ReturnType; unsetClassName: (type?: string) => ReturnType; setId: (id?: string, type?: string) => ReturnType; + toggleId: (id?: string, type?: string) => ReturnType; unsetId: (type?: string) => ReturnType; + setStyles: (style?: string, type?: string) => ReturnType; + toggleStyles: (style?: string, type?: string) => ReturnType; + unsetStyles: (type?: string) => ReturnType; }; } } diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts index 6d49c46461..4f6d241f4b 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts @@ -9,6 +9,31 @@ export interface SpanOptions { HTMLAttributes: Record; } +function parseStyles(style: string | undefined): Record { + const items: Record = {}; + + (style ?? '') + .split(';') + .map((x) => x.trim()) + .filter((x) => x) + .forEach((rule) => { + const [key, value] = rule.split(':'); + if (key && value) { + items[key.trim()] = value.trim(); + } + }); + + return items; +} + +function serializeStyles(items: Record): string { + return ( + Object.entries(items) + .map(([key, value]) => `${key}: ${value}`) + .join(';') + ';' + ); +} + export const Span = Mark.create({ name: 'span', @@ -32,29 +57,61 @@ export const Span = Mark.create({ if (!styles) return false; const existing = editor.getAttributes(this.name)?.style as string; - if (!existing && !editor.isActive(this.name)) { return commands.setMark(this.name, { style: styles }); } - const rules = ((existing ?? '') + ';' + styles).split(';'); - const items: Record = {}; + const items = { + ...parseStyles(existing), + ...parseStyles(styles), + }; - rules - .filter((x) => x) - .forEach((rule) => { - if (rule.trim() !== '') { - const [key, value] = rule.split(':'); - items[key.trim()] = value.trim(); - } - }); - - const style = Object.entries(items) - .map(([key, value]) => `${key}: ${value}`) - .join(';'); + const style = serializeStyles(items); + if (style === ';') return false; return commands.updateAttributes(this.name, { style }); }, + toggleSpanStyle: + (styles) => + ({ commands, editor }) => { + if (!styles) return false; + const existing = editor.getAttributes(this.name)?.style as string; + return existing?.includes(styles) === true ? commands.unsetSpanStyle(styles) : commands.setSpanStyle(styles); + }, + unsetSpanStyle: + (styles) => + ({ commands, editor }) => { + if (!styles) return false; + + parseStyles(styles); + + const toBeRemoved = new Set(); + + styles + .split(';') + .map((x) => x.trim()) + .filter((x) => x) + .forEach((rule) => { + const [key] = rule.split(':'); + if (key) toBeRemoved.add(key.trim()); + }); + + if (toBeRemoved.size === 0) return false; + + const existing = editor.getAttributes(this.name)?.style as string; + const items = parseStyles(existing); + + // Remove keys + for (const key of toBeRemoved) { + delete items[key]; + } + + const style = serializeStyles(items); + + return style === ';' + ? commands.resetAttributes(this.name, 'style') + : commands.updateAttributes(this.name, { style }); + }, }; }, }); @@ -63,6 +120,8 @@ declare module '@tiptap/core' { interface Commands { span: { setSpanStyle: (styles?: string) => ReturnType; + toggleSpanStyle: (styles?: string) => ReturnType; + unsetSpanStyle: (styles?: string) => ReturnType; }; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/style-menu.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/style-menu.tiptap-toolbar-api.ts index 797c382aec..dda2e85f26 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/style-menu.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/style-menu.tiptap-toolbar-api.ts @@ -15,12 +15,12 @@ export default class UmbTiptapToolbarStyleMenuApi extends UmbTiptapToolbarElemen code: { type: 'code', command: (chain) => chain.toggleCode() }, codeBlock: { type: 'codeBlock', command: (chain) => chain.toggleCodeBlock() }, div: { type: 'div', command: (chain) => chain.toggleNode('div', 'paragraph') }, - em: { type: 'italic', command: (chain) => chain.setItalic() }, + em: { type: 'italic', command: (chain) => chain.toggleItalic() }, ol: { type: 'orderedList', command: (chain) => chain.toggleOrderedList() }, - strong: { type: 'bold', command: (chain) => chain.setBold() }, - s: { type: 'strike', command: (chain) => chain.setStrike() }, + strong: { type: 'bold', command: (chain) => chain.toggleBold() }, + s: { type: 'strike', command: (chain) => chain.toggleStrike() }, span: { type: 'span', command: (chain) => chain.toggleMark('span') }, - u: { type: 'underline', command: (chain) => chain.setUnderline() }, + u: { type: 'underline', command: (chain) => chain.toggleUnderline() }, ul: { type: 'bulletList', command: (chain) => chain.toggleBulletList() }, }; @@ -29,6 +29,6 @@ export default class UmbTiptapToolbarStyleMenuApi extends UmbTiptapToolbarElemen const { tag, id, class: className } = item.data; const focus = editor.chain().focus(); const ext = tag ? this.#commands[tag] : null; - (ext?.command?.(focus) ?? focus).setId(id, ext?.type).setClassName(className, ext?.type).run(); + (ext?.command?.(focus) ?? focus).toggleId(id, ext?.type).toggleClassName(className, ext?.type).run(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 06305ea6bd..1e96b77519 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -611,15 +611,15 @@ const toolbarExtensions: Array = [ name: 'Font Size Tiptap Extension', api: () => import('./toolbar/font-size.tiptap-toolbar-api.js'), items: [ - { label: '8pt', data: '8pt;' }, - { label: '10pt', data: '10pt;' }, - { label: '12pt', data: '12pt;' }, - { label: '14pt', data: '14pt;' }, - { label: '16pt', data: '16pt;' }, - { label: '18pt', data: '18pt;' }, - { label: '24pt', data: '24pt;' }, - { label: '26pt', data: '26pt;' }, - { label: '48pt', data: '48pt;' }, + { label: '8pt', data: '8pt' }, + { label: '10pt', data: '10pt' }, + { label: '12pt', data: '12pt' }, + { label: '14pt', data: '14pt' }, + { label: '16pt', data: '16pt' }, + { label: '18pt', data: '18pt' }, + { label: '24pt', data: '24pt' }, + { label: '26pt', data: '26pt' }, + { label: '48pt', data: '48pt' }, ], meta: { alias: 'umbFontSize', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/clear-formatting.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/clear-formatting.tiptap-toolbar-api.ts index 2541a24615..9da1b318f4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/clear-formatting.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/clear-formatting.tiptap-toolbar-api.ts @@ -3,6 +3,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapToolbarClearFormattingExtensionApi extends UmbTiptapToolbarElementApiBase { override execute(editor?: Editor) { - editor?.chain().focus().unsetAllMarks().run(); + editor?.chain().focus().unsetAllMarks().unsetClassName().unsetStyles().run(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family.tiptap-toolbar-api.ts index 02b6b168a3..41f698d3f9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family.tiptap-toolbar-api.ts @@ -5,6 +5,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapToolbarFontFamilyExtensionApi extends UmbTiptapToolbarElementApiBase { override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) { if (!item?.data) return; - editor?.chain().focus().setSpanStyle(`font-family: ${item.data};`).run(); + editor?.chain().focus().toggleSpanStyle(`font-family: ${item.data};`).run(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size.tiptap-toolbar-api.ts index d62b9c82a1..3b7f4c5333 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size.tiptap-toolbar-api.ts @@ -5,6 +5,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapToolbarFontFamilyExtensionApi extends UmbTiptapToolbarElementApiBase { override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) { if (!item?.data) return; - editor?.chain().focus().setSpanStyle(`font-size: ${item.data};`).run(); + editor?.chain().focus().toggleSpanStyle(`font-size: ${item.data};`).run(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-background.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-background.tiptap-toolbar-api.ts index f29d160c77..75b53fba9a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-background.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-background.tiptap-toolbar-api.ts @@ -3,6 +3,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapToolbarTextColorBackgroundExtensionApi extends UmbTiptapToolbarElementApiBase { override execute(editor?: Editor, selectedColor?: string) { - editor?.chain().focus().setSpanStyle(`background-color: ${selectedColor};`).run(); + editor?.chain().focus().toggleSpanStyle(`background-color: ${selectedColor};`).run(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-foreground.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-foreground.tiptap-toolbar-api.ts index e53f7d054a..41ac8e82f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-foreground.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-foreground.tiptap-toolbar-api.ts @@ -3,6 +3,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapToolbarTextColorForegroundExtensionApi extends UmbTiptapToolbarElementApiBase { override execute(editor?: Editor, selectedColor?: string) { - editor?.chain().focus().setSpanStyle(`color: ${selectedColor};`).run(); + editor?.chain().focus().toggleSpanStyle(`color: ${selectedColor};`).run(); } }