@navette-js/forms 1.0.0-beta.2 → 1.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/builder.d.ts +45 -4
- package/lib/builder.js +99 -16
- package/lib/index.d.ts +722 -68
- package/lib/index.js +1705 -234
- package/lib/rules.d.ts +19 -0
- package/lib/rules.js +148 -0
- package/package.json +4 -4
- package/views/components/text-input.ejs +14 -5
package/lib/builder.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ColorManager } from '@navette-js/support';
|
|
2
2
|
import { Action } from '@navette-js/actions';
|
|
3
3
|
import { Schema, type Component } from '@navette-js/schemas';
|
|
4
|
-
import { Field } from './index.ts';
|
|
4
|
+
import { Field, type Rule } from './index.ts';
|
|
5
5
|
/** Whether an item key is acceptable: keys come from the browser. */
|
|
6
6
|
export declare const isItemKey: (key: string) => boolean;
|
|
7
7
|
/** Random four-character alphanumeric key. */
|
|
@@ -32,9 +32,17 @@ export declare class Block {
|
|
|
32
32
|
private _icon;
|
|
33
33
|
private _schema;
|
|
34
34
|
private _defaults;
|
|
35
|
+
private _pickerGroup;
|
|
36
|
+
private _maxItems;
|
|
35
37
|
constructor(name: string);
|
|
38
|
+
/** How many items of this block the builder may hold; at the limit, the block leaves the picker (checked on the server). */
|
|
39
|
+
maxItems(count: number | null): this;
|
|
40
|
+
getMaxItems(): number | null;
|
|
36
41
|
static make(name: string): Block;
|
|
37
42
|
label(label: string | null): this;
|
|
43
|
+
/** Group of the block in the block picker (also set by passing `{ 'Group label': [blocks] }` to `blocks()`). */
|
|
44
|
+
pickerGroup(label: string | null): this;
|
|
45
|
+
getPickerGroup(): string | null;
|
|
38
46
|
icon(name: string | null): this;
|
|
39
47
|
/** Item components: a function called for each item (with its current data). */
|
|
40
48
|
schema(factory: (ctx: BlockContext) => Component[]): this;
|
|
@@ -66,10 +74,20 @@ export declare class Builder extends Field {
|
|
|
66
74
|
private _addIcon;
|
|
67
75
|
private _addAlignment;
|
|
68
76
|
private _maxItems;
|
|
77
|
+
private _minItems;
|
|
78
|
+
private _addable;
|
|
79
|
+
private _deletable;
|
|
80
|
+
private _reorderable;
|
|
81
|
+
private _reorderableWithButtons;
|
|
82
|
+
private _collapsible;
|
|
83
|
+
private _collapsed;
|
|
84
|
+
private _persistCollapsed;
|
|
85
|
+
private _searchable;
|
|
69
86
|
private _extraClass;
|
|
70
87
|
private _uuid;
|
|
71
88
|
static make(name: string): Builder;
|
|
72
|
-
|
|
89
|
+
/** Blocks, or `{ 'Group label': [blocks] }` to group them in the block picker. */
|
|
90
|
+
blocks(blocks: Block[] | Record<string, Block[]> | (() => Block[])): this;
|
|
73
91
|
blockPickerColumns(columns?: number | null): this;
|
|
74
92
|
blockPickerWidth(width: string | null): this;
|
|
75
93
|
labelBetweenItems(label: string | null): this;
|
|
@@ -89,12 +107,35 @@ export declare class Builder extends Field {
|
|
|
89
107
|
addActionIcon(name: string | null): this;
|
|
90
108
|
addActionAlignment(alignment: 'start' | 'center' | 'end' | null): this;
|
|
91
109
|
maxItems(count: number): this;
|
|
110
|
+
/** Minimum number of items, checked on the server. */
|
|
111
|
+
minItems(count: number | null): this;
|
|
112
|
+
/** `false`: no block picker (adding is refused on the server). */
|
|
113
|
+
addable(condition?: boolean): this;
|
|
114
|
+
/** `false`: no delete button (deleting is refused on the server). */
|
|
115
|
+
deletable(condition?: boolean): this;
|
|
116
|
+
/** `false`: items keep their order. */
|
|
117
|
+
reorderable(condition?: boolean): this;
|
|
118
|
+
private _dragAndDrop;
|
|
119
|
+
/** `false`: no drag and drop (buttons may still reorder, with `reorderableWithButtons()`). */
|
|
120
|
+
reorderableWithDragAndDrop(condition?: boolean): this;
|
|
121
|
+
private canDrag;
|
|
122
|
+
private canMove;
|
|
123
|
+
/** Move up / move down buttons in each item header. */
|
|
124
|
+
reorderableWithButtons(condition?: boolean): this;
|
|
125
|
+
/** Items can be collapsed from their header; `collapsed()` collapses them on load. */
|
|
126
|
+
collapsible(condition?: boolean): this;
|
|
127
|
+
collapsed(condition?: boolean): this;
|
|
128
|
+
/** Keeps each item's collapsed state in the browser (local storage). */
|
|
129
|
+
persistCollapsed(condition?: boolean): this;
|
|
130
|
+
/** Search field in the block picker. */
|
|
131
|
+
searchable(condition?: boolean): this;
|
|
132
|
+
getValidationRules(): Promise<Rule[]>;
|
|
92
133
|
/** Class added to the root (e.g. `fi-fo-builder-not-contained`). */
|
|
93
134
|
extraClass(name: string | null): this;
|
|
94
135
|
generateUuidUsing(generator: () => string): this;
|
|
95
136
|
getBlocks(): Block[];
|
|
96
137
|
getBlock(name: unknown): Block | undefined;
|
|
97
|
-
/** Blocks offered in the picker (a subclass may remove
|
|
138
|
+
/** Blocks offered in the picker: those under their `maxItems()` (a subclass may remove more). */
|
|
98
139
|
getBlockPickerBlocks(): Block[];
|
|
99
140
|
/** Well-formed items only: simple key, declared type, object data. Anything else the browser sends is ignored. */
|
|
100
141
|
getItems(): Record<string, Item>;
|
|
@@ -115,7 +156,7 @@ export declare class Builder extends Field {
|
|
|
115
156
|
getChildSchemas(): Schema[];
|
|
116
157
|
protected newItem(block: Block): Item;
|
|
117
158
|
/** A subclass may refuse adding (a rule cap, for instance). */
|
|
118
|
-
protected canAdd(
|
|
159
|
+
protected canAdd(block: Block): boolean;
|
|
119
160
|
getActions(): Action[];
|
|
120
161
|
/** Copy of an item; a subclass renews the keys of nested items. */
|
|
121
162
|
protected cloneItem(item: Item): Item;
|
package/lib/builder.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @navette-js/forms/builder — a list of items, each one a "block" picked from a menu.
|
|
2
2
|
// The state is a map `item key → { type, data }`; key order is display order.
|
|
3
|
-
// Not supported yet: block previews
|
|
3
|
+
// Not supported yet: block previews.
|
|
4
4
|
import { Attributes, cls, e, icon, js, loadingIndicator, views, configured } from '@navette-js/support';
|
|
5
5
|
import { t } from '@navette-js/support/i18n';
|
|
6
6
|
import { Action } from '@navette-js/actions';
|
|
@@ -45,9 +45,17 @@ export class Block {
|
|
|
45
45
|
_icon = null;
|
|
46
46
|
_schema = () => [];
|
|
47
47
|
_defaults = null;
|
|
48
|
+
_pickerGroup = null;
|
|
49
|
+
_maxItems = null;
|
|
48
50
|
constructor(name) { this.name = name; }
|
|
51
|
+
/** How many items of this block the builder may hold; at the limit, the block leaves the picker (checked on the server). */
|
|
52
|
+
maxItems(count) { this._maxItems = count; return this; }
|
|
53
|
+
getMaxItems() { return this._maxItems; }
|
|
49
54
|
static make(name) { return configured(new Block(name)); }
|
|
50
55
|
label(label) { this._label = label; return this; }
|
|
56
|
+
/** Group of the block in the block picker (also set by passing `{ 'Group label': [blocks] }` to `blocks()`). */
|
|
57
|
+
pickerGroup(label) { this._pickerGroup = label; return this; }
|
|
58
|
+
getPickerGroup() { return this._pickerGroup; }
|
|
51
59
|
icon(name) { this._icon = name; return this; }
|
|
52
60
|
/** Item components: a function called for each item (with its current data). */
|
|
53
61
|
schema(factory) { this._schema = factory; return this; }
|
|
@@ -77,10 +85,24 @@ export class Builder extends Field {
|
|
|
77
85
|
_addIcon = null;
|
|
78
86
|
_addAlignment = null;
|
|
79
87
|
_maxItems = 100;
|
|
88
|
+
_minItems = null;
|
|
89
|
+
_addable = true;
|
|
90
|
+
_deletable = true;
|
|
91
|
+
_reorderable = true;
|
|
92
|
+
_reorderableWithButtons = false;
|
|
93
|
+
_collapsible = false;
|
|
94
|
+
_collapsed = false;
|
|
95
|
+
_persistCollapsed = false;
|
|
96
|
+
_searchable = false;
|
|
80
97
|
_extraClass = null;
|
|
81
98
|
_uuid = randomUUID;
|
|
82
99
|
static make(name) { return configured(new Builder(name)); }
|
|
83
|
-
|
|
100
|
+
/** Blocks, or `{ 'Group label': [blocks] }` to group them in the block picker. */
|
|
101
|
+
blocks(blocks) {
|
|
102
|
+
this._blocks = typeof blocks === 'function' || Array.isArray(blocks) ? blocks : Object.entries(blocks).flatMap(([group, list]) => list.map((block) => block.pickerGroup(group)));
|
|
103
|
+
this._resolvedBlocks = null;
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
84
106
|
blockPickerColumns(columns = 2) { this._pickerColumns = columns; return this; }
|
|
85
107
|
blockPickerWidth(width) { this._pickerWidth = width; return this; }
|
|
86
108
|
labelBetweenItems(label) { this._labelBetweenItems = label; return this; }
|
|
@@ -100,13 +122,49 @@ export class Builder extends Field {
|
|
|
100
122
|
addActionIcon(name) { this._addIcon = name; return this; }
|
|
101
123
|
addActionAlignment(alignment) { this._addAlignment = alignment; return this; }
|
|
102
124
|
maxItems(count) { this._maxItems = count; return this; }
|
|
125
|
+
/** Minimum number of items, checked on the server. */
|
|
126
|
+
minItems(count) { this._minItems = count; return this; }
|
|
127
|
+
/** `false`: no block picker (adding is refused on the server). */
|
|
128
|
+
addable(condition = true) { this._addable = condition; return this; }
|
|
129
|
+
/** `false`: no delete button (deleting is refused on the server). */
|
|
130
|
+
deletable(condition = true) { this._deletable = condition; return this; }
|
|
131
|
+
/** `false`: items keep their order. */
|
|
132
|
+
reorderable(condition = true) { this._reorderable = condition; return this; }
|
|
133
|
+
_dragAndDrop = true;
|
|
134
|
+
/** `false`: no drag and drop (buttons may still reorder, with `reorderableWithButtons()`). */
|
|
135
|
+
reorderableWithDragAndDrop(condition = true) { this._dragAndDrop = condition; return this; }
|
|
136
|
+
canDrag() { return this._reorderable && this._dragAndDrop; }
|
|
137
|
+
canMove() { return this._reorderable && (this._dragAndDrop || this._reorderableWithButtons); }
|
|
138
|
+
/** Move up / move down buttons in each item header. */
|
|
139
|
+
reorderableWithButtons(condition = true) { this._reorderableWithButtons = condition; return this; }
|
|
140
|
+
/** Items can be collapsed from their header; `collapsed()` collapses them on load. */
|
|
141
|
+
collapsible(condition = true) { this._collapsible = condition; return this; }
|
|
142
|
+
collapsed(condition = true) { this._collapsed = condition; if (condition)
|
|
143
|
+
this._collapsible = true; return this; }
|
|
144
|
+
/** Keeps each item's collapsed state in the browser (local storage). */
|
|
145
|
+
persistCollapsed(condition = true) { this._persistCollapsed = condition; return this; }
|
|
146
|
+
/** Search field in the block picker. */
|
|
147
|
+
searchable(condition = true) { this._searchable = condition; return this; }
|
|
148
|
+
async getValidationRules() {
|
|
149
|
+
const count = () => Object.keys(this.getItems()).length;
|
|
150
|
+
const rules = [];
|
|
151
|
+
if (await this.isRequired())
|
|
152
|
+
rules.push({ name: 'required', passes: () => count() > 0, message: (a) => `The ${a} field is required.` });
|
|
153
|
+
if (this._minItems !== null)
|
|
154
|
+
rules.push({ name: 'min', implicit: true, passes: () => count() >= this._minItems, message: (a) => `The ${a} field must have at least ${this._minItems} items.` });
|
|
155
|
+
const items = () => Object.values(this.getItems());
|
|
156
|
+
for (const block of this.getBlocks())
|
|
157
|
+
if (block.getMaxItems() !== null)
|
|
158
|
+
rules.push({ name: 'max', implicit: true, passes: () => items().filter((item) => item.type === block.name).length <= block.getMaxItems(), message: (a) => `The ${a} field must not have more than ${block.getMaxItems()} ${block.getLabel()} items.` });
|
|
159
|
+
return [...rules, ...this._rules];
|
|
160
|
+
}
|
|
103
161
|
/** Class added to the root (e.g. `fi-fo-builder-not-contained`). */
|
|
104
162
|
extraClass(name) { this._extraClass = name; return this; }
|
|
105
163
|
generateUuidUsing(generator) { this._uuid = generator; return this; }
|
|
106
164
|
getBlocks() { return (this._resolvedBlocks ??= typeof this._blocks === 'function' ? this._blocks() : this._blocks); }
|
|
107
165
|
getBlock(name) { return typeof name === 'string' ? this.getBlocks().find((block) => block.name === name) : undefined; }
|
|
108
|
-
/** Blocks offered in the picker (a subclass may remove
|
|
109
|
-
getBlockPickerBlocks() { return this.getBlocks(); }
|
|
166
|
+
/** Blocks offered in the picker: those under their `maxItems()` (a subclass may remove more). */
|
|
167
|
+
getBlockPickerBlocks() { const items = Object.values(this.getItems()); return this.getBlocks().filter((block) => block.getMaxItems() === null || items.filter((item) => item.type === block.name).length < block.getMaxItems()); }
|
|
110
168
|
/** Well-formed items only: simple key, declared type, object data. Anything else the browser sends is ignored. */
|
|
111
169
|
getItems() {
|
|
112
170
|
const state = this.context().state;
|
|
@@ -165,7 +223,7 @@ export class Builder extends Field {
|
|
|
165
223
|
/** Item schema: key `<builder>.<item>.data`, state at the same path. */
|
|
166
224
|
itemSchema(itemKey, item) {
|
|
167
225
|
const container = this.container;
|
|
168
|
-
return new Schema(container.livewire).key(`${this.getKey()}.${itemKey}.data`).statePath(`${this.getStatePath()}.${itemKey}.data`).operation(container.getOperation()).record(container.getRecord()).root(false)
|
|
226
|
+
return new Schema(container.livewire).key(`${this.getKey()}.${itemKey}.data`).statePath(`${this.getStatePath()}.${itemKey}.data`).operation(container.getOperation()).record(container.getRecord()).root(false).parentComponent(this)
|
|
169
227
|
.components(this.getBlock(item.type).getComponents({ builder: this, itemKey, data: item.data }));
|
|
170
228
|
}
|
|
171
229
|
getChildSchemas() { return Object.entries(this.getItems()).map(([key, item]) => this.itemSchema(key, item)); }
|
|
@@ -179,7 +237,10 @@ export class Builder extends Field {
|
|
|
179
237
|
return item;
|
|
180
238
|
}
|
|
181
239
|
/** A subclass may refuse adding (a rule cap, for instance). */
|
|
182
|
-
canAdd(
|
|
240
|
+
canAdd(block) {
|
|
241
|
+
const items = Object.values(this.getItems());
|
|
242
|
+
return this._addable && items.length < this._maxItems && (block.getMaxItems() === null || items.filter((item) => item.type === block.name).length < block.getMaxItems());
|
|
243
|
+
}
|
|
183
244
|
getActions() {
|
|
184
245
|
const insert = (items, after, key, item) => {
|
|
185
246
|
if (after === null || !Object.hasOwn(items, after))
|
|
@@ -187,6 +248,8 @@ export class Builder extends Field {
|
|
|
187
248
|
return Object.fromEntries(Object.entries(items).flatMap(([k, v]) => (k === after ? [[k, v], [key, item]] : [[k, v]])));
|
|
188
249
|
};
|
|
189
250
|
const move = (offset) => ({ arguments: args }) => {
|
|
251
|
+
if (!this.canMove())
|
|
252
|
+
return;
|
|
190
253
|
const items = this.getItems();
|
|
191
254
|
const keys = Object.keys(items);
|
|
192
255
|
const from = keys.indexOf(String(args.item));
|
|
@@ -211,7 +274,7 @@ export class Builder extends Field {
|
|
|
211
274
|
return;
|
|
212
275
|
this.setItems(insert(items, args.afterItem, this._uuid(), this.newItem(block)));
|
|
213
276
|
}),
|
|
214
|
-
Action.make('delete').action(({ arguments: args }) => { const items = this.getItems(); const key = String(args.item); if (!Object.hasOwn(items, key))
|
|
277
|
+
Action.make('delete').action(({ arguments: args }) => { const items = this.getItems(); const key = String(args.item); if (!this._deletable || !Object.hasOwn(items, key))
|
|
215
278
|
return; delete items[key]; this.setItems(items); }),
|
|
216
279
|
Action.make('clone').action(({ arguments: args }) => {
|
|
217
280
|
const items = this.getItems();
|
|
@@ -224,6 +287,8 @@ export class Builder extends Field {
|
|
|
224
287
|
Action.make('moveDown').action(move(1)),
|
|
225
288
|
// Drag and drop: the new order is only accepted if it is a permutation of the existing keys.
|
|
226
289
|
Action.make('reorder').action(({ arguments: args }) => {
|
|
290
|
+
if (!this.canDrag())
|
|
291
|
+
return;
|
|
227
292
|
const items = this.getItems();
|
|
228
293
|
const keys = Object.keys(items);
|
|
229
294
|
const order = Array.isArray(args.items) ? args.items.map(String) : [];
|
|
@@ -249,20 +314,31 @@ export class Builder extends Field {
|
|
|
249
314
|
const alignment = between ? null : this._addAlignment;
|
|
250
315
|
const placement = alignment === 'start' ? 'bottom-start' : alignment === 'end' ? 'bottom-end' : null;
|
|
251
316
|
const widthClass = this._pickerWidth ? `fi-width-${this._pickerWidth}` : null;
|
|
252
|
-
const signature = `${widthClass ?? ''}|${placement ?? ''}|${blocks.length ? 1 : 0}
|
|
317
|
+
const signature = `${widthClass ?? ''}|${placement ?? ''}|${blocks.length ? 1 : 0}|${this._searchable ? 1 : 0}`;
|
|
253
318
|
const wireKey = `${key}.${actionName}.${afterItem ?? ''}.block-picker.${PICKER_HASHES[signature] ?? createHash('md5').update(signature).digest('hex')}`;
|
|
254
319
|
const items = blocks.map((block) => {
|
|
255
320
|
const expression = `mountAction('${actionName}', ${js(between ? { block: block.name, afterItem } : { block: block.name })}, { schemaComponent: ${js(key)} })`;
|
|
256
321
|
const blockIcon = block.getIcon();
|
|
257
|
-
|
|
322
|
+
const search = this._searchable ? ` role="button" tabindex="0" wire:key="${e(createHash('md5').update(expression + block.getLabel().toLowerCase()).digest('hex'))}" data-block-label="${e(block.getLabel().toLowerCase())}" x-show="isBlockVisible($el)"` : '';
|
|
323
|
+
return `<button x-on:click="close" wire:click="${e(expression)}"${search} type="button" wire:loading.attr="disabled" wire:target="${e(expression)}" class="fi-dropdown-list-item">${blockIcon ? icon(blockIcon, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': expression }) }) + loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': expression })) : ''}<span class="fi-dropdown-list-item-label">${e(block.getLabel())}</span></button>`;
|
|
258
324
|
});
|
|
259
325
|
const grid = this._pickerColumns ? ` class="fi-grid fi-grid-direction-col lg:fi-grid-cols" style="--cols-lg: ${this._pickerColumns}; --cols-default: 1;"` : ' class="fi-grid fi-grid-direction-col" style="--cols-default: 1;"';
|
|
260
|
-
|
|
326
|
+
// Grouped blocks: one labelled group per picker group (ungrouped blocks first), the groups laid out in the grid.
|
|
327
|
+
const groups = new Map();
|
|
328
|
+
blocks.forEach((block, index) => { const group = block.getPickerGroup() ?? ''; groups.set(group, [...(groups.get(group) ?? []), items[index]]); });
|
|
329
|
+
const grouped = [...groups.keys()].some((group) => group !== '');
|
|
330
|
+
const content = grouped
|
|
331
|
+
? `<div${grid.replace(/class="([^"]*)"/, 'class="$1 fi-fo-builder-block-picker-groups"')}>${[...groups].map(([group, groupItems]) => `<div class="fi-fo-builder-block-picker-group"${group ? ` role="group" aria-label="${e(group)}"` : ''}>${group ? `<div class="fi-fo-builder-block-picker-group-label">${e(group)}</div>` : ''}${groupItems.join('')}</div>`).join('')}</div>`
|
|
332
|
+
: `<div${grid}>${items.join('')}</div>`;
|
|
333
|
+
const searchPrompt = tr('block_picker.search_prompt');
|
|
334
|
+
const searchInput = this._searchable ? `<div class="fi-fo-builder-block-picker-search-ctn"><div class="fi-input-wrp"><div class="fi-input-wrp-content-ctn"><input type="text" x-ref="searchInput" x-on:keydown.enter.prevent="" aria-label="${e(searchPrompt)}" placeholder="${e(searchPrompt)}" x-model.debounce.0="search" class="fi-input" /></div></div></div>` : '';
|
|
335
|
+
const noResults = this._searchable ? `<div x-cloak x-show="hasNoSearchResults" role="status" aria-live="polite" class="fi-fo-builder-block-picker-no-search-results-message">${e(tr('block_picker.no_search_results_message'))}</div>` : '';
|
|
336
|
+
const list = blocks.length ? `<div${this._searchable ? ' x-data="blockPicker()"' : ''} class="fi-dropdown-list">${searchInput}<div>${content}</div>${noResults}</div>` : '';
|
|
261
337
|
const addIcon = !between && this._addIcon ? icon(this._addIcon, { size: 'sm' }) : '';
|
|
262
338
|
const triggerLabel = between
|
|
263
339
|
? (this._addBetweenLabel ?? tr('actions.add_between.label'))
|
|
264
340
|
: (this._addLabel ?? tr('actions.add.label').replace(':label', this._labelText.replace(/^./, (c) => c.toLowerCase())).trim());
|
|
265
|
-
return `<div x-data="filamentDropdown({ isMenu: true })" wire:key="${e(wireKey)}" class="${cls(['fi-dropdown', 'fi-fo-builder-block-picker', alignment ? `fi-align-${alignment}` : null])}"><div x-on:mousedown="if ($event.button === 0) toggle($event)" class="fi-dropdown-trigger"><button type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="fi-ac-btn-action fi-btn fi-size-sm">${addIcon}${e(triggerLabel)}</button></div><div x-cloak="x-cloak" x-ref="panel" wire:key="${e(wireKey)}.panel" wire:ignore.self="" x-transition:enter-start="fi-opacity-0" x-transition:leave-end="fi-opacity-0" x-float${placement ? `.placement.${placement}` : ''}.flip.shift.offset="{ offset: 8 }" class="${cls(['fi-dropdown-panel', widthClass])}">${list}</div></div>`;
|
|
341
|
+
return `<div x-data="filamentDropdown({ isMenu: ${this._searchable ? 'false' : 'true'} })" wire:key="${e(wireKey)}" class="${cls(['fi-dropdown', 'fi-fo-builder-block-picker', alignment ? `fi-align-${alignment}` : null])}"><div x-on:mousedown="if ($event.button === 0) toggle($event)" class="fi-dropdown-trigger"><button type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="fi-ac-btn-action fi-btn fi-size-sm">${addIcon}${e(triggerLabel)}</button></div><div x-cloak="x-cloak" x-ref="panel" wire:key="${e(wireKey)}.panel" wire:ignore.self="" x-transition:enter-start="fi-opacity-0" x-transition:leave-end="fi-opacity-0" x-float${placement ? `.placement.${placement}` : ''}.flip.shift.offset="{ offset: 8 }" class="${cls(['fi-dropdown-panel', widthClass])}">${list}</div></div>`;
|
|
266
342
|
}
|
|
267
343
|
_labelText = '';
|
|
268
344
|
async toHtml() {
|
|
@@ -283,13 +359,17 @@ export class Builder extends Field {
|
|
|
283
359
|
if (this._blockHeaders) {
|
|
284
360
|
const moveButton = (action, label, iconName, direction, disabled) => {
|
|
285
361
|
const expression = this.actionExpression(action, { item: itemKey });
|
|
286
|
-
return `<li x-on:click.stop class="fi-sr-only">${iconActionButton({ inHeader: true, label, icon: iconName, expression, disabled, attributes: { tabindex: '-1', 'data-reorder-hidden': 'data-reorder-hidden', 'data-reorder-direction': direction, 'data-reorder-action': expression, 'aria-describedby': descriptionId(itemKey) } })}</li>`;
|
|
362
|
+
return `<li x-on:click.stop${this._reorderableWithButtons ? '' : ' class="fi-sr-only"'}>${iconActionButton({ inHeader: true, label, icon: iconName, expression, disabled, attributes: { tabindex: this._reorderableWithButtons ? null : '-1', 'data-reorder-hidden': 'data-reorder-hidden', 'data-reorder-direction': direction, 'data-reorder-action': expression, 'aria-describedby': descriptionId(itemKey) } })}</li>`;
|
|
287
363
|
};
|
|
288
364
|
const blockIcon = block.getIcon();
|
|
289
|
-
|
|
365
|
+
const gray = colorsOf(this.livewire)?.getComponentClasses('icon-button', 'gray') ?? [];
|
|
366
|
+
const collapse = this._collapsible ? `<li class="fi-fo-builder-item-header-collapsible-actions" x-on:click.stop="toggleCollapsed"><div class="fi-fo-builder-item-header-collapse-action"><button title="${e(tr('actions.collapse.label'))}" aria-label="${e(tr('actions.collapse.label'))}" type="button" class="${cls(['fi-ac-icon-btn-action', 'fi-icon-btn', 'fi-size-sm', ...gray])}">${icon('m-chevron-up')}</button></div><div class="fi-fo-builder-item-header-expand-action"><button title="${e(tr('actions.expand.label'))}" aria-label="${e(tr('actions.expand.label'))}" type="button" class="${cls(['fi-ac-icon-btn-action', 'fi-icon-btn', 'fi-size-sm', ...gray])}">${icon('m-chevron-down')}</button></div></li>` : '';
|
|
367
|
+
const reorder = this.canDrag() ? `<li x-on:click.stop><button title="${e(tr('actions.reorder.label'))}" aria-label="${e(tr('actions.reorder.label'))}" type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="fi-ac-icon-btn-action fi-icon-btn fi-size-sm" x-sortable-handle="x-sortable-handle">${icon('m-arrows-up-down')}</button></li>` : '';
|
|
368
|
+
const moves = this.canMove() ? `${moveButton('moveUp', tr('actions.move_up.label'), 'm-arrow-up', 'up', index === 0)}${moveButton('moveDown', tr('actions.move_down.label'), 'm-arrow-down', 'down', index === keys.length - 1)}` : '';
|
|
369
|
+
header = `<div${this._collapsible ? ' x-on:click.stop="toggleCollapsed"' : ''} class="fi-fo-builder-item-header">${reorder || moves ? `<ul class="fi-fo-builder-item-header-start-actions">${reorder}${moves}</ul>` : ''}${this._blockIcons && blockIcon ? icon(blockIcon, { attributes: new Attributes().class(['fi-fo-builder-item-header-icon']) }) : ''}<${this._headingTag} class="fi-fo-builder-item-header-label fi-truncated">${e(block.getLabel())}${this._blockNumbers ? ` ${index + 1}` : ''}</${this._headingTag}><ul class="fi-fo-builder-item-header-end-actions">${this._cloneable ? `<li x-on:click.stop>${iconActionButton({ inHeader: true, label: tr('actions.clone.label'), icon: 'm-square-2-stack', expression: this.actionExpression('clone', { item: itemKey }) })}</li>` : ''}${this._deletable ? `<li x-on:click.stop>${iconActionButton({ inHeader: true, label: tr('actions.delete.label'), icon: 'm-trash', expression: this.actionExpression('delete', { item: itemKey }), colorClasses: danger })}</li>` : ''}${collapse}</ul></div>`;
|
|
290
370
|
}
|
|
291
|
-
rows.push(`<li wire:ignore.self wire:key="${e(livewireKey)}.${e(itemKey)}.data.item" x-data="item(${e(js({ isCollapsed:
|
|
292
|
-
if (index < keys.length - 1 && this._addBetween && this.
|
|
371
|
+
rows.push(`<li wire:ignore.self wire:key="${e(livewireKey)}.${e(itemKey)}.data.item" x-data="item(${e(js({ isCollapsed: this._collapsed, collapseKey: this._persistCollapsed ? `filament:builder:collapsed:${key}-${itemKey}` : null }))})" x-on:builder-expand.window="expandFromEvent" x-on:builder-collapse.window="collapseFromEvent" x-on:expand="expand" x-sortable-item="${e(itemKey)}" class="${cls(['fi-fo-builder-item', { 'fi-fo-builder-item-has-header': this._blockHeaders }])}" x-bind:class="itemClasses">${header}<div x-show="! isCollapsed" class="fi-fo-builder-item-content">${await this.itemSchema(itemKey, item).toHtml()}</div></li>`);
|
|
372
|
+
if (index < keys.length - 1 && this._addBetween && this._addable && this.getBlockPickerBlocks().length && Object.keys(items).length < this._maxItems)
|
|
293
373
|
rows.push(`<li class="fi-fo-builder-add-between-items-ctn"><div class="fi-fo-builder-add-between-items"><div class="fi-fo-builder-block-picker-ctn">${this.blockPickerHtml(itemKey)}</div></div></li>`);
|
|
294
374
|
else if (index < keys.length - 1 && this._labelBetweenItems)
|
|
295
375
|
rows.push(`<li class="fi-fo-builder-label-between-items-ctn"><div class="fi-fo-builder-label-between-items-divider-before"></div><span class="fi-fo-builder-label-between-items">${e(this._labelBetweenItems)}</span><div class="fi-fo-builder-label-between-items-divider-after"></div></li>`);
|
|
@@ -300,7 +380,10 @@ export class Builder extends Field {
|
|
|
300
380
|
return `<span class="fi-sr-only" data-reorder-description="${e(itemKey)}" data-reorder-label="${e(label)}" id="${e(descriptionId(itemKey))}">${e(tr('reordering.position', { label, position: String(index + 1), count: String(keys.length) }))}</span>`;
|
|
301
381
|
}).join('');
|
|
302
382
|
const list = keys.length ? `<ul x-sortable x-ref="reorderItems" data-sortable-animation-duration="300" x-on:end.stop="reorder" class="fi-fo-builder-items">${rows.join('')}</ul>` : '';
|
|
303
|
-
const
|
|
383
|
+
const link = (label, click) => `<span x-on:click="${click}"><button type="button" class="${cls(['fi-ac-link-action', 'fi-link', 'fi-size-sm', ...(colorsOf(this.livewire)?.getComponentClasses('link', 'gray') ?? [])])}"><span class="fi-link-label">${e(label)}</span></button></span>`;
|
|
384
|
+
const collapseAll = this._collapsible ? `<div class="${cls(['fi-fo-builder-actions', { 'fi-hidden': keys.length < 2 }])}">${link(tr('actions.collapse_all.label'), 'collapseAll')}${link(tr('actions.expand_all.label'), 'expandAll')}</div>` : '';
|
|
385
|
+
const picker = this._addable && keys.length < this._maxItems ? this.blockPickerHtml() : '';
|
|
386
|
+
const html = `<div aria-labelledby="${e(id)}-label" id="${e(id)}" role="group" data-reorder-owner="data-reorder-owner" data-reorder-items="${e(JSON.stringify(keys))}" x-load="x-load" x-load-src="${e(asset('builder'))}" x-data="builderFormComponent(${e(js(config))})" class="${cls([this._extraClass, 'fi-fo-builder', { 'fi-collapsible': this._collapsible }])}"><span wire:ignore x-ref="reorderStatus" class="fi-sr-only" role="status" aria-live="polite" aria-atomic="true"></span>${collapseAll}${list}${picker}${descriptions}</div>`;
|
|
304
387
|
return this.wrap(html, { labelAsDiv: true });
|
|
305
388
|
}
|
|
306
389
|
}
|