Skip to content

Commit 919b65e

Browse files
authored
Tiptap RTE: Style Menu action toggles (#19520)
* Tiptap style menu toggles (for classes and IDs) Fixes #19244 * Tiptap style menu toggles (for font/color) Fixes #19508 * Tiptap "Clear Formatting" remove classes and styles * Tiptap font sizes, removes trailing semicolon as the API handles the delimiter * Tiptap global attrs: adds set/unset styles commands
1 parent fe7f055 commit 919b65e

9 files changed

+132
-31
lines changed

src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ export const HtmlGlobalAttributes = Extension.create<HtmlGlobalAttributesOptions
6464
.map((type) => commands.updateAttributes(type, { class: className }))
6565
.every((response) => response);
6666
},
67+
toggleClassName:
68+
(className, type) =>
69+
({ commands, editor }) => {
70+
if (!className) return false;
71+
const types = type ? [type] : this.options.types;
72+
const existing = types.map((type) => editor.getAttributes(type)?.class as string).filter((x) => x);
73+
return existing.length ? commands.unsetClassName(type) : commands.setClassName(className, type);
74+
},
6775
unsetClassName:
6876
(type) =>
6977
({ commands }) => {
@@ -77,12 +85,41 @@ export const HtmlGlobalAttributes = Extension.create<HtmlGlobalAttributesOptions
7785
const types = type ? [type] : this.options.types;
7886
return types.map((type) => commands.updateAttributes(type, { id })).every((response) => response);
7987
},
88+
toggleId:
89+
(id, type) =>
90+
({ commands, editor }) => {
91+
if (!id) return false;
92+
const types = type ? [type] : this.options.types;
93+
const existing = types.map((type) => editor.getAttributes(type)?.id as string).filter((x) => x);
94+
return existing.length ? commands.unsetId(type) : commands.setId(id, type);
95+
},
8096
unsetId:
8197
(type) =>
8298
({ commands }) => {
8399
const types = type ? [type] : this.options.types;
84100
return types.map((type) => commands.resetAttributes(type, 'id')).every((response) => response);
85101
},
102+
setStyles:
103+
(style, type) =>
104+
({ commands }) => {
105+
if (!style) return false;
106+
const types = type ? [type] : this.options.types;
107+
return types.map((type) => commands.updateAttributes(type, { style })).every((response) => response);
108+
},
109+
toggleStyles:
110+
(style, type) =>
111+
({ commands, editor }) => {
112+
if (!style) return false;
113+
const types = type ? [type] : this.options.types;
114+
const existing = types.map((type) => editor.getAttributes(type)?.style as string).filter((x) => x);
115+
return existing.length ? commands.unsetStyles(type) : commands.setStyles(style, type);
116+
},
117+
unsetStyles:
118+
(type) =>
119+
({ commands }) => {
120+
const types = type ? [type] : this.options.types;
121+
return types.map((type) => commands.resetAttributes(type, 'style')).every((response) => response);
122+
},
86123
};
87124
},
88125
});
@@ -91,9 +128,14 @@ declare module '@tiptap/core' {
91128
interface Commands<ReturnType> {
92129
htmlGlobalAttributes: {
93130
setClassName: (className?: string, type?: string) => ReturnType;
131+
toggleClassName: (className?: string, type?: string) => ReturnType;
94132
unsetClassName: (type?: string) => ReturnType;
95133
setId: (id?: string, type?: string) => ReturnType;
134+
toggleId: (id?: string, type?: string) => ReturnType;
96135
unsetId: (type?: string) => ReturnType;
136+
setStyles: (style?: string, type?: string) => ReturnType;
137+
toggleStyles: (style?: string, type?: string) => ReturnType;
138+
unsetStyles: (type?: string) => ReturnType;
97139
};
98140
}
99141
}

src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,31 @@ export interface SpanOptions {
99
HTMLAttributes: Record<string, any>;
1010
}
1111

