Tiptap RTE: Toolbar configuration sorter (#19901)
* Fix issue dragging tiptap toolbar buttons * Moved the `.items` CSS rules to the group element * Refactored the toolbar-group element - Renamed "toolbar-item-click" event to "remove", to show intent - Reordered the method names alphabetically - Renamed `value` to `items`, to show intent - Removed `toolbarValue`, as not required - Added `data-mark` for menu/styleMenu buttons * Renamed/relocated "umb-tiptap-toolbar-group-configuration" element * Updated tag name --------- Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk> Co-authored-by: leekelleher <leekelleher@gmail.com>
This commit is contained in:
@@ -5,12 +5,15 @@ import type {
|
||||
UmbTiptapToolbarRowViewModel,
|
||||
} from '../types.js';
|
||||
import type { UmbTiptapToolbarValue } from '../../../components/types.js';
|
||||
import type { UmbTiptapToolbarGroupConfigurationElement } from './tiptap-toolbar-group-configuration.element.js';
|
||||
import { customElement, css, html, property, repeat, state, when, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { debounce } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
|
||||
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
|
||||
|
||||
import './tiptap-toolbar-group-configuration.element.js';
|
||||
|
||||
@customElement('umb-property-editor-ui-tiptap-toolbar-configuration')
|
||||
export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
|
||||
extends UmbLitElement
|
||||
@@ -74,7 +77,14 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
|
||||
this.#initialized = true;
|
||||
}
|
||||
|
||||
#onClick(item: UmbTiptapToolbarExtension) {
|
||||
#onChangeToolbarGroup(event: CustomEvent & { target: UmbTiptapToolbarGroupConfigurationElement }) {
|
||||
event.stopPropagation();
|
||||
const element = event.target;
|
||||
const aliases = element.items.map((item) => item.alias);
|
||||
this.#context.updateToolbarItem(aliases, [element.rowIndex, element.groupIndex]);
|
||||
}
|
||||
|
||||
#onClickAvailableItem(item: UmbTiptapToolbarExtension) {
|
||||
const lastRow = (this.#value?.length ?? 1) - 1;
|
||||
const lastGroup = (this.#value?.[lastRow].length ?? 1) - 1;
|
||||
const lastItem = this.#value?.[lastRow][lastGroup].length ?? 0;
|
||||
@@ -128,6 +138,11 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
|
||||
this.#debouncedFilter(query);
|
||||
}
|
||||
|
||||
#onRemoveToolbarItem(event: CustomEvent) {
|
||||
const { groupIndex, index, rowIndex } = event.detail;
|
||||
this.#context?.removeToolbarItem([rowIndex, groupIndex, index]);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`${this.#renderDesigner()} ${this.#renderAvailableItems()}`;
|
||||
}
|
||||
@@ -173,7 +188,7 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
|
||||
label=${label}
|
||||
look=${forbidden ? 'placeholder' : 'outline'}
|
||||
?disabled=${forbidden || inUse}
|
||||
@click=${() => this.#onClick(item)}
|
||||
@click=${() => this.#onClickAvailableItem(item)}
|
||||
@dragstart=${(e: DragEvent) => this.#onDragStart(e, item.alias)}
|
||||
@dragend=${this.#onDragEnd}>
|
||||
<div class="inner">
|
||||
@@ -242,19 +257,22 @@ 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);
|
||||
return html`
|
||||
<div
|
||||
class="group"
|
||||
dropzone="move"
|
||||
@dragover=${this.#onDragOver}
|
||||
@drop=${(e: DragEvent) => this.#onDrop(e, [rowIndex, groupIndex, group.data.length - 1])}>
|
||||
<div class="items">
|
||||
${when(
|
||||
group?.data.length === 0,
|
||||
() => html`<em><umb-localize key="tiptap_toolbar_emptyGroup">Empty</umb-localize></em>`,
|
||||
() => html`${group!.data.map((alias, idx) => this.#renderItem(alias, rowIndex, groupIndex, idx))}`,
|
||||
)}
|
||||
</div>
|
||||
<umb-tiptap-toolbar-group-configuration
|
||||
.items=${items}
|
||||
.rowIndex=${rowIndex}
|
||||
.groupIndex=${groupIndex}
|
||||
@change=${this.#onChangeToolbarGroup}
|
||||
@remove=${this.#onRemoveToolbarItem}>
|
||||
</umb-tiptap-toolbar-group-configuration>
|
||||
${when(
|
||||
showActionBar,
|
||||
() => html`
|
||||
@@ -276,62 +294,6 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
|
||||
`;
|
||||
}
|
||||
|
||||
#renderItem(alias: string, rowIndex = 0, groupIndex = 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);
|
||||
|
||||
switch (item.kind) {
|
||||
case 'styleMenu':
|
||||
case 'menu':
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
class=${forbidden ? 'forbidden' : ''}
|
||||
draggable="true"
|
||||
label=${label}
|
||||
look=${forbidden ? 'placeholder' : 'outline'}
|
||||
title=${label}
|
||||
?disabled=${forbidden}
|
||||
@click=${() => this.#context.removeToolbarItem([rowIndex, groupIndex, itemIndex])}
|
||||
@dragend=${this.#onDragEnd}
|
||||
@dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}>
|
||||
<div class="inner">
|
||||
<span>${label}</span>
|
||||
</div>
|
||||
<uui-symbol-expand slot="extra" open></uui-symbol-expand>
|
||||
</uui-button>
|
||||
`;
|
||||
|
||||
case 'button':
|
||||
default:
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
class=${forbidden ? 'forbidden' : ''}
|
||||
data-mark="tiptap-toolbar-item:${item.alias}"
|
||||
draggable="true"
|
||||
look=${forbidden ? 'placeholder' : 'outline'}
|
||||
label=${label}
|
||||
title=${label}
|
||||
?disabled=${forbidden}
|
||||
@click=${() => this.#context.removeToolbarItem([rowIndex, groupIndex, itemIndex])}
|
||||
@dragend=${this.#onDragEnd}
|
||||
@dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}>
|
||||
<div class="inner">
|
||||
${when(
|
||||
item.icon,
|
||||
() => html`<umb-icon .name=${item.icon}></umb-icon>`,
|
||||
() => html`<span>${label}</span>`,
|
||||
)}
|
||||
</div>
|
||||
</uui-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static override readonly styles = [
|
||||
css`
|
||||
:host {
|
||||
@@ -468,45 +430,6 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--uui-size-1);
|
||||
|
||||
uui-button {
|
||||
--uui-button-font-weight: normal;
|
||||
|
||||
&[draggable='true'],
|
||||
&[draggable='true'] > .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);
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
gap: var(--uui-size-1);
|
||||
}
|
||||
|
||||
uui-symbol-expand {
|
||||
margin-left: var(--uui-size-space-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { UmbTiptapToolbarConfigurationContext } from '../contexts/tiptap-toolbar-configuration.context.js';
|
||||
import type { UmbTiptapToolbarExtension } from '../types.js';
|
||||
import { css, customElement, html, property, repeat, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbSorterController, UmbSorterResolvePlacementAsGrid } from '@umbraco-cms/backoffice/sorter';
|
||||
|
||||
@customElement('umb-tiptap-toolbar-group-configuration')
|
||||
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',
|
||||
identifier: 'umb-tiptap-toolbar-sorter',
|
||||
containerSelector: '.items',
|
||||
resolvePlacement: UmbSorterResolvePlacementAsGrid,
|
||||
onContainerChange: ({ item, model }) => {
|
||||
this.dispatchEvent(new CustomEvent('container-change', { detail: { item, model } }));
|
||||
},
|
||||
onChange: ({ model }) => {
|
||||
this.#items = model;
|
||||
this.requestUpdate();
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
},
|
||||
});
|
||||
|
||||
#context = new UmbTiptapToolbarConfigurationContext(this);
|
||||
|
||||
@property({ type: Array, attribute: false })
|
||||
public set items(value: Array<TiptapToolbarItem> | undefined) {
|
||||
this.#items = (value ?? []).filter((item, index, self) => self.findIndex((x) => x.alias === item.alias) === index);
|
||||
this.#sorter.setModel(this.#items);
|
||||
}
|
||||
public get items(): Array<TiptapToolbarItem> {
|
||||
return this.#items;
|
||||
}
|
||||
#items: Array<TiptapToolbarItem> = [];
|
||||
|
||||
@property({ type: Number })
|
||||
rowIndex = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
groupIndex = 0;
|
||||
|
||||
#onRequestRemove(item: TiptapToolbarItem, index = 0) {
|
||||
this.items = this.items.filter((x) => x.alias !== item.alias);
|
||||
const rowIndex = this.rowIndex;
|
||||
const groupIndex = this.groupIndex;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('remove', { detail: { rowIndex, groupIndex, index }, bubbles: true, composed: true }),
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="items">
|
||||
${when(
|
||||
this.items?.length === 0,
|
||||
() => html`<em><umb-localize key="tiptap_toolbar_emptyGroup">Empty</umb-localize></em>`,
|
||||
() =>
|
||||
repeat(
|
||||
this.items,
|
||||
(item) => item.alias,
|
||||
(item, index) => this.#renderItem(item, index),
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderItem(item: TiptapToolbarItem, index = 0) {
|
||||
const label = this.localize.string(item.label);
|
||||
const forbidden = !this.#context?.isExtensionEnabled(item.alias);
|
||||
|
||||
switch (item.kind) {
|
||||
case 'styleMenu':
|
||||
case 'menu':
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
class=${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>
|
||||
</div>
|
||||
<uui-symbol-expand slot="extra" open></uui-symbol-expand>
|
||||
</uui-button>
|
||||
`;
|
||||
|
||||
case 'button':
|
||||
default:
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
class=${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(
|
||||
item.icon,
|
||||
() => html`<umb-icon .name=${item.icon}></umb-icon>`,
|
||||
() => html`<span>${label}</span>`,
|
||||
)}
|
||||
</div>
|
||||
</uui-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
.items {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--uui-size-1);
|
||||
|
||||
uui-button {
|
||||
--uui-button-font-weight: normal;
|
||||
|
||||
&[draggable='true'],
|
||||
&[draggable='true'] > .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);
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
gap: var(--uui-size-1);
|
||||
}
|
||||
|
||||
uui-symbol-expand {
|
||||
margin-left: var(--uui-size-space-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uui-button[look='outline'] {
|
||||
--uui-button-background-color-hover: var(--uui-color-surface);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbTiptapToolbarGroupConfigurationElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-tiptap-toolbar-group-configuration': UmbTiptapToolbarGroupConfigurationElement;
|
||||
}
|
||||
}
|
||||
@@ -240,6 +240,27 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase {
|
||||
this.#toolbar.setValue(toolbar);
|
||||
}
|
||||
|
||||
public updateToolbarItem(aliases: Array<string>, to: [number, number]) {
|
||||
const toolbar = [...this.#toolbar.getValue()];
|
||||
const [rowIndex, groupIndex] = to;
|
||||
|
||||
const newToolbar = toolbar.map((row, rIdx) => {
|
||||
if (rIdx !== rowIndex) return row;
|
||||
return {
|
||||
...row,
|
||||
data: row.data.map((group, gIdx) => {
|
||||
if (gIdx !== groupIndex) return group;
|
||||
return {
|
||||
...group,
|
||||
data: [...aliases],
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
this.#toolbar.setValue(newToolbar);
|
||||
}
|
||||
|
||||
public updateToolbarRow(rowIndex: number, groups: Array<UmbTiptapToolbarGroupViewModel>) {
|
||||
const toolbar = [...this.#toolbar.getValue()];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user