diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/cascading-menu-popover/cascading-menu-popover.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/cascading-menu-popover/cascading-menu-popover.element.ts new file mode 100644 index 0000000000..3f953367b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/cascading-menu-popover/cascading-menu-popover.element.ts @@ -0,0 +1,140 @@ +import { css, customElement, html, property, repeat, when } from '@umbraco-cms/backoffice/external/lit'; +import { UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui'; + +export type UmbCascadingMenuItem = { + unique: string; + label: string; + icon?: string; + items?: Array; + element?: HTMLElement; + separatorAfter?: boolean; + execute?: () => void; + isActive?: () => boolean; +}; + +@customElement('umb-cascading-menu-popover') +export class UmbCascadingMenuPopoverElement extends UUIPopoverContainerElement { + @property({ type: Array }) + items?: Array; + + #getPopoverById(popoverId: string): UUIPopoverContainerElement | null | undefined { + return this.shadowRoot?.querySelector(`#${popoverId}`) as UUIPopoverContainerElement; + } + + #onMouseEnter(popoverId: string) { + const popover = this.#getPopoverById(popoverId); + if (!popover) return; + + // TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + popover.showPopover(); + } + + #onMouseLeave(popoverId: string) { + const popover = this.#getPopoverById(popoverId); + if (!popover) return; + + // TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + popover.hidePopover(); + } + + #onClick(item: UmbCascadingMenuItem) { + item.execute?.(); + + setTimeout(() => { + this.#onMouseLeave(item.unique); + }, 100); + } + + override render() { + return html` + + ${when( + this.items?.length, + () => html` + ${repeat( + this.items!, + (item) => item.unique, + (item) => this.#renderItem(item), + )} + ${super.render()} + `, + () => super.render(), + )} + + `; + } + + #renderItem(item: UmbCascadingMenuItem) { + const element = item.element; + if (element) { + element.setAttribute('popovertarget', item.unique); + } + return html` +
this.#onMouseEnter(item.unique)} @mouseleave=${() => this.#onMouseLeave(item.unique)}> + ${when( + element, + () => element, + () => html` + this.#onClick(item)} + class=${item.separatorAfter ? 'separator' : ''}> + ${when(item.icon, (icon) => html``)} + + + `, + )} + + +
+ `; + } + + static override readonly styles = [ + ...UUIPopoverContainerElement.styles, + css` + :host { + --uui-menu-item-flat-structure: 1; + + background: var(--uui-color-surface); + border-radius: var(--uui-border-radius); + box-shadow: var(--uui-shadow-depth-3); + padding: var(--uui-size-space-1); + } + + .menu-item { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--uui-size-space-4); + } + + .separator::after { + content: ''; + position: absolute; + border-bottom: 1px solid var(--uui-color-border); + width: 100%; + } + + uui-scroll-container { + max-height: 500px; + } + `, + ]; +} + +export default UmbCascadingMenuPopoverElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-cascading-menu-popover': UmbCascadingMenuPopoverElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts index f03903fb07..edb0192afd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts @@ -1,2 +1,2 @@ export * from './input-tiptap/index.js'; -export * from './toolbar/tiptap-toolbar-dropdown-base.element.js'; +export * from './cascading-menu-popover/cascading-menu-popover.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts index 21b0cecd02..dab7f27fd2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts @@ -6,7 +6,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; -import '../toolbar/tiptap-toolbar-dropdown-base.element.js'; +import '../cascading-menu-popover/cascading-menu-popover.element.js'; @customElement('umb-tiptap-toolbar') export class UmbTiptapToolbarElement extends UmbLitElement { @@ -53,6 +53,9 @@ export class UmbTiptapToolbarElement extends UmbLitElement { (extensionControllers) => { this._lookup = new Map(extensionControllers.map((ext) => [ext.alias, ext.component])); }, + undefined, + undefined, + () => import('../toolbar/default-tiptap-toolbar-element.api.js'), ); this.#extensionsController.apiProperties = { configuration: this.configuration }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/default-tiptap-toolbar-element.api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/default-tiptap-toolbar-element.api.ts new file mode 100644 index 0000000000..9d845ccb92 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/default-tiptap-toolbar-element.api.ts @@ -0,0 +1,5 @@ +import { UmbTiptapToolbarElementApiBase } from '../../extensions/base.js'; + +export default class UmbTiptapToolbarDefaultExtensionApi extends UmbTiptapToolbarElementApiBase { + public override execute() {} +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts index 971a1828c4..7a567400b6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts @@ -48,7 +48,7 @@ export class UmbTiptapToolbarButtonElement extends UmbLitElement { @click=${() => (this.api && this.editor ? this.api.execute(this.editor) : null)}> ${when( this.manifest?.meta.icon, - () => html``, + (icon) => html``, () => html`${this.manifest?.meta.label}`, )} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-dropdown-base.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-dropdown-base.element.ts deleted file mode 100644 index a5308a8539..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-dropdown-base.element.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { css, html, nothing, repeat, type TemplateResult } from '@umbraco-cms/backoffice/external/lit'; -import type { PopoverContainerPlacement, UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; - -export type TiptapDropdownItem = { - alias: string; - label: string; - nested?: TiptapDropdownItem[]; - execute?: () => void; - isActive?: () => boolean; -}; - -export abstract class UmbTiptapToolbarDropdownBaseElement extends UmbLitElement { - protected abstract get items(): TiptapDropdownItem[]; - protected abstract get label(): string; - - readonly #onMouseEnter = (popoverId: string) => { - const popover = this.shadowRoot?.querySelector(`#${this.makeAlias(popoverId)}`) as UUIPopoverContainerElement; - if (!popover) return; - popover.showPopover(); - }; - - readonly #onMouseLeave = (popoverId: string) => { - popoverId = popoverId.replace(/\s/g, '-').toLowerCase(); - const popover = this.shadowRoot?.querySelector(`#${this.makeAlias(popoverId)}`) as UUIPopoverContainerElement; - if (!popover) return; - popover.hidePopover(); - }; - - protected makeAlias(label: string) { - return label.replace(/\s/g, '-').toLowerCase(); - } - - protected renderItem(item: TiptapDropdownItem): TemplateResult { - return html` - - `; - } - - protected renderItems( - label: string, - items: Array, - placement: PopoverContainerPlacement = 'right-start', - ): TemplateResult { - return html` -
- ${repeat( - items, - (item) => item.alias, - (item) => html`${this.renderItem(item)}`, - )} -
-
`; - } - protected override render() { - return html` - - ${this.renderItems(this.label, this.items, 'bottom-start')} - `; - } - - static override readonly styles = [ - UmbTextStyles, - css` - button { - border: unset; - background-color: unset; - font: unset; - text-align: unset; - } - - uui-symbol-expand { - position: absolute; - right: 5px; - top: 5px; - } - - .label { - border-radius: var(--uui-border-radius); - width: 100%; - box-sizing: border-box; - align-content: center; - padding: var(--uui-size-space-1) var(--uui-size-space-3); - padding-right: 21px; - align-items: center; - cursor: pointer; - color: var(--uui-color-text); - position: relative; - } - - .label:hover { - background: var(--uui-color-surface-alt); - color: var(--uui-color-interactive-emphasis); - } - - .selected-value { - background: var(--uui-color-surface-alt); - } - - .popover-content { - background: var(--uui-color-surface); - border-radius: var(--uui-border-radius); - box-shadow: var(--uui-shadow-depth-3); - padding: var(--uui-size-space-1); - } - `, - ]; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 786b3fdb54..8615810705 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -1,4 +1,5 @@ import { manifests as blockExtensions } from './block/manifests.js'; +import { manifests as styleSelectExtensions } from './style-select/manifests.js'; import type { ManifestTiptapExtension } from './tiptap.extension.js'; import type { ManifestTiptapToolbarExtension } from './tiptap-toolbar.extension.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; @@ -487,6 +488,6 @@ const toolbarExtensions: Array = [ }, ]; -const extensions = [...coreExtensions, ...toolbarExtensions, ...blockExtensions]; +const extensions = [...coreExtensions, ...toolbarExtensions, ...blockExtensions, ...styleSelectExtensions]; export const manifests = [...kinds, ...extensions]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts new file mode 100644 index 0000000000..8e2bbb6fa5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts @@ -0,0 +1,15 @@ +import type { ManifestTiptapToolbarExtension } from '../types.js'; + +export const manifests: Array = [ + { + type: 'tiptapToolbarExtension', + alias: 'Umb.Tiptap.Toolbar.StyleSelect', + name: 'Style Select Tiptap Extension', + element: () => import('./style-select-tiptap-toolbar.element.js'), + meta: { + alias: 'umbStyleSelect', + icon: 'icon-palette', + label: 'Style Select', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts new file mode 100644 index 0000000000..32d5479d67 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts @@ -0,0 +1,93 @@ +import type { ManifestTiptapToolbarExtension } from '../tiptap-toolbar.extension.js'; +import type { UmbCascadingMenuItem } from '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; +import { css, customElement, html, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +import '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; + +@customElement('umb-tiptap-style-select-toolbar-element') +export class UmbTiptapToolbarStyleSelectToolbarElement extends UmbLitElement { + #menu: Array = [ + { + unique: 'headers', + label: 'Headers', + items: [ + { + unique: 'h2', + label: 'Page heading', + execute: () => this.editor?.chain().focus().toggleHeading({ level: 2 }).run(), + }, + { + unique: 'h3', + label: 'Section heading', + execute: () => this.editor?.chain().focus().toggleHeading({ level: 3 }).run(), + }, + { + unique: 'h4', + label: 'Paragraph heading', + execute: () => this.editor?.chain().focus().toggleHeading({ level: 4 }).run(), + }, + ], + }, + { + unique: 'blocks', + label: 'Blocks', + items: [ + { + unique: 'p', + label: 'Paragraph', + execute: () => this.editor?.chain().focus().setParagraph().run(), + }, + ], + }, + { + unique: 'containers', + label: 'Containers', + items: [ + { unique: 'blockquote', label: 'Quote', execute: () => this.editor?.chain().focus().toggleBlockquote().run() }, + { unique: 'code', label: 'Code', execute: () => this.editor?.chain().focus().toggleCodeBlock().run() }, + ], + }, + ]; + + public editor?: Editor; + + public manifest?: ManifestTiptapToolbarExtension; + + override render() { + return html` + + Style select + + + + + `; + } + + static override readonly styles = [ + css` + :host { + --uui-button-font-weight: normal; + } + + uui-button > uui-symbol-expand { + margin-left: var(--uui-size-space-4); + } + `, + ]; +} + +export { UmbTiptapToolbarStyleSelectToolbarElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-style-select-toolbar-element': UmbTiptapToolbarStyleSelectToolbarElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/style-select.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/style-select.tiptap-toolbar-api.ts deleted file mode 100644 index 96868aa349..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/style-select.tiptap-toolbar-api.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { UmbTiptapToolbarDropdownBaseElement, type TiptapDropdownItem } from '../../components/index.js'; -import { customElement, state } from '@umbraco-cms/backoffice/external/lit'; - -const elementName = 'umb-tiptap-style-select-toolbar-element'; - -@customElement(elementName) -export class UmbTiptapToolbarStyleSelectToolbarElement extends UmbTiptapToolbarDropdownBaseElement { - protected override label = 'Style select'; - - @state() - protected override get items(): TiptapDropdownItem[] { - throw new Error('Method not implemented.'); - } - - static override readonly styles = UmbTiptapToolbarDropdownBaseElement.styles; -} - -export { UmbTiptapToolbarStyleSelectToolbarElement as element }; - -declare global { - interface HTMLElementTagNameMap { - [elementName]: UmbTiptapToolbarStyleSelectToolbarElement; - } -}