Skip to content

Commit f6ab6ee

Browse files
authored
Tiptap RTE: Reusable toolbar menu component (#18483)
* Adds Tiptap Toolbar Menu extension kind a reusable menu component. Removes the `unique` property for menu-items. * Implements Font Family as toolbar menu kind * Implements Font Size as toolbar menu kind * Implements Style Select as toolbar menu kind * Implements Table as toolbar menu kind * Markup amends * Mock data RTE content addition * "TextDirection" manifest correction * Text Align: made to be toggleable
1 parent 9809db4 commit f6ab6ee

21 files changed

+400
-447
lines changed

src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,7 @@ export const data: Array<UmbMockDocumentModel> = [
914914
<p>
915915
<span id="foo">Some</span> value for the RTE with an <a href="https://google.com">external link</a> and an <a type="document" href="/{localLink:c05da24d-7740-447b-9cdc-bd8ce2172e38}">internal link</a>.
916916
</p>
917+
<p><a href="https://gist.github.com/leekelleher/9490718" target="_blank">All HTML tags</a></p>
917918
<div data-foo-bar="123">
918919
<span>This is a plain old span tag.</span>
919920
<span style="color:red;">Hello <span style="color:blue;">world</span>.</span>

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

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import { css, customElement, html, property, repeat, when } from '@umbraco-cms/backoffice/external/lit';
1+
import { css, customElement, html, ifDefined, property, repeat, when } from '@umbraco-cms/backoffice/external/lit';
22
import { UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui';
33

44
export type UmbCascadingMenuItem = {
5-
unique: string;
65
label: string;
76
icon?: string;
87
items?: Array<UmbCascadingMenuItem>;
98
element?: HTMLElement;
109
separatorAfter?: boolean;
10+
style?: string;
1111
execute?: () => void;
12-
isActive?: () => boolean;
1312
};
1413

1514
@customElement('umb-cascading-menu-popover')
@@ -21,10 +20,10 @@ export class UmbCascadingMenuPopoverElement extends UUIPopoverContainerElement {
2120
return this.shadowRoot?.querySelector(`#${popoverId}`) as UUIPopoverContainerElement;
2221
}
2322

24-
#onMouseEnter(item: UmbCascadingMenuItem) {
23+
#onMouseEnter(item: UmbCascadingMenuItem, popoverId: string) {
2524
if (!item.items?.length) return;
2625

27-
const popover = this.#getPopoverById(item.unique);
26+
const popover = this.#getPopoverById(popoverId);
2827
if (!popover) return;
2928

3029
// TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
@@ -33,8 +32,8 @@ export class UmbCascadingMenuPopoverElement extends UUIPopoverContainerElement {
3332
popover.showPopover();
3433
}
3534

36-
#onMouseLeave(item: UmbCascadingMenuItem) {
37-
const popover = this.#getPopoverById(item.unique);
35+
#onMouseLeave(item: UmbCascadingMenuItem, popoverId: string) {
36+
const popover = this.#getPopoverById(popoverId);
3837
if (!popover) return;
3938

4039
// TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
@@ -43,11 +42,11 @@ export class UmbCascadingMenuPopoverElement extends UUIPopoverContainerElement {
4342
popover.hidePopover();
4443
}
4544

46-
#onClick(item: UmbCascadingMenuItem) {
45+
#onClick(item: UmbCascadingMenuItem, popoverId: string) {
4746
item.execute?.();
4847

4948
setTimeout(() => {
50-
this.#onMouseLeave(item);
49+
this.#onMouseLeave(item, popoverId);
5150
}, 100);
5251
}
5352

@@ -56,44 +55,40 @@ export class UmbCascadingMenuPopoverElement extends UUIPopoverContainerElement {
5655
<uui-scroll-container>
5756
${when(
5857
this.items?.length,
59-
() => html`
60-
${repeat(
61-
this.items!,
62-
(item) => item.unique,
63-
(item) => this.#renderItem(item),
64-
)}
65-
${super.render()}
66-
`,
58+
() => html` ${repeat(this.items!, (item, index) => this.#renderItem(item, index))} ${super.render()} `,
6759
() => super.render(),
6860
)}
6961
</uui-scroll-container>
7062
`;
7163
}
7264

73-
#renderItem(item: UmbCascadingMenuItem) {
65+
#renderItem(item: UmbCascadingMenuItem, index: number) {
7466
const element = item.element;
67+
const popoverId = `item-${index}`;
7568
if (element) {
76-
element.setAttribute('popovertarget', item.unique);
69+
element.setAttribute('popovertarget', popoverId);
7770
}
7871
return html`
79-
<div @mouseenter=${() => this.#onMouseEnter(item)} @mouseleave=${() => this.#onMouseLeave(item)}>
72+
<div
73+
@mouseenter=${() => this.#onMouseEnter(item, popoverId)}
74+
@mouseleave=${() => this.#onMouseLeave(item, popoverId)}>
8075
${when(
8176
element,
8277
() => element,
8378
() => html`
8479
<uui-menu-item
8580
class=${item.separatorAfter ? 'separator' : ''}
86-
popovertarget=${item.unique}
87-
@click-label=${() => this.#onClick(item)}>
81+
popovertarget=${popoverId}
82+
@click-label=${() => this.#onClick(item, popoverId)}>
8883
${when(item.icon, (icon) => html`<uui-icon slot="icon" name=${icon}></uui-icon>`)}
8984
<div slot="label" class="menu-item">
90-
<span>${item.label}</span>
85+
<span style=${ifDefined(item.style)}>${item.label}</span>
9186
${when(item.items, () => html`<uui-symbol-expand></uui-symbol-expand>`)}
9287
</div>
9388
</uui-menu-item>
9489
`,
9590
)}
96-
<umb-cascading-menu-popover id=${item.unique} placement="right-start" .items=${item.items}>
91+
<umb-cascading-menu-popover id=${popoverId} placement="right-start" .items=${item.items}>
9792
</umb-cascading-menu-popover>
9893
</div>
9994
`;

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import { UmbTiptapToolbarButtonElement } from './tiptap-toolbar-button.element.js';
2-
import { customElement, html, ifDefined, when } from '@umbraco-cms/backoffice/external/lit';
2+
import { customElement, html, when } from '@umbraco-cms/backoffice/external/lit';
33

44
@customElement('umb-tiptap-toolbar-button-disabled')
55
export class UmbTiptapToolbarButtonDisabledElement extends UmbTiptapToolbarButtonElement {
66
override render() {
7+
const label = this.localize.string(this.manifest?.meta.label);
78
return html`
89
<uui-button
910
compact
1011
look="default"
11-
label=${ifDefined(this.manifest?.meta.label)}
12-
title=${this.manifest?.meta.label ? this.localize.string(this.manifest.meta.label) : ''}
12+
label=${label}
13+
title=${label}
1314
?disabled=${!this.isActive}
14-
@click=${() => (this.api && this.editor ? this.api.execute(this.editor) : null)}>
15+
@click=${() => this.api?.execute(this.editor)}>
1516
${when(
1617
this.manifest?.meta.icon,
1718
() => html`<umb-icon name=${this.manifest!.meta.icon}></umb-icon>`,
18-
() => html`<span>${this.manifest?.meta.label}</span>`,
19+
() => html`<span>${label}</span>`,
1920
)}
2021
</uui-button>
2122
`;

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,19 @@ export class UmbTiptapToolbarButtonElement extends UmbLitElement {
3838
};
3939

4040
override render() {
41+
const label = this.localize.string(this.manifest?.meta.label);
4142
return html`
4243
<uui-button
4344
compact
4445
look=${this.isActive ? 'outline' : 'default'}
4546
label=${ifDefined(this.manifest?.meta.label)}
46-
title=${this.manifest?.meta.label ? this.localize.string(this.manifest.meta.label) : ''}
47+
title=${label}
4748
?disabled=${this.api && this.editor && this.api.isDisabled(this.editor)}
48-
@click=${() => (this.api && this.editor ? this.api.execute(this.editor) : null)}>
49+
@click=${() => this.api?.execute(this.editor)}>
4950
${when(
5051
this.manifest?.meta.icon,
5152
(icon) => html`<umb-icon name=${icon}></umb-icon>`,
52-
() => html`<span>${this.manifest?.meta.label}</span>`,
53+
() => html`<span>${label}</span>`,
5354
)}
5455
</uui-button>
5556
`;
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type {
2+
ManifestTiptapToolbarExtensionMenuKind,
3+
MetaTiptapToolbarMenuItem,
4+
UmbTiptapToolbarElementApi,
5+
} from '../../extensions/index.js';
6+
import type { UmbCascadingMenuItem } from '../../components/cascading-menu-popover/cascading-menu-popover.element.js';
7+
import { css, customElement, html, ifDefined, state, when } from '@umbraco-cms/backoffice/external/lit';
8+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
9+
import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
10+
11+
import '../../components/cascading-menu-popover/cascading-menu-popover.element.js';
12+
13+
@customElement('umb-tiptap-toolbar-menu-element')
14+
export class UmbTiptapToolbarMenuElement extends UmbLitElement {
15+
#menu: Array<UmbCascadingMenuItem> = [];
16+
17+
@state()
18+
protected isActive = false;
19+
20+
public api?: UmbTiptapToolbarElementApi;
21+
22+
public editor?: Editor;
23+
24+
public set manifest(value: ManifestTiptapToolbarExtensionMenuKind | undefined) {
25+
this.#manifest = value;
26+
this.#setMenu();
27+
}
28+
public get manifest(): ManifestTiptapToolbarExtensionMenuKind | undefined {
29+
return this.#manifest;
30+
}
31+
#manifest?: ManifestTiptapToolbarExtensionMenuKind | undefined;
32+
33+
override connectedCallback() {
34+
super.connectedCallback();
35+
36+
if (this.editor) {
37+
this.editor.on('selectionUpdate', this.#onEditorUpdate);
38+
this.editor.on('update', this.#onEditorUpdate);
39+
}
40+
}
41+
42+
override disconnectedCallback() {
43+
super.disconnectedCallback();
44+
45+
if (this.editor) {
46+
this.editor.off('selectionUpdate', this.#onEditorUpdate);
47+
this.editor.off('update', this.#onEditorUpdate);
48+
}
49+
}
50+
51+
async #setMenu() {
52+
if (!this.#manifest?.meta.items) return;
53+
this.#menu = await this.#getMenuItems(this.#manifest.meta.items);
54+
}
55+
56+
async #getMenuItems(items: Array<MetaTiptapToolbarMenuItem>): Promise<Array<UmbCascadingMenuItem>> {
57+
const menu = [];
58+
59+
for (const item of items) {
60+
const menuItem = await this.#getMenuItem(item);
61+
menu.push(menuItem);
62+
}
63+
64+
return menu;
65+
}
66+
67+
async #getMenuItem(item: MetaTiptapToolbarMenuItem): Promise<UmbCascadingMenuItem> {
68+
let element;
69+
70+
// TODO: Commented out as needs review of how async/await is being handled here. [LK]
71+
// if (item.element) {
72+
// const elementConstructor = await loadManifestElement(item.element);
73+
// if (elementConstructor) {
74+
// element = new elementConstructor();
75+
// }
76+
// }
77+
78+
if (!element && item.elementName) {
79+
element = document.createElement(item.elementName);
80+
}
81+
82+
if (element) {
83+
// TODO: Enforce a type for the element, that has an `editor` property. [LK]
84+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
85+
// @ts-ignore
86+
element.editor = this.editor;
87+
}
88+
89+
let items;
90+
if (item.items) {
91+
items = await this.#getMenuItems(item.items);
92+
}
93+
94+
return {
95+
icon: item.icon,
96+
items,
97+
label: item.label,
98+
style: item.style,
99+
separatorAfter: item.separatorAfter,
100+
element,
101+
execute: () => this.api?.execute(this.editor, item),
102+
};
103+
}
104+
105+
readonly #onEditorUpdate = () => {
106+
if (this.api && this.editor && this.manifest) {
107+
this.isActive = this.api.isActive(this.editor);
108+
}
109+
};
110+
111+
override render() {
112+
const label = this.localize.string(this.manifest?.meta.label);
113+
return html`
114+
${when(
115+
this.manifest?.meta.look === 'icon',
116+
() => html`
117+
<uui-button
118+
compact
119+
look=${this.isActive ? 'outline' : 'default'}
120+
label=${ifDefined(label)}
121+
title=${label}
122+
popovertarget="popover-menu">
123+
${when(
124+
this.manifest?.meta.icon,
125+
(icon) => html`<umb-icon name=${icon}></umb-icon>`,
126+
() => html`<span>${this.manifest?.meta.label}</span>`,
127+
)}
128+
<uui-symbol-expand slot="extra" open></uui-symbol-expand>
129+
</uui-button>
130+
`,
131+
() => html`
132+
<uui-button compact look="secondary" label=${ifDefined(label)} popovertarget="popover-menu">
133+
<span>${label}</span>
134+
<uui-symbol-expand slot="extra" open></uui-symbol-expand>
135+
</uui-button>
136+
`,
137+
)}
138+
<umb-cascading-menu-popover id="popover-menu" placement="bottom-start" .items=${this.#menu}>
139+
</umb-cascading-menu-popover>
140+
`;
141+
}
142+
143+
static override readonly styles = [
144+
css`
145+
:host {
146+
--uui-button-font-weight: normal;
147+
--uui-menu-item-flat-structure: 1;
148+
149+
margin-inline-start: var(--uui-size-space-1);
150+
}
151+
152+
uui-button > uui-symbol-expand {
153+
margin-left: var(--uui-size-space-4);
154+
}
155+
`,
156+
];
157+
}
158+
159+
export { UmbTiptapToolbarMenuElement as element };
160+
161+
declare global {
162+
interface HTMLElementTagNameMap {
163+
'umb-tiptap-toolbar-menu-element': UmbTiptapToolbarMenuElement;
164+
}
165+
}

0 commit comments

Comments
 (0)