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.
This commit is contained in:
@@ -9,6 +9,7 @@ export type UmbCascadingMenuItem = {
|
||||
element?: HTMLElement;
|
||||
separatorAfter?: boolean;
|
||||
style?: string;
|
||||
isActive?: () => boolean | undefined;
|
||||
execute?: () => void;
|
||||
};
|
||||
|
||||
@@ -21,8 +22,12 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
|
||||
return this.shadowRoot?.querySelector(`#${popoverId}`) as UUIPopoverContainerElement;
|
||||
}
|
||||
|
||||
#onMouseEnter(item: UmbCascadingMenuItem, popoverId: string) {
|
||||
if (!item.items?.length) return;
|
||||
#isMenuActive(items?: UmbCascadingMenuItem[]): boolean {
|
||||
return !!items?.some((item) => item.isActive?.() || this.#isMenuActive(item.items));
|
||||
}
|
||||
|
||||
#onMouseEnter(item: UmbCascadingMenuItem, popoverId?: string) {
|
||||
if (!item.items?.length || !popoverId) return;
|
||||
|
||||
const popover = this.#getPopoverById(popoverId);
|
||||
if (!popover) return;
|
||||
@@ -33,7 +38,9 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
|
||||
popover.showPopover();
|
||||
}
|
||||
|
||||
#onMouseLeave(item: UmbCascadingMenuItem, popoverId: string) {
|
||||
#onMouseLeave(item: UmbCascadingMenuItem, popoverId?: string) {
|
||||
if (!popoverId) return;
|
||||
|
||||
const popover = this.#getPopoverById(popoverId);
|
||||
if (!popover) return;
|
||||
|
||||
@@ -43,12 +50,16 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
|
||||
popover.hidePopover();
|
||||
}
|
||||
|
||||
#onClick(item: UmbCascadingMenuItem, popoverId: string) {
|
||||
#onClick(item: UmbCascadingMenuItem, popoverId?: string) {
|
||||
item.execute?.();
|
||||
|
||||
setTimeout(() => {
|
||||
this.#onMouseLeave(item, popoverId);
|
||||
}, 100);
|
||||
if (!popoverId) {
|
||||
setTimeout(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.hidePopover();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -64,14 +75,15 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
|
||||
}
|
||||
|
||||
#renderItem(item: UmbCascadingMenuItem, index: number) {
|
||||
const popoverId = `item-${index}`;
|
||||
const popoverId = item.items ? `menu-${index}` : undefined;
|
||||
|
||||
const element = item.element;
|
||||
if (element) {
|
||||
if (element && popoverId) {
|
||||
element.setAttribute('popovertarget', popoverId);
|
||||
}
|
||||
|
||||
const label = this.localize.string(item.label);
|
||||
const isActive = item.isActive?.() || this.#isMenuActive(item.items) || false;
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -84,7 +96,9 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
|
||||
<uui-menu-item
|
||||
class=${item.separatorAfter ? 'separator' : ''}
|
||||
label=${label}
|
||||
popovertarget=${popoverId}
|
||||
popovertarget=${ifDefined(popoverId)}
|
||||
select-mode="highlight"
|
||||
?selected=${isActive}
|
||||
@click-label=${() => this.#onClick(item, popoverId)}>
|
||||
${when(item.icon, (icon) => html`<uui-icon slot="icon" name=${icon}></uui-icon>`)}
|
||||
<div slot="label" class="menu-item">
|
||||
@@ -94,8 +108,13 @@ export class UmbCascadingMenuPopoverElement extends UmbElementMixin(UUIPopoverCo
|
||||
</uui-menu-item>
|
||||
`,
|
||||
)}
|
||||
<umb-cascading-menu-popover id=${popoverId} placement="right-start" .items=${item.items}>
|
||||
</umb-cascading-menu-popover>
|
||||
${when(
|
||||
popoverId,
|
||||
(popoverId) => html`
|
||||
<umb-cascading-menu-popover id=${popoverId} placement="right-start" .items=${item.items}>
|
||||
</umb-cascading-menu-popover>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,28 @@ import { UmbTiptapToolbarElementApiBase } from '../../extensions/base.js';
|
||||
import type { MetaTiptapToolbarStyleMenuItem } from '../../extensions/types.js';
|
||||
import type { ChainedCommands, Editor } from '@umbraco-cms/backoffice/external/tiptap';
|
||||
|
||||
type UmbTiptapToolbarStyleMenuCommandType = {
|
||||
type: string;
|
||||
command: (chain: ChainedCommands) => ChainedCommands;
|
||||
isActive?: (editor?: Editor) => boolean | undefined;
|
||||
};
|
||||
|
||||
export default class UmbTiptapToolbarStyleMenuApi extends UmbTiptapToolbarElementApiBase {
|
||||
#commands: Record<string, { type: string; command: (chain: ChainedCommands) => ChainedCommands }> = {
|
||||
h1: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 1 }) },
|
||||
h2: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 2 }) },
|
||||
h3: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 3 }) },
|
||||
h4: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 4 }) },
|
||||
h5: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 5 }) },
|
||||
h6: { type: 'heading', command: (chain) => chain.toggleHeading({ level: 6 }) },
|
||||
#headingCommand(level: 1 | 2 | 3 | 4 | 5 | 6): UmbTiptapToolbarStyleMenuCommandType {
|
||||
return {
|
||||
type: 'heading',
|
||||
command: (chain) => chain.toggleHeading({ level }),
|
||||
isActive: (editor) => editor?.isActive('heading', { level }),
|
||||
};
|
||||
}
|
||||
|
||||
#commands: Record<string, UmbTiptapToolbarStyleMenuCommandType> = {
|
||||
h1: this.#headingCommand(1),
|
||||
h2: this.#headingCommand(2),
|
||||
h3: this.#headingCommand(3),
|
||||
h4: this.#headingCommand(4),
|
||||
h5: this.#headingCommand(5),
|
||||
h6: this.#headingCommand(6),
|
||||
p: { type: 'paragraph', command: (chain) => chain.setParagraph() },
|
||||
blockquote: { type: 'blockquote', command: (chain) => chain.toggleBlockquote() },
|
||||
code: { type: 'code', command: (chain) => chain.toggleCode() },
|
||||
@@ -24,6 +38,20 @@ export default class UmbTiptapToolbarStyleMenuApi extends UmbTiptapToolbarElemen
|
||||
ul: { type: 'bulletList', command: (chain) => chain.toggleBulletList() },
|
||||
};
|
||||
|
||||
override isActive(editor?: Editor, item?: MetaTiptapToolbarStyleMenuItem) {
|
||||
if (!editor || !item?.data) return false;
|
||||
|
||||
const { tag, id, class: className } = item.data;
|
||||
const ext = tag ? this.#commands[tag] : null;
|
||||
const attrs = editor?.getAttributes(ext?.type ?? 'paragraph');
|
||||
|
||||
const tagMatch = !tag ? true : ext ? (ext.isActive?.(editor) ?? editor?.isActive(ext.type) ?? false) : false;
|
||||
const idMatch = !id ? true : attrs.id === id;
|
||||
const classMatch = !className ? true : attrs.class?.includes(className) === true;
|
||||
|
||||
return tagMatch && idMatch && classMatch;
|
||||
}
|
||||
|
||||
override execute(editor?: Editor, item?: MetaTiptapToolbarStyleMenuItem) {
|
||||
if (!editor || !item?.data) return;
|
||||
const { tag, id, class: className } = item.data;
|
||||
|
||||
@@ -99,13 +99,18 @@ export class UmbTiptapToolbarMenuElement extends UmbLitElement {
|
||||
style: item.appearance?.style ?? item.style,
|
||||
separatorAfter: item.separatorAfter,
|
||||
element,
|
||||
isActive: () => this.api?.isActive(this.editor, item),
|
||||
execute: () => this.api?.execute(this.editor, item),
|
||||
};
|
||||
}
|
||||
|
||||
#isMenuActive(items?: UmbCascadingMenuItem[]): boolean {
|
||||
return !!items?.some((item) => item.isActive?.() || this.#isMenuActive(item.items));
|
||||
}
|
||||
|
||||
readonly #onEditorUpdate = () => {
|
||||
if (this.api && this.editor && this.manifest) {
|
||||
this.isActive = this.api.isActive(this.editor);
|
||||
this.isActive = this.api.isActive(this.editor) || this.#isMenuActive(this.#menu) || false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,8 +122,8 @@ export class UmbTiptapToolbarMenuElement extends UmbLitElement {
|
||||
() => html`
|
||||
<uui-button
|
||||
compact
|
||||
look=${this.isActive ? 'outline' : 'default'}
|
||||
label=${ifDefined(label)}
|
||||
look=${this.isActive ? 'outline' : 'default'}
|
||||
title=${label}
|
||||
popovertarget="popover-menu">
|
||||
${when(
|
||||
@@ -130,7 +135,11 @@ export class UmbTiptapToolbarMenuElement extends UmbLitElement {
|
||||
</uui-button>
|
||||
`,
|
||||
() => html`
|
||||
<uui-button compact label=${ifDefined(label)} popovertarget="popover-menu">
|
||||
<uui-button
|
||||
compact
|
||||
label=${ifDefined(label)}
|
||||
look=${this.isActive ? 'outline' : 'default'}
|
||||
popovertarget="popover-menu">
|
||||
<span>${label}</span>
|
||||
<uui-symbol-expand slot="extra" open></uui-symbol-expand>
|
||||
</uui-button>
|
||||
|
||||
@@ -32,7 +32,7 @@ export class UmbTiptapTableToolbarMenuElement extends UmbTiptapToolbarMenuElemen
|
||||
`,
|
||||
)}
|
||||
${this.renderMenu()}
|
||||
<uui-popover-container id="popover-insert">
|
||||
<uui-popover-container id="popover-insert" style="box-shadow: var(--uui-shadow-depth-3);">
|
||||
<umb-tiptap-table-insert .editor=${this.editor}></umb-tiptap-table-insert>
|
||||
</uui-popover-container>
|
||||
`;
|
||||
|
||||
@@ -29,6 +29,11 @@ export class UmbTiptapToolbarTableExtensionApi extends UmbTiptapToolbarElementAp
|
||||
tableProperties: (editor) => this.#tableProperties(editor),
|
||||
};
|
||||
|
||||
override isActive(editor?: Editor, item?: unknown) {
|
||||
if (!item) return super.isActive(editor);
|
||||
return false;
|
||||
}
|
||||
|
||||
async #tableProperties(editor?: Editor) {
|
||||
if (!editor || !editor.isActive('table')) return;
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ import type { MetaTiptapToolbarMenuItem } from '../types.js';
|
||||
import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
|
||||
|
||||
export default class UmbTiptapToolbarFontFamilyExtensionApi extends UmbTiptapToolbarElementApiBase {
|
||||
override isActive(editor?: Editor, item?: MetaTiptapToolbarMenuItem) {
|
||||
const styles = editor?.getAttributes('span')?.style;
|
||||
return styles?.includes(`font-family: ${item?.data};`) === true;
|
||||
}
|
||||
|
||||
override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) {
|
||||
if (!item?.data) return;
|
||||
editor?.chain().focus().toggleSpanStyle(`font-family: ${item.data};`).run();
|
||||
|
||||
@@ -3,6 +3,11 @@ import type { MetaTiptapToolbarMenuItem } from '../types.js';
|
||||
import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
|
||||
|
||||
export default class UmbTiptapToolbarFontFamilyExtensionApi extends UmbTiptapToolbarElementApiBase {
|
||||
override isActive(editor?: Editor, item?: MetaTiptapToolbarMenuItem) {
|
||||
const styles = editor?.getAttributes('span')?.style;
|
||||
return styles?.includes(`font-size: ${item?.data};`) === true;
|
||||
}
|
||||
|
||||
override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) {
|
||||
if (!item?.data) return;
|
||||
editor?.chain().focus().toggleSpanStyle(`font-size: ${item.data};`).run();
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface UmbTiptapToolbarElementApi extends UmbApi, UmbTiptapExtensionAr
|
||||
/**
|
||||
* Checks if the toolbar element is active.
|
||||
*/
|
||||
isActive(editor?: Editor): boolean;
|
||||
isActive(editor?: Editor, ...args: Array<unknown>): boolean;
|
||||
|
||||
/**
|
||||
* Checks if the toolbar element is disabled.
|
||||
|
||||
Reference in New Issue
Block a user