Tiptap RTE: Allow removal of unregistered extensions (#20571)
* Tiptap toolbar config: enable removal of unregistered extensions * Tiptap statusbar config: enable removal of unregistered extensions * Tiptap toolbar config: Typescript tidy-up * Tiptap toolbar sorting amend Removed the need for the `tiptap-toolbar-alias` attribute, we can reuse the `data-mark`. * Tiptap extension config UI amend If the extension doesn't have a `description`, then add the `alias` to the title/tooltip, to give a DX hint. * Tiptap toolbar: adds `title` to placeholder skeleton * Added missing `forExtensions` for Style Select and Horizontal Rule toolbar extensions * Update src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/property-editor-ui-tiptap-statusbar-configuration.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -92,11 +92,11 @@ export class UmbTiptapToolbarElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#renderActions(aliases: Array<string>) {
|
||||
return repeat(aliases, (alias) => this.#lookup?.get(alias) ?? this.#renderActionPlaceholder());
|
||||
return repeat(aliases, (alias) => this.#lookup?.get(alias) ?? this.#renderActionPlaceholder(alias));
|
||||
}
|
||||
|
||||
#renderActionPlaceholder() {
|
||||
return html`<span class="skeleton" role="none"></span>`;
|
||||
#renderActionPlaceholder(alias: string) {
|
||||
return html`<span class="skeleton" role="none" title="Loading '${alias}'"></span>`;
|
||||
}
|
||||
|
||||
static override readonly styles = css`
|
||||
|
||||
@@ -16,6 +16,7 @@ export const manifests: Array<UmbExtensionManifest> = [
|
||||
alias: 'Umb.Tiptap.Toolbar.HorizontalRule',
|
||||
name: 'Horizontal Rule Tiptap Toolbar Extension',
|
||||
api: () => import('./horizontal-rule.tiptap-toolbar-api.js'),
|
||||
forExtensions: ['Umb.Tiptap.HorizontalRule'],
|
||||
meta: {
|
||||
alias: 'horizontalRule',
|
||||
icon: 'icon-horizontal-rule',
|
||||
|
||||
@@ -4,6 +4,7 @@ export const manifests: Array<UmbExtensionManifest> = [
|
||||
kind: 'styleMenu',
|
||||
alias: 'Umb.Tiptap.Toolbar.StyleSelect',
|
||||
name: 'Style Select Tiptap Extension',
|
||||
forExtensions: ['Umb.Tiptap.Heading', 'Umb.Tiptap.Blockquote', 'Umb.Tiptap.CodeBlock'],
|
||||
items: [
|
||||
{
|
||||
label: 'Headers',
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
ifDefined,
|
||||
nothing,
|
||||
property,
|
||||
state,
|
||||
repeat,
|
||||
when,
|
||||
} from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, customElement, html, nothing, property, state, repeat, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
|
||||
@@ -166,7 +156,7 @@ export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement
|
||||
${repeat(
|
||||
group.extensions,
|
||||
(item) => html`
|
||||
<li title=${ifDefined(item.description)}>
|
||||
<li title=${item.description ?? item.alias}>
|
||||
<uui-checkbox
|
||||
label=${this.localize.string(item.label)}
|
||||
value=${item.alias}
|
||||
|
||||
@@ -161,7 +161,7 @@ export class UmbPropertyEditorUiTiptapStatusbarConfigurationElement
|
||||
<uui-button
|
||||
compact
|
||||
class=${forbidden ? 'forbidden' : ''}
|
||||
data-mark="tiptap-toolbar-item:${item.alias}"
|
||||
data-mark="tiptap-statusbar-item:${item.alias}"
|
||||
draggable="true"
|
||||
label=${label}
|
||||
look=${forbidden ? 'placeholder' : 'outline'}
|
||||
@@ -212,30 +212,44 @@ export class UmbPropertyEditorUiTiptapStatusbarConfigurationElement
|
||||
|
||||
#renderItem(alias: string, areaIndex = 0, itemIndex = 0) {
|
||||
const item = this.#context?.getExtensionByAlias(alias);
|
||||
if (!item) return nothing;
|
||||
|
||||
const forbidden = !this.#context?.isExtensionEnabled(item.alias);
|
||||
const label = this.localize.string(item.label);
|
||||
const label = this.localize.string(item.label) || item.alias;
|
||||
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
class=${forbidden ? 'forbidden' : ''}
|
||||
data-mark="tiptap-toolbar-item:${item.alias}"
|
||||
draggable="true"
|
||||
label=${label}
|
||||
look=${forbidden ? 'placeholder' : 'outline'}
|
||||
title=${label}
|
||||
?disabled=${forbidden}
|
||||
@click=${() => this.#context.removeStatusbarItem([areaIndex, itemIndex])}
|
||||
@dragend=${this.#onDragEnd}
|
||||
@dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [areaIndex, itemIndex])}>
|
||||
<div class="inner">
|
||||
${when(item.icon, (icon) => html`<umb-icon .name=${icon}></umb-icon>`)}
|
||||
<span>${label}</span>
|
||||
</div>
|
||||
</uui-button>
|
||||
`;
|
||||
switch (item.kind) {
|
||||
case 'unknown':
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
class="missing"
|
||||
data-mark="tiptap-statusbar-item:${item.alias}"
|
||||
color="danger"
|
||||
look="placeholder"
|
||||
label="Missing extension"
|
||||
title="Missing extension: ${item.alias}"
|
||||
@click=${() => this.#context.removeStatusbarItem([areaIndex, itemIndex])}></uui-button>
|
||||
`;
|
||||
|
||||
default:
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
class=${forbidden ? 'forbidden' : ''}
|
||||
data-mark="tiptap-statusbar-item:${item.alias}"
|
||||
draggable="true"
|
||||
label=${label}
|
||||
look=${forbidden ? 'placeholder' : 'outline'}
|
||||
title=${label}
|
||||
@click=${() => this.#context.removeStatusbarItem([areaIndex, itemIndex])}
|
||||
@dragend=${this.#onDragEnd}
|
||||
@dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [areaIndex, itemIndex])}>
|
||||
<div class="inner">
|
||||
${when(item.icon, (icon) => html`<umb-icon .name=${icon}></umb-icon>`)}
|
||||
<span>${label}</span>
|
||||
</div>
|
||||
</uui-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static override readonly styles = [
|
||||
@@ -303,8 +317,8 @@ export class UmbPropertyEditorUiTiptapStatusbarConfigurationElement
|
||||
--color-standalone: var(--uui-color-danger-standalone);
|
||||
--color-emphasis: var(--uui-color-danger-emphasis);
|
||||
--color-contrast: var(--uui-color-danger);
|
||||
--uui-button-contrast-disabled: var(--uui-color-danger);
|
||||
--uui-button-border-color-disabled: var(--uui-color-danger);
|
||||
--uui-button-contrast: var(--uui-color-danger);
|
||||
--uui-button-border-color: var(--uui-color-danger);
|
||||
}
|
||||
|
||||
div {
|
||||
|
||||
@@ -31,6 +31,7 @@ export class UmbTiptapStatusbarConfigurationContext extends UmbContextBase {
|
||||
const _extensions = extensions
|
||||
.sort((a, b) => a.alias.localeCompare(b.alias))
|
||||
.map((ext) => ({
|
||||
kind: 'default',
|
||||
alias: ext.alias,
|
||||
label: ext.meta.label,
|
||||
icon: ext.meta.icon,
|
||||
@@ -75,8 +76,8 @@ export class UmbTiptapStatusbarConfigurationContext extends UmbContextBase {
|
||||
.filter((ext) => ext.alias?.toLowerCase().includes(query) || ext.label?.toLowerCase().includes(query));
|
||||
}
|
||||
|
||||
public getExtensionByAlias(alias: string): UmbTiptapStatusbarExtension | undefined {
|
||||
return this.#lookup?.get(alias);
|
||||
public getExtensionByAlias(alias: string): UmbTiptapStatusbarExtension {
|
||||
return this.#lookup?.get(alias) ?? { label: '', alias, icon: '', kind: 'unknown' };
|
||||
}
|
||||
|
||||
public isExtensionEnabled(alias: string): boolean {
|
||||
|
||||
@@ -255,9 +255,7 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
|
||||
#renderGroup(group?: UmbTiptapToolbarGroupViewModel, rowIndex = 0, groupIndex = 0) {
|
||||
if (!group) return nothing;
|
||||
const showActionBar = this._toolbar[rowIndex].data.length > 1 && group.data.length === 0;
|
||||
const items: UmbTiptapToolbarExtension[] = group!.data
|
||||
.map((alias) => this.#context?.getExtensionByAlias(alias))
|
||||
.filter((item): item is UmbTiptapToolbarExtension => !!item);
|
||||
const items = group.data.map((alias) => this.#context?.getExtensionByAlias(alias));
|
||||
return html`
|
||||
<div
|
||||
class="group"
|
||||
|
||||
@@ -80,8 +80,8 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase {
|
||||
.filter((ext) => ext.alias?.toLowerCase().includes(query) || ext.label?.toLowerCase().includes(query));
|
||||
}
|
||||
|
||||
public getExtensionByAlias(alias: string): UmbTiptapToolbarExtension | undefined {
|
||||
return this.#lookup?.get(alias);
|
||||
public getExtensionByAlias(alias: string): UmbTiptapToolbarExtension {
|
||||
return this.#lookup?.get(alias) ?? { label: '', alias, icon: '', kind: 'unknown' };
|
||||
}
|
||||
|
||||
public isExtensionEnabled(alias: string): boolean {
|
||||
|
||||
@@ -10,9 +10,9 @@ export class UmbTiptapToolbarGroupConfigurationElement<
|
||||
TiptapToolbarItem extends UmbTiptapToolbarExtension = UmbTiptapToolbarExtension,
|
||||
> extends UmbLitElement {
|
||||
#sorter = new UmbSorterController<TiptapToolbarItem, HTMLElement>(this, {
|
||||
getUniqueOfElement: (element) => element.getAttribute('tiptap-toolbar-alias'),
|
||||
getUniqueOfModel: (modelEntry) => modelEntry.alias!,
|
||||
itemSelector: 'uui-button',
|
||||
getUniqueOfElement: (element) => element.dataset.mark,
|
||||
getUniqueOfModel: (modelEntry) => `tiptap-toolbar-item:${modelEntry.alias}`,
|
||||
itemSelector: '.draggable',
|
||||
identifier: 'umb-tiptap-toolbar-sorter',
|
||||
containerSelector: '.items',
|
||||
resolvePlacement: UmbSorterResolvePlacementAsGrid,
|
||||
@@ -71,7 +71,7 @@ export class UmbTiptapToolbarGroupConfigurationElement<
|
||||
}
|
||||
|
||||
#renderItem(item: TiptapToolbarItem, index = 0) {
|
||||
const label = this.localize.string(item.label);
|
||||
const label = this.localize.string(item.label) || item.alias;
|
||||
const forbidden = !this.#context?.isExtensionEnabled(item.alias);
|
||||
|
||||
switch (item.kind) {
|
||||
@@ -80,13 +80,11 @@ export class UmbTiptapToolbarGroupConfigurationElement<
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
class=${forbidden ? 'forbidden' : ''}
|
||||
class="draggable ${forbidden ? 'forbidden' : ''}"
|
||||
data-mark="tiptap-toolbar-item:${item.alias}"
|
||||
look=${forbidden ? 'placeholder' : 'outline'}
|
||||
label=${label}
|
||||
title=${label}
|
||||
?disabled=${forbidden}
|
||||
tiptap-toolbar-alias=${item.alias}
|
||||
@click=${() => this.#onRequestRemove(item, index)}>
|
||||
<div class="inner">
|
||||
<span>${label}</span>
|
||||
@@ -95,18 +93,29 @@ export class UmbTiptapToolbarGroupConfigurationElement<
|
||||
</uui-button>
|
||||
`;
|
||||
|
||||
case 'unknown':
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
data-mark="tiptap-toolbar-item:${item.alias}"
|
||||
color="danger"
|
||||
look="placeholder"
|
||||
label="Missing extension"
|
||||
title="Missing extension: ${item.alias}"
|
||||
@click=${() => this.#onRequestRemove(item, index)}></uui-button>
|
||||
`;
|
||||
|
||||
case 'button':
|
||||
case 'colorPickerButton':
|
||||
default:
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
class=${forbidden ? 'forbidden' : ''}
|
||||
class="draggable ${forbidden ? 'forbidden' : ''}"
|
||||
data-mark="tiptap-toolbar-item:${item.alias}"
|
||||
look=${forbidden ? 'placeholder' : 'outline'}
|
||||
label=${label}
|
||||
title=${label}
|
||||
?disabled=${forbidden}
|
||||
tiptap-toolbar-alias=${item.alias}
|
||||
@click=${() => this.#onRequestRemove(item, index)}>
|
||||
<div class="inner">
|
||||
${when(
|
||||
@@ -131,23 +140,18 @@ export class UmbTiptapToolbarGroupConfigurationElement<
|
||||
uui-button {
|
||||
--uui-button-font-weight: normal;
|
||||
|
||||
&[draggable='true'],
|
||||
&[draggable='true'] > .inner {
|
||||
&.draggable,
|
||||
&.draggable > .inner {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&[disabled] > .inner {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.forbidden {
|
||||
--color: var(--uui-color-danger);
|
||||
--color-standalone: var(--uui-color-danger-standalone);
|
||||
--color-emphasis: var(--uui-color-danger-emphasis);
|
||||
--color-contrast: var(--uui-color-danger);
|
||||
--uui-button-contrast-disabled: var(--uui-color-danger);
|
||||
--uui-button-border-color-disabled: var(--uui-color-danger);
|
||||
--uui-button-contrast: var(--uui-color-danger);
|
||||
--uui-button-border-color: var(--uui-color-danger);
|
||||
}
|
||||
|
||||
div {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
export type UmbTiptapSortableViewModel<T> = { unique: string; data: T };
|
||||
|
||||
export type UmbTiptapStatusbarExtension = {
|
||||
export type UmbTiptapExtensionBase = {
|
||||
kind?: string;
|
||||
alias: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
dependencies?: Array<string>;
|
||||
};
|
||||
|
||||
export type UmbTiptapStatusbarExtension = UmbTiptapExtensionBase;
|
||||
|
||||
export type UmbTiptapStatusbarViewModel = UmbTiptapSortableViewModel<Array<string>>;
|
||||
|
||||
export type UmbTiptapToolbarExtension = UmbTiptapStatusbarExtension & {
|
||||
kind?: string;
|
||||
};
|
||||
export type UmbTiptapToolbarExtension = UmbTiptapExtensionBase;
|
||||
|
||||
export type UmbTiptapToolbarRowViewModel = UmbTiptapSortableViewModel<Array<UmbTiptapToolbarGroupViewModel>>;
|
||||
export type UmbTiptapToolbarGroupViewModel = UmbTiptapSortableViewModel<Array<string>>;
|
||||
|
||||
Reference in New Issue
Block a user