Skip to content

Commit cdf7b3d

Browse files
authored
Tiptap RTE: Toolbar menu active highlighting (#19532)
* Adds optional parameter to Tiptap toolbar item's `isActive` * Adds `isActive` support to toolbar menus and cascading menus * Adds `isActive` support to the font menus * Adds `isActive` support to the table menu + UI/CSS tweak * Adds `isActive` support to the style menu API + refactored the commands * Improves cascading menu popover closing it previously didn't close the menu when an action was clicked.
1 parent ede906e commit cdf7b3d

File tree

8 files changed

+95
-24
lines changed

8 files changed

+95
-24
lines changed

src/Umbraco.Web.UI.Client/src/packages/tiptap/components/cascading-menu-popover/cascading-menu-popover.element.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type UmbCascadingMenuItem = {
99
element?: HTMLElement;
1010
separatorAfter?: boolean;
1111
style?: string;
12+
isActive?: () => boolean | undefined;
1213
execute?: () => void;
1314
};
1415

@@ -21,8 +22,12 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
2122
return this.shadowRoot?.querySelector(`#${popoverId}`) as UUIPopoverContainerElement;
2223
}
2324

24-
#onMouseEnter(item: UmbCascadingMenuItem, popoverId: string) {
25-
if (!item.items?.length) return;
25+
#isMenuActive(items?: UmbCascadingMenuItem[]): boolean {
26+
return !!items?.some((item) => item.isActive?.() || this.#isMenuActive(item.items));
27+
}
28+
29+
#onMouseEnter(item: UmbCascadingMenuItem, popoverId?: string) {
30+
if (!item.items?.length || !popoverId) return;
2631

2732
const popover = this.#getPopoverById(popoverId);
2833
if (!popover) return;
@@ -33,7 +38,9 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
3338
popover.showPopover();
3439
}
3540

36-
#onMouseLeave(item: UmbCascadingMenuItem, popoverId: string) {
41+
#onMouseLeave(item: UmbCascadingMenuItem, popoverId?: string) {
42+
if (!popoverId) return;
43+
3744
const popover = this.#getPopoverById(popoverId);
3845
if (!popover) return;
3946

@@ -43,12 +50,16 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
4350
popover.hidePopover();
4451
}
4552

46-
#onClick(item: UmbCascadingMenuItem, popoverId: string) {
53+
#onClick(item: UmbCascadingMenuItem, popoverId?: string) {
4754
item.execute?.();
4855

49-
setTimeout(() => {
50-
this.#onMouseLeave(item, popoverId);
51-
}, 100);
56+
if (!popoverId) {
57+
setTimeout(() => {
58+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
59+
// @ts-ignore
60+
this.hidePopover();
61+
}, 100);
62+
}
5263
}
5364

5465
override render() {
@@ -64,14 +75,15 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
6475
}
6576

6677
#renderItem(item: UmbCascadingMenuItem, index: number) {
67-
const popoverId = `item-${index}`;
78+
const popoverId = item.items ? `menu-${index}` : undefined;
6879

