Skip to content

Commit 8a94383

Browse files
leekelleherCopilot
andauthored
Tiptap RTE: Refactor Table menus to use menu extension-type (#19789)
* Added `action` kind for `menuItem` extension-type * Adds `<umb-tiptap-menu>` component * Adds support for `menu` extensions to the `<umb-cascading-menu-popover>` component * Adds support for `menu` extensions to the `tiptapToolbarExtension` extension-type * Adds support for `menu` extensions to the `<umb-tiptap-toolbar-menu>` component * Adds manifests for table column/row menus Deprecates the `umb-tiptap-table-column-menu` and `umb-tiptap-table-row-menu` components. * Adds table column menu actions * Adds table row menu actions * Adds table cell menu actions * Adds table (general) menu actions * Replaces table toolbar menu with the new `menu` extensions * Adds `UMB_TIPTAP_RTE_CONTEXT` so that the menu actions can access the Editor instance. * Update src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/actions/table-properties.action.ts Co-authored-by: Copilot <[email protected]> * `UmbTiptapMenuElement` doesn't use the `editor` property --------- Co-authored-by: Copilot <[email protected]>
1 parent 63ed1ee commit 8a94383

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+719
-195
lines changed

src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-bubble-menu.extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import type { PluginView } from '@tiptap/pm/state';
77

88
export interface UmbTiptapBubbleMenuElement extends HTMLElement {
99
editor?: Editor;
10+
menuAlias?: string;
1011
}
1112

1213
export type UmbBubbleMenuPluginProps = {
1314
unique: string;
1415
placement?: UUIPopoverContainerElement['placement'];
1516
elementName?: string | null;
17+
menuAlias?: string;
1618
shouldShow?:
1719
| ((props: { editor: Editor; view: EditorView; state: EditorState; from: number; to: number }) => boolean)
1820
| null;
@@ -42,6 +44,7 @@ export const UmbBubbleMenu = Extension.create<UmbBubbleMenuOptions>({
4244
unique: this.options.unique,
4345
placement: this.options.placement,
4446
elementName: this.options.elementName,
47+
menuAlias: this.options.menuAlias,
4548
shouldShow: this.options.shouldShow,
4649
}),
4750
];
@@ -68,6 +71,7 @@ class UmbBubbleMenuPluginView implements PluginView {
6871
if (props.elementName) {
6972
const menu = document.createElement(props.elementName) as UmbTiptapBubbleMenuElement;
7073
menu.editor = editor;
74+
menu.menuAlias = props.menuAlias;
7175
this.#popover.appendChild(menu);
7276
}
7377

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const UmbTable = Table.configure({ resizable: true, View: UmbTableView })
3838

3939
export const UmbTableRow = TableRow.extend({
4040
allowGapCursor: false,
41+
content: '(tableCell | tableHeader)*',
4142
});
4243

4344
export const UmbTableHeader = TableHeader.extend({
@@ -70,7 +71,8 @@ export const UmbTableHeader = TableHeader.extend({
7071
UmbBubbleMenuPlugin(this.editor, {
7172
unique: 'table-column-menu',
7273
placement: 'top',
73-
elementName: 'umb-tiptap-table-column-menu',
74+
elementName: 'umb-tiptap-menu',
75+
menuAlias: 'Umb.Menu.Tiptap.TableColumn',
7476
shouldShow(props) {
7577
return isColumnGripSelected(props);
7678
},
@@ -162,7 +164,8 @@ export const UmbTableCell = TableCell.extend({
162164
UmbBubbleMenuPlugin(this.editor, {
163165
unique: 'table-row-menu',
164166
placement: 'left',
165-
elementName: 'umb-tiptap-table-row-menu',
167+
elementName: 'umb-tiptap-menu',
168+
menuAlias: 'Umb.Menu.Tiptap.TableRow',
166169
shouldShow(props) {
167170
return isRowGripSelected(props);
168171
},
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { UmbMenuItemActionApi, UmbMenuItemActionApiArgs } from './types.js';
2+
import { UmbActionBase } from '@umbraco-cms/backoffice/action';
3+
4+
export abstract class UmbMenuItemActionApiBase<ArgsMetaType = never>
5+
extends UmbActionBase<UmbMenuItemActionApiArgs<ArgsMetaType>>
6+
implements UmbMenuItemActionApi<ArgsMetaType>
7+
{
8+
public execute(): Promise<void> {
9+
return Promise.resolve();
10+
}
11+
}
12+
13+
export class UmbActionMenuItemApi extends UmbMenuItemActionApiBase {
14+
override async execute() {}
15+
}
16+
17+
export default UmbActionMenuItemApi;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { ManifestMenuItemActionKind, UmbMenuItemActionApi, UmbMenuItemActionElement } from './types.js';
2+
import { customElement, html, ifDefined, property, when } from '@umbraco-cms/backoffice/external/lit';
3+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
4+
import type { UUIMenuItemEvent } from '@umbraco-cms/backoffice/external/uui';
5+
6+
@customElement('umb-action-menu-item')
7+
export default class UmbActionMenuItemElement extends UmbLitElement implements UmbMenuItemActionElement {
8+
@property({ attribute: false })
9+
api?: UmbMenuItemActionApi;
10+
11+
@property({ attribute: false })
12+
manifest?: ManifestMenuItemActionKind;
13+
14+
#onClickLabel(event: UUIMenuItemEvent) {
15+
event.stopPropagation();
16+
17+
try {
18+
this.api?.execute();
19+
} catch (error) {
20+
console.error('Error menu item action:', error);
21+
}
22+
}
23+
24+
// Prevents the regular click event from bubbling up.
25+
// This could be handled in the UUI Menu item component, to prevent dispatching both "click-label" and "click" events at the same time.
26+
#onClick(event: PointerEvent) {
27+
event.stopPropagation();
28+
}
29+
30+
override render() {
31+
const label = this.localize.string(this.manifest?.meta.label ?? this.manifest?.name);
32+
return html`
33+
<uui-menu-item label=${ifDefined(label)} @click-label=${this.#onClickLabel} @click=${this.#onClick}>
34+
${when(this.manifest?.meta.icon, (icon) => html`<umb-icon slot="icon" name=${icon}></umb-icon>`)}
35+
</uui-menu-item>
36+
`;
37+
}
38+
}
39+
40+
declare global {
41+
interface HTMLElementTagNameMap {
42+
'umb-action-menu-item': UmbActionMenuItemElement;
43+
}
44+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
2+
3+
export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [
4+
{
5+
type: 'kind',
6+
alias: 'Umb.Kind.MenuItem.Action',
7+
matchKind: 'action',
8+
matchType: 'menuItem',
9+
manifest: {
10+
type: 'menuItem',
11+
kind: 'action',
12+
api: () => import('./action-menu-item.api.js'),
13+
element: () => import('./action-menu-item.element.js'),
14+
},
15+
},
16+
];
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { MetaMenuItem } from '../../../menu-item.extension.js';
2+
import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
3+
import type { UmbAction } from '@umbraco-cms/backoffice/action';
4+
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
5+
6+
export interface UmbMenuItemActionApiArgs<MetaArgsType> {
7+
meta: MetaArgsType;
8+
}
9+
10+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
11+
export interface UmbMenuItemActionApi<ArgsMetaType = never> extends UmbAction<UmbMenuItemActionApiArgs<ArgsMetaType>> {}
12+
13+
export interface UmbMenuItemActionElement extends UmbControllerHostElement {
14+
manifest?: ManifestMenuItemActionKind;
15+
}
16+
17+
// Unable to extend from `ManifestMenuItem` as `UmbMenuItemElement` is of type `HTMLElement`, but for use with an API,
18+
// it needs to be of type `UmbControllerHostElement`, and modifying `UmbMenuItemElement` would be a breaking-change. [LK]
19+
export interface ManifestMenuItemActionKind
20+
extends ManifestElementAndApi<UmbMenuItemActionElement, UmbMenuItemActionApi<MetaMenuItemActionKind>>,
21+
ManifestWithDynamicConditions<UmbExtensionConditionConfig> {
22+
type: 'menuItem';
23+
kind: 'action';
24+
meta: MetaMenuItemActionKind;
25+
}
26+
27+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
28+
export interface MetaMenuItemActionKind extends Exclude<MetaMenuItem, 'entityType'> {}
29+
30+
declare global {
31+
interface UmbExtensionManifestMap {
32+
umbActionMenuItemKind: ManifestMenuItemActionKind;
33+
}
34+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './menu-item-default.element.js';
2+
export * from './action/action-menu-item.api.js';
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1+
import { manifests as actionManifests } from './action/manifests.js';
12
import { manifests as linkManifests } from './link/manifests.js';
23
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
34

4-
export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [...linkManifests];
5+
export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [...actionManifests, ...linkManifests];
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export type * from './action/types.js';
12
export type * from './link/types.js';

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { css, customElement, html, ifDefined, property, repeat, when } from '@umbraco-cms/backoffice/external/lit';
22
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
33
import { UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui';
4+
import type { ManifestMenu } from '@umbraco-cms/backoffice/menu';
45

56
export type UmbCascadingMenuItem = {
67
label: string;
78
icon?: string;
89
items?: Array<UmbCascadingMenuItem>;
910
element?: HTMLElement;
11+
menu?: string;
1012
separatorAfter?: boolean;
1113
style?: string;
1214
isActive?: () => boolean | undefined;
@@ -27,7 +29,7 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
2729
}
2830

2931
#onMouseEnter(item: UmbCascadingMenuItem, popoverId?: string) {
30-
if (!item.items?.length || !popoverId) return;
32+
if (!(item.items?.length || item.menu) || !popoverId) return;
3133

3234
const popover = this.#getPopoverById(popoverId);
3335
if (!popover) return;
@@ -65,17 +67,16 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
6567
override render() {
6668
return html`
6769
<uui-scroll-container>
68-
${when(
69-
this.items?.length,
70-
() => html`${repeat(this.items!, (item, index) => this.#renderItem(item, index))} ${super.render()}`,
71-
() => super.render(),
72-
)}
70+
${when(this.items?.length, () => repeat(this.items!, (item, index) => this.#renderItem(item, index)))}
71+
${super.render()}
7372
</uui-scroll-container>
7473
`;
7574
}
7675

7776
#renderItem(item: UmbCascadingMenuItem, index: number) {
78-
const popoverId = item.items ? `menu-${index}` : undefined;
77+
const hasChildMenu = item.items?.length || !!item.menu;
78+
79+
const popoverId = hasChildMenu ? `menu-${index}` : undefined;
7980

8081
const element = item.element;
8182
if (element && popoverId) {
@@ -103,7 +104,7 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
103104
${when(item.icon, (icon) => html`<uui-icon slot="icon" name=${icon}></uui-icon>`)}
104105
<div slot="label" class="menu-item">
105106
<span style=${ifDefined(item.style)}>${label}</span>
106-
${when(item.items, () => html`<uui-symbol-expand></uui-symbol-expand>`)}
107+
${when(hasChildMenu, () => html`<uui-symbol-expand></uui-symbol-expand>`)}
107108
</div>
108109
</uui-menu-item>
109110
`,
@@ -112,6 +113,16 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
112113
popoverId,
113114
(popoverId) => html`
114115
<umb-cascading-menu-popover id=${popoverId} placement="right-start" .items=${item.items}>
116+
${when(
117+
item.menu,
118+
(menuAlias) => html`
119+
<umb-extension-slot
120+
type="menu"
121+
default-element="umb-tiptap-menu"
122+
single
123+
.filter=${(menu: ManifestMenu) => menu.alias === menuAlias}></umb-extension-slot>
124+
`,
125+
)}
115126
</umb-cascading-menu-popover>
116127
`,
117128
)}

0 commit comments

Comments
 (0)