12+
function parseStyles(style: string | undefined): Record<string, string> {
13+
const items: Record<string, string> = {};
14+
15+
(style ?? '')
16+
.split(';')
17+
.map((x) => x.trim())
18+
.filter((x) => x)
19+
.forEach((rule) => {
20+
const [key, value] = rule.split(':');
21+
if (key && value) {
22+
items[key.trim()] = value.trim();
23+
}
24+
});
25+
26+
return items;
27+
}
28+
29+
function serializeStyles(items: Record<string, string>): string {
30+
return (
31+
Object.entries(items)
32+
.map(([key, value]) => `${key}: ${value}`)
33+
.join(';') + ';'
34+
);
35+
}
36+
1237
export const Span = Mark.create<SpanOptions>({
1338
name: 'span',
1439

@@ -32,28 +57,60 @@ export const Span = Mark.create<SpanOptions>({
3257
if (!styles) return false;
3358

3459
const existing = editor.getAttributes(this.name)?.style as string;
35-
3660
if (!existing && !editor.isActive(this.name)) {
3761
return commands.setMark(this.name, { style: styles });
3862
}
3963

40-
const rules = ((existing ?? '') + ';' + styles).split(';');
41-
const items: Record<string, string> = {};
64+
const items = {
65+
...parseStyles(existing),
66+
...parseStyles(styles),
67+
};
4268

43-
rules
69+
const style = serializeStyles(items);
70+
if (style === ';') return false;
71+
72+
return commands.updateAttributes(this.name, { style });
73+
},
74+
toggleSpanStyle:
75+
(styles) =>
76+
({ commands, editor }) => {
77+
if (!styles) return false;
78+
const existing = editor.getAttributes(this.name)?.style as string;
79+
return existing?.includes(styles) === true ? commands.unsetSpanStyle(styles) : commands.setSpanStyle(styles);
80+
},
81+
unsetSpanStyle:
82+
(styles) =>
83+
({ commands, editor }) => {
84+
if (!styles) return false;
85+
86+
parseStyles(styles);
87+
88+
const toBeRemoved = new Set<string>();
89+
90+
styles
91+
.split(';')
92+
.map((x) => x.trim())
4493
.filter((x) => x)
4594
.forEach((rule) => {
46-
if (rule.trim() !== '') {
47-
const [key, value] = rule.split(':');
48-
items[key.trim()] = value.trim();
49-
}
95+
const [key] = rule.split(':');
96+
if (key) toBeRemoved.add(key.trim());
5097
});
5198

52-
const style = Object.entries(items)
53-
.map(([key, value]) => `${key}: ${value}`)
54-
.join(';');
99+
if (toBeRemoved.size === 0) return false;
55100

56-
return commands.updateAttributes(this.name, { style });
101+
const existing = editor.getAttributes(this.name)?.style as string;
102+
const items = parseStyles(existing);
103+
104+
// Remove keys
105+
for (const key of toBeRemoved) {
106+
delete items[key];
107+
}
108+
109+
const style = serializeStyles(items);
110+
111+
return style === ';'
112+
? commands.resetAttributes(this.name, 'style')
113+
: commands.updateAttributes(this.name, { style });
57114
},
58115
};
59116
},
@@ -63,6 +120,8 @@ declare module '@tiptap/core' {
63120
interface Commands<ReturnType> {
64121
span: {
65122
setSpanStyle: (styles?: string) => ReturnType;
123+
toggleSpanStyle: (styles?: string) => ReturnType;
124+
unsetSpanStyle: (styles?: string) => ReturnType;
66125
};
67126
}
68127
}

src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/style-menu.tiptap-toolbar-api.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ export default class UmbTiptapToolbarStyleMenuApi extends UmbTiptapToolbarElemen
1515
code: { type: 'code', command: (chain) => chain.toggleCode() },
1616
codeBlock: { type: 'codeBlock', command: (chain) => chain.toggleCodeBlock() },
1717
div: { type: 'div', command: (chain) => chain.toggleNode('div', 'paragraph') },
18-
em: { type: 'italic', command: (chain) => chain.setItalic() },
18+
em: { type: 'italic', command: (chain) => chain.toggleItalic() },
1919
ol: { type: 'orderedList', command: (chain) => chain.toggleOrderedList() },
20-
strong: { type: 'bold', command: (chain) => chain.setBold() },
21-
s: { type: 'strike', command: (chain) => chain.setStrike() },
20+
strong: { type: 'bold', command: (chain) => chain.toggleBold() },
21+
s: { type: 'strike', command: (chain) => chain.toggleStrike() },
2222
span: { type: 'span', command: (chain) => chain.toggleMark('span') },
23-
u: { type: 'underline', command: (chain) => chain.setUnderline() },
23+
u: { type: 'underline', command: (chain) => chain.toggleUnderline() },
2424
ul: { type: 'bulletList', command: (chain) => chain.toggleBulletList() },
2525
};
2626