6980
const element = item.element;
70-
if (element) {
81+
if (element && popoverId) {
7182
element.setAttribute('popovertarget', popoverId);
7283
}
7384

7485
const label = this.localize.string(item.label);
86+
const isActive = item.isActive?.() || this.#isMenuActive(item.items) || false;
7587

7688
return html`
7789
<div
@@ -84,7 +96,9 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
8496
<uui-menu-item
8597
class=${item.separatorAfter ? 'separator' : ''}
8698
label=${label}
87-
popovertarget=${popoverId}
99+
popovertarget=${ifDefined(popoverId)}
100+
select-mode="highlight"
101+
?selected=${isActive}
88102
@click-label=${() => this.#onClick(item, popoverId)}>
89103
${when(item.icon, (icon) => html`<uui-icon slot="icon" name=${icon}></uui-icon>`)}
90104
<div slot="label" class="menu-item">
@@ -94,8 +108,13 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
94108
</uui-menu-item>
95109
`,
96110
)}
97-
<umb-cascading-menu-popover id=${popoverId} placement="right-start" .items=${item.items}>
98-
</umb-cascading-menu-popover>
111+
${when(
112+
popoverId,
113+
(popoverId) => html`
114+
<umb-cascading-menu-popover id=${popoverId} placement="right-start" .items=${item.items}>
115+
</umb-cascading-menu-popover>
116+
`,
117+
)}
99118
</div>
100119
`;
101120
}

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,28 @@ import { UmbTiptapToolbarElementApiBase } from '../../extensions/base.js';
22
import type { MetaTiptapToolbarStyleMenuItem } from '../../extensions/types.js';
33
import type { ChainedCommands, Editor } from '@umbraco-cms/backoffice/external/tiptap';
44

5+
type UmbTiptapToolbarStyleMenuCommandType = {
6+
type: string;
7+
command: (chain: ChainedCommands) => ChainedCommands;
8+
isActive?: (editor?: Editor) => boolean | undefined;
9+
};
10+
511
export default class UmbTiptapToolbarStyleMenuApi extends UmbTiptapToolbarElementApiBase {
6-
#commands: Record<string, { type: string; command: (chain: ChainedCommands) => ChainedCommands }> = {
7-
h1: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 1 }) },
8-
h2: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 2 }) },
9-
h3: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 3 }) },
10-
h4: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 4 }) },
11-
h5: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 5 }) },
12-
h6: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 6 }) },
12+
#headingCommand(level: 1 | 2 | 3 | 4 | 5 | 6): UmbTiptapToolbarStyleMenuCommandType {
13+
return {
14+
type: 'heading',
15+
command: (chain) => chain.toggleHeading({ level }),
16+
isActive: (editor) => editor?.isActive('heading', { level }),
17+
};
18+
}
19+
20+
#commands: Record<string, UmbTiptapToolbarStyleMenuCommandType> = {
21+
h1: this.#headingCommand(1),
22+
h2: this.#headingCommand(2),
23+
h3: this.#headingCommand(3),
24+
h4: this.#headingCommand(4),
25+
h5: this.#headingCommand(5),
26+
h6: this.#headingCommand(6),
1327
p: { type: 'paragraph', command: (chain) => chain.setParagraph() },
1428
blockquote: { type: 'blockquote', command: (chain) => chain.toggleBlockquote() },
1529
code: { type: 'code', command: (chain) => chain.toggleCode() },
@@ -24,6 +38,20 @@ export default class UmbTiptapToolbarStyleMenuApi extends UmbTiptapToolbarElemen
2438
ul: { type: 'bulletList', command: (chain) => chain.toggleBulletList() },
2539
};
2640

41+
override isActive(editor?: Editor, item?: MetaTiptapToolbarStyleMenuItem) {
42+
if (!editor || !item?.data) return false;
43+
44+
const { tag, id, class: className } = item.data;
45+
const ext = tag ? this.#commands[tag] : null;
46+
const attrs = editor?.getAttributes(ext?.type ?? 'paragraph');
47+
48+
const tagMatch = !tag ? true : ext ? (ext.isActive?.(editor) ?? editor?.isActive(ext.type) ?? false) : false;
49+
const idMatch = !id ? true : attrs.id === id;
50+
const classMatch = !className ? true : attrs.class?.includes(className) === true;
51+
52+
return tagMatch && idMatch && classMatch;
53+
}
54+
2755
override execute(editor?: Editor, item?: MetaTiptapToolbarStyleMenuItem) {
2856
if (!editor || !item?.data) return;
2957
const { tag, id, class: className } = item.data;

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,18 @@ export class UmbTiptapToolbarMenuElement extends UmbLitElement {
9999
style: item.appearance?.style ?? item.style,
100100
separatorAfter: item.separatorAfter,
101101
element,
102+
isActive: () => this.api?.isActive(this.editor, item),
102103
execute: () => this.api?.execute(this.editor, item),
103104
};
104105
}
105106

107+
#isMenuActive(items?: UmbCascadingMenuItem[]): boolean {
108+
return !!items?.some((item) => item.isActive?.() || this.#isMenuActive(item.items));
109+
}
110+
106111
readonly #onEditorUpdate = () => {
107112
if (this.api && this.editor && this.manifest) {
108-
this.isActive = this.api.isActive(this.editor);
113+
this.isActive = this.api.isActive(this.editor) || this.#isMenuActive(this.#menu) || false;
109114
}
110115
};
111116

@@ -117,8 +122,8 @@ export class UmbTiptapToolbarMenuElement extends UmbLitElement {
117122
() => html`
118123
<uui-button
119124
compact
120-
look=${this.isActive ? 'outline' : 'default'}
121125
label=${ifDefined(label)}
126+
look=${this.isActive ? 'outline' : 'default'}
122127
title=${label}
123128
popovertarget="popover-menu">
124129
${when(
@@ -130,7 +135,11 @@ export class UmbTiptapToolbarMenuElement extends UmbLitElement {
130135
</uui-button>
131136
`,
132137
() => html`
133-
<uui-button compact label=${ifDefined(label)} popovertarget="popover-menu">
138+
<uui-button
139+
compact
140+
label=${ifDefined(label)}
141+
look=${this.isActive ? 'outline' : 'default'}
142+
popovertarget="popover-menu">
134143
<span>${label}</span>
135144
<uui-symbol-expand slot="extra" open></uui-symbol-expand>
136145
</uui-button>

src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/components/table-toolbar-menu.element.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class UmbTiptapTableToolbarMenuElement extends UmbTiptapToolbarMenuElemen
3232
`,
3333
)}
3434
${this.renderMenu()}
35-
<uui-popover-container id="popover-insert">
35+
<uui-popover-container id="popover-insert" style="box-shadow: var(--uui-shadow-depth-3);">
3636
<umb-tiptap-table-insert .editor=${this.editor}></umb-tiptap-table-insert>
3737
</uui-popover-container>
3838
`;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export class UmbTiptapToolbarTableExtensionApi extends UmbTiptapToolbarElementAp
2929
tableProperties: (editor) => this.#tableProperties(editor),
3030
};
3131

32+
override isActive(editor?: Editor, item?: unknown) {
33+
if (!item) return super.isActive(editor);
34+
return false;
35+
}
36+
3237
async #tableProperties(editor?: Editor) {
3338
if (!editor || !editor.isActive('table')) return;
3439

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import type { MetaTiptapToolbarMenuItem } from '../types.js';
33
import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
44

55
export default class UmbTiptapToolbarFontFamilyExtensionApi extends UmbTiptapToolbarElementApiBase {
6+
override isActive(editor?: Editor, item?: MetaTiptapToolbarMenuItem) {
7+
const styles = editor?.getAttributes('span')?.style;
8+
return styles?.includes(`font-family: ${item?.data};`) === true;
9+
}
10+
611
override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) {
712
if (!item?.data) return;
813
editor?.chain().focus().toggleSpanStyle(`font-family: ${item.data};`).run();

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import type { MetaTiptapToolbarMenuItem } from '../types.js';
33
import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
44

55
export default class UmbTiptapToolbarFontFamilyExtensionApi extends UmbTiptapToolbarElementApiBase {
6+
override isActive(editor?: Editor, item?: MetaTiptapToolbarMenuItem) {
7+
const styles = editor?.getAttributes('span')?.style;
8+
return styles?.includes(`font-size: ${item?.data};`) === true;
9+
}
10+
611
override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) {
712
if (!item?.data) return;
813
editor?.chain().focus().toggleSpanStyle(`font-size: ${item.data};`).run();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export interface UmbTiptapToolbarElementApi extends UmbApi, UmbTiptapExtensionAr
5454
/**
5555
* Checks if the toolbar element is active.
5656
*/
57-
isActive(editor?: Editor): boolean;
57+
isActive(editor?: Editor, ...args: Array<unknown>): boolean;
5858

5959
/**
6060
* Checks if the toolbar element is disabled.

0 commit comments

Comments
 (0)