Skip to content

Commit ae8411e

Browse files
NguyenThuyLanLanThuyNguyenleekelleher
authored
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 <[email protected]> Co-authored-by: leekelleher <[email protected]>
1 parent 1bd9583 commit ae8411e

File tree

3 files changed

+225
-104
lines changed

3 files changed

+225
-104
lines changed

src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts

Lines changed: 27 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import type {
55
UmbTiptapToolbarRowViewModel,
66
} from '../types.js';
77
import type { UmbTiptapToolbarValue } from '../../../components/types.js';
8+
import type { UmbTiptapToolbarGroupConfigurationElement } from './tiptap-toolbar-group-configuration.element.js';
89
import { customElement, css, html, property, repeat, state, when, nothing } from '@umbraco-cms/backoffice/external/lit';
910
import { debounce } from '@umbraco-cms/backoffice/utils';
1011
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
1112
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
1213
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
1314

15+
import './tiptap-toolbar-group-configuration.element.js';
16+
1417
@customElement('umb-property-editor-ui-tiptap-toolbar-configuration')
1518
export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
1619
extends UmbLitElement
@@ -74,7 +77,14 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
7477
this.#initialized = true;
7578
}
7679

77-
#onClick(item: UmbTiptapToolbarExtension) {
80+
#onChangeToolbarGroup(event: CustomEvent & { target: UmbTiptapToolbarGroupConfigurationElement }) {
81+
event.stopPropagation();
82+
const element = event.target;
83+
const aliases = element.items.map((item) => item.alias);
84+
this.#context.updateToolbarItem(aliases, [element.rowIndex, element.groupIndex]);
85+
}
86+
87+
#onClickAvailableItem(item: UmbTiptapToolbarExtension) {
7888
const lastRow = (this.#value?.length ?? 1) - 1;
7989
const lastGroup = (this.#value?.[lastRow].length ?? 1) - 1;
8090
const lastItem = this.#value?.[lastRow][lastGroup].length ?? 0;
@@ -128,6 +138,11 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
128138
this.#debouncedFilter(query);
129139
}
130140

141+
#onRemoveToolbarItem(event: CustomEvent) {
142+
const { groupIndex, index, rowIndex } = event.detail;
143+
this.#context?.removeToolbarItem([rowIndex, groupIndex, index]);
144+
}
145+
131146
override render() {
132147
return html`${this.#renderDesigner()} ${this.#renderAvailableItems()}`;
133148
}
@@ -173,7 +188,7 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
173188
label=${label}
174189
look=${forbidden ? 'placeholder' : 'outline'}
175190
?disabled=${forbidden || inUse}
176-
@click=${() => this.#onClick(item)}
191+
@click=${() => this.#onClickAvailableItem(item)}
177192
@dragstart=${(e: DragEvent) => this.#onDragStart(e, item.alias)}
178193
@dragend=${this.#onDragEnd}>
179194
<div class="inner">
@@ -242,19 +257,22 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
242257
#renderGroup(group?: UmbTiptapToolbarGroupViewModel, rowIndex = 0, groupIndex = 0) {
243258
if (!group) return nothing;
244259
const showActionBar = this._toolbar[rowIndex].data.length > 1 && group.data.length === 0;
260+
const items: UmbTiptapToolbarExtension[] = group!.data
261+
.map((alias) => this.#context?.getExtensionByAlias(alias))
262+
.filter((item): item is UmbTiptapToolbarExtension => !!item);
245263
return html`
246264
<div
247265
class="group"
248266
dropzone="move"
249267
@dragover=${this.#onDragOver}
250268
@drop=${(e: DragEvent) => this.#onDrop(e, [rowIndex, groupIndex, group.data.length - 1])}>
251-
<div class="items">
252-
${when(
253-
group?.data.length === 0,
254-
() => html`<em><umb-localize key="tiptap_toolbar_emptyGroup">Empty</umb-localize></em>`,
255-
() => html`${group!.data.map((alias, idx) => this.#renderItem(alias, rowIndex, groupIndex, idx))}`,
256-
)}
257-
</div>
269+
<umb-tiptap-toolbar-group-configuration
270+
.items=${items}
271+
.rowIndex=${rowIndex}
272+
.groupIndex=${groupIndex}
273+
@change=${this.#onChangeToolbarGroup}
274+
@remove=${this.#onRemoveToolbarItem}>
275+
</umb-tiptap-toolbar-group-configuration>
258276
${when(
259277
showActionBar,
260278
() => html`
@@ -276,62 +294,6 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
276294
`;
277295
}
278296

279-
#renderItem(alias: string, rowIndex = 0, groupIndex = 0, itemIndex = 0) {
280-
const item = this.#context?.getExtensionByAlias(alias);
281-
if (!item) return nothing;
282-
283-
const forbidden = !this.#context?.isExtensionEnabled(item.alias);
284-
const label = this.localize.string(item.label);
285-
286-
switch (item.kind) {
287-
case 'styleMenu':
288-
case 'menu':
289-
return html`
290-
<uui-button
291-
compact
292-
class=${forbidden ? 'forbidden' : ''}
293-
draggable="true"
294-
label=${label}
295-
look=${forbidden ? 'placeholder' : 'outline'}
296-
title=${label}
297-
?disabled=${forbidden}
298-
@click=${() => this.#context.removeToolbarItem([rowIndex, groupIndex, itemIndex])}
299-
@dragend=${this.#onDragEnd}
300-
@dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}>
301-
<div class="inner">
302-
<span>${label}</span>
303-
</div>
304-
<uui-symbol-expand slot="extra" open></uui-symbol-expand>
305-
</uui-button>
306-
`;
307-
308-
case 'button':
309-
default:
310-
return html`
311-
<uui-button
312-
compact
313-
class=${forbidden ? 'forbidden' : ''}
314-
data-mark="tiptap-toolbar-item:${item.alias}"
315-
draggable="true"
316-
look=${forbidden ? 'placeholder' : 'outline'}
317-
label=${label}
318-
title=${label}
319-
?disabled=${forbidden}
320-
@click=${() => this.#context.removeToolbarItem([rowIndex, groupIndex, itemIndex])}
321-
@dragend=${this.#onDragEnd}
322-
@dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}>
323-
<div class="inner">
324-
${when(
325-
item.icon,
326-
() => html`<umb-icon .name=${item.icon}></umb-icon>`,
327-
() => html`<span>${label}</span>`,
328-
)}
329-
</div>
330-
</uui-button>
331-
`;
332-
}
333-
}
334-
335297
static override readonly styles = [
336298
css`
337299
:host {
@@ -468,45 +430,6 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
468430
opacity: 1;
469431
}
470432
}
471-
472-
.items {
473-
display: flex;
474-
flex-direction: row;
475-
flex-wrap: wrap;
476-
gap: var(--uui-size-1);
477-
478-
uui-button {
479-
--uui-button-font-weight: normal;
480-
481-
&[draggable='true'],
482-
&[draggable='true'] > .inner {
483-
cursor: move;
484-
}
485-
486-
&[disabled],
487-
&[disabled] > .inner {
488-
cursor: not-allowed;
489-
}
490-
491-
&.forbidden {
492-
--color: var(--uui-color-danger);
493-
--color-standalone: var(--uui-color-danger-standalone);
494-
--color-emphasis: var(--uui-color-danger-emphasis);
495-
--color-contrast: var(--uui-color-danger);
496-
--uui-button-contrast-disabled: var(--uui-color-danger);
497-
--uui-button-border-color-disabled: var(--uui-color-danger);
498-
}
499-
500-
div {
501-
display: flex;
502-
gap: var(--uui-size-1);
503-
}
504-
505-
uui-symbol-expand {
506-
margin-left: var(--uui-size-space-2);
507-
}
508-
}
509-
}
510433
}
511434
}
512435
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { UmbTiptapToolbarConfigurationContext } from '../contexts/tiptap-toolbar-configuration.context.js';
2+
import type { UmbTiptapToolbarExtension } from '../types.js';
3+
import { css, customElement, html, property, repeat, when } from '@umbraco-cms/backoffice/external/lit';
4+
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
5+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
6+
import { UmbSorterController, UmbSorterResolvePlacementAsGrid } from '@umbraco-cms/backoffice/sorter';
7+
8+
@customElement('umb-tiptap-toolbar-group-configuration')
9+
export class UmbTiptapToolbarGroupConfigurationElement<
10+
TiptapToolbarItem extends UmbTiptapToolbarExtension = UmbTiptapToolbarExtension,
11+
> extends UmbLitElement {
12+
#sorter = new UmbSorterController<TiptapToolbarItem, HTMLElement>(this, {
13+
getUniqueOfElement: (element) => element.getAttribute('tiptap-toolbar-alias'),
14+
getUniqueOfModel: (modelEntry) => modelEntry.alias!,
15+
itemSelector: 'uui-button',
16+
identifier: 'umb-tiptap-toolbar-sorter',
17+
containerSelector: '.items',
18+
resolvePlacement: UmbSorterResolvePlacementAsGrid,
19+
onContainerChange: ({ item, model }) => {
20+
this.dispatchEvent(new CustomEvent('container-change', { detail: { item, model } }));
21+
},
22+
onChange: ({ model }) => {
23+
this.#items = model;
24+
this.requestUpdate();
25+
this.dispatchEvent(new UmbChangeEvent());
26+
},
27+
});
28+
29+
#context = new UmbTiptapToolbarConfigurationContext(this);
30+
31+
@property({ type: Array, attribute: false })
32+
public set items(value: Array<TiptapToolbarItem> | undefined) {
33+
this.#items = (value ?? []).filter((item, index, self) => self.findIndex((x) => x.alias === item.alias) === index);
34+
this.#sorter.setModel(this.#items);
35+
}
36+
public get items(): Array<TiptapToolbarItem> {
37+
return this.#items;
38+
}
39+
#items: Array<TiptapToolbarItem> = [];
40+
41+
@property({ type: Number })
42+
rowIndex = 0;
43+
44+
@property({ type: Number })
45+
groupIndex = 0;
46+
47+
#onRequestRemove(item: TiptapToolbarItem, index = 0) {
48+
this.items = this.items.filter((x) => x.alias !== item.alias);
49+
const rowIndex = this.rowIndex;
50+
const groupIndex = this.groupIndex;
51+
this.dispatchEvent(
52+
new CustomEvent('remove', { detail: { rowIndex, groupIndex, index }, bubbles: true, composed: true }),
53+
);
54+
}
55+
56+
override render() {
57+
return html`
58+
<div class="items">
59+
${when(
60+
this.items?.length === 0,
61+
() => html`<em><umb-localize key="tiptap_toolbar_emptyGroup">Empty</umb-localize></em>`,
62+
() =>
63+
repeat(
64+
this.items,
65+
(item) => item.alias,
66+
(item, index) => this.#renderItem(item, index),
67+
),
68+
)}
69+
</div>
70+
`;
71+
}
72+
73+
#renderItem(item: TiptapToolbarItem, index = 0) {
74+
const label = this.localize.string(item.label);
75+
const forbidden = !this.#context?.isExtensionEnabled(item.alias);
76+
77+
switch (item.kind) {
78+
case 'styleMenu':
79+
case 'menu':
80+
return html`
81+
<uui-button
82+
compact
83+
class=${forbidden ? 'forbidden' : ''}
84+
data-mark="tiptap-toolbar-item:${item.alias}"
85+
look=${forbidden ? 'placeholder' : 'outline'}
86+
label=${label}
87+
title=${label}
88+
?disabled=${forbidden}
89+
tiptap-toolbar-alias=${item.alias}
90+
@click=${() => this.#onRequestRemove(item, index)}>
91+
<div class="inner">
92+
<span>${label}</span>
93+
</div>
94+
<uui-symbol-expand slot="extra" open></uui-symbol-expand>
95+
</uui-button>
96+
`;
97+
98+
case 'button':
99+
default:
100+
return html`
101+
<uui-button
102+
compact
103+
class=${forbidden ? 'forbidden' : ''}
104+
data-mark="tiptap-toolbar-item:${item.alias}"
105+
look=${forbidden ? 'placeholder' : 'outline'}
106+
label=${label}
107+
title=${label}
108+
?disabled=${forbidden}
109+
tiptap-toolbar-alias=${item.alias}
110+
@click=${() => this.#onRequestRemove(item, index)}>
111+
<div class="inner">
112+
${when(
113+
item.icon,
114+
() => html`<umb-icon .name=${item.icon}></umb-icon>`,
115+
() => html`<span>${label}</span>`,
116+
)}
117+
</div>
118+
</uui-button>
119+
`;
120+
}
121+
}
122+
123+
static override styles = [
124+
css`
125+
.items {
126+
display: flex;
127+
flex-direction: row;
128+
flex-wrap: wrap;
129+
gap: var(--uui-size-1);
130+
131+
uui-button {
132+
--uui-button-font-weight: normal;
133+
134+
&[draggable='true'],
135+
&[draggable='true'] > .inner {
136+
cursor: move;
137+
}
138+
139+
&[disabled],
140+
&[disabled] > .inner {
141+
cursor: not-allowed;
142+
}
143+
144+
&.forbidden {
145+
--color: var(--uui-color-danger);
146+
--color-standalone: var(--uui-color-danger-standalone);
147+
--color-emphasis: var(--uui-color-danger-emphasis);
148+
--color-contrast: var(--uui-color-danger);
149+
--uui-button-contrast-disabled: var(--uui-color-danger);
150+
--uui-button-border-color-disabled: var(--uui-color-danger);
151+
}
152+
153+
div {
154+
display: flex;
155+
gap: var(--uui-size-1);
156+
}
157+
158+
uui-symbol-expand {
159+
margin-left: var(--uui-size-space-2);
160+
}
161+
}
162+
}
163+
164+
uui-button[look='outline'] {
165+
--uui-button-background-color-hover: var(--uui-color-surface);
166+
}
167+
`,
168+
];
169+
}
170+
171+
export default UmbTiptapToolbarGroupConfigurationElement;
172+
173+
declare global {
174+
interface HTMLElementTagNameMap {
175+
'umb-tiptap-toolbar-group-configuration': UmbTiptapToolbarGroupConfigurationElement;
176+
}
177+
}

src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,27 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase {
240240
this.#toolbar.setValue(toolbar);
241241
}
242242

243+
public updateToolbarItem(aliases: Array<string>, to: [number, number]) {
244+
const toolbar = [...this.#toolbar.getValue()];
245+
const [rowIndex, groupIndex] = to;
246+
247+
const newToolbar = toolbar.map((row, rIdx) => {
248+
if (rIdx !== rowIndex) return row;
249+
return {
250+
...row,
251+
data: row.data.map((group, gIdx) => {
252+
if (gIdx !== groupIndex) return group;
253+
return {
254+
...group,
255+
data: [...aliases],
256+
};
257+
}),
258+
};
259+
});
260+
261+
this.#toolbar.setValue(newToolbar);
262+
}
263+
243264
public updateToolbarRow(rowIndex: number, groups: Array<UmbTiptapToolbarGroupViewModel>) {
244265
const toolbar = [...this.#toolbar.getValue()];
245266

0 commit comments

Comments
 (0)