@@ -29,6 +29,6 @@ export default class UmbTiptapToolbarStyleMenuApi extends UmbTiptapToolbarElemen
2929
const { tag, id, class: className } = item.data;
3030
const focus = editor.chain().focus();
3131
const ext = tag ? this.#commands[tag] : null;
32-
(ext?.command?.(focus) ?? focus).setId(id, ext?.type).setClassName(className, ext?.type).run();
32+
(ext?.command?.(focus) ?? focus).toggleId(id, ext?.type).toggleClassName(className, ext?.type).run();
3333
}
3434
}

src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -611,15 +611,15 @@ const toolbarExtensions: Array<UmbExtensionManifest> = [
611611
name: 'Font Size Tiptap Extension',
612612
api: () => import('./toolbar/font-size.tiptap-toolbar-api.js'),
613613
items: [
614-
{ label: '8pt', data: '8pt;' },
615-
{ label: '10pt', data: '10pt;' },
616-
{ label: '12pt', data: '12pt;' },
617-
{ label: '14pt', data: '14pt;' },
618-
{ label: '16pt', data: '16pt;' },
619-
{ label: '18pt', data: '18pt;' },
620-
{ label: '24pt', data: '24pt;' },
621-
{ label: '26pt', data: '26pt;' },
622-
{ label: '48pt', data: '48pt;' },
614+
{ label: '8pt', data: '8pt' },
615+
{ label: '10pt', data: '10pt' },
616+
{ label: '12pt', data: '12pt' },
617+
{ label: '14pt', data: '14pt' },
618+
{ label: '16pt', data: '16pt' },
619+
{ label: '18pt', data: '18pt' },
620+
{ label: '24pt', data: '24pt' },
621+
{ label: '26pt', data: '26pt' },
622+
{ label: '48pt', data: '48pt' },
623623
],
624624
meta: {
625625
alias: 'umbFontSize',

src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/clear-formatting.tiptap-toolbar-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
33

44
export default class UmbTiptapToolbarClearFormattingExtensionApi extends UmbTiptapToolbarElementApiBase {
55
override execute(editor?: Editor) {
6-
editor?.chain().focus().unsetAllMarks().run();
6+
editor?.chain().focus().unsetAllMarks().unsetClassName().unsetStyles().run();
77
}
88
}

src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family.tiptap-toolbar-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
55
export default class UmbTiptapToolbarFontFamilyExtensionApi extends UmbTiptapToolbarElementApiBase {
66
override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) {
77
if (!item?.data) return;
8-
editor?.chain().focus().setSpanStyle(`font-family: ${item.data};`).run();
8+
editor?.chain().focus().toggleSpanStyle(`font-family: ${item.data};`).run();
99
}
1010
}

src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size.tiptap-toolbar-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
55
export default class UmbTiptapToolbarFontFamilyExtensionApi extends UmbTiptapToolbarElementApiBase {
66
override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) {
77
if (!item?.data) return;
8-
editor?.chain().focus().setSpanStyle(`font-size: ${item.data};`).run();
8+
editor?.chain().focus().toggleSpanStyle(`font-size: ${item.data};`).run();
99
}
1010
}

src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-background.tiptap-toolbar-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
33

44
export default class UmbTiptapToolbarTextColorBackgroundExtensionApi extends UmbTiptapToolbarElementApiBase {
55
override execute(editor?: Editor, selectedColor?: string) {
6-
editor?.chain().focus().setSpanStyle(`background-color: ${selectedColor};`).run();
6+
editor?.chain().focus().toggleSpanStyle(`background-color: ${selectedColor};`).run();
77
}
88
}

src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-foreground.tiptap-toolbar-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
33

44
export default class UmbTiptapToolbarTextColorForegroundExtensionApi extends UmbTiptapToolbarElementApiBase {
55
override execute(editor?: Editor, selectedColor?: string) {
6-
editor?.chain().focus().setSpanStyle(`color: ${selectedColor};`).run();
6+
editor?.chain().focus().toggleSpanStyle(`color: ${selectedColor};`).run();
77
}
88
}

0 commit comments

Comments
 (0)