@navette-js/forms 1.0.0-beta.1 → 1.0.0-beta.3
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/README.md +2 -2
- package/lib/builder.d.ts +23 -23
- package/lib/builder.js +29 -32
- package/lib/index.d.ts +170 -175
- package/lib/index.js +209 -219
- package/package.json +5 -6
- package/views/components/field-wrapper.ejs +3 -3
- package/views/components/rich-editor.ejs +2 -3
- package/views/components/text-input.ejs +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @navette-js/forms
|
|
2
2
|
|
|
3
|
-
Form fields for Navette
|
|
3
|
+
Form fields and validation for Navette admin panels.
|
|
4
4
|
|
|
5
|
-
Part of [Navette](https://github.com/NavetteJS/navette),
|
|
5
|
+
Part of [Navette](https://github.com/NavetteJS/navette), admin panels for Node.js. Start a project with `npm create navette@latest`, or install the framework with `npm install @navette-js/panels`.
|
|
6
6
|
|
|
7
7
|
**Beta:** the API may still change between beta releases.
|
|
8
8
|
|
package/lib/builder.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ 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
4
|
import { Field } 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. */
|
|
8
8
|
export declare const shortKey: () => string;
|
|
9
|
-
/**
|
|
9
|
+
/** Writes a value at a state path of the host component. */
|
|
10
10
|
export declare function writeState(target: any, segments: string[], value: unknown): void;
|
|
11
|
-
/**
|
|
11
|
+
/** Icon button for a component action: the icon, then a loading indicator. */
|
|
12
12
|
export declare function iconActionButton(options: {
|
|
13
13
|
label: string;
|
|
14
14
|
icon: string;
|
|
@@ -18,14 +18,14 @@ export declare function iconActionButton(options: {
|
|
|
18
18
|
disabled?: boolean;
|
|
19
19
|
inHeader?: boolean;
|
|
20
20
|
}): string;
|
|
21
|
-
/**
|
|
21
|
+
/** Color manager of the host's panel (pages and widgets expose it through `panel`). */
|
|
22
22
|
export declare const colorsOf: (livewire: unknown) => ColorManager | null;
|
|
23
23
|
type BlockContext = {
|
|
24
24
|
builder: Builder;
|
|
25
25
|
itemKey: string;
|
|
26
26
|
data: Record<string, unknown>;
|
|
27
27
|
};
|
|
28
|
-
/**
|
|
28
|
+
/** Item type of a Builder. */
|
|
29
29
|
export declare class Block {
|
|
30
30
|
readonly name: string;
|
|
31
31
|
private _label;
|
|
@@ -36,9 +36,9 @@ export declare class Block {
|
|
|
36
36
|
static make(name: string): Block;
|
|
37
37
|
label(label: string | null): this;
|
|
38
38
|
icon(name: string | null): this;
|
|
39
|
-
/**
|
|
39
|
+
/** Item components: a function called for each item (with its current data). */
|
|
40
40
|
schema(factory: (ctx: BlockContext) => Component[]): this;
|
|
41
|
-
/**
|
|
41
|
+
/** Data of a new item when it cannot be derived from the fields' defaults (fields depending on another). */
|
|
42
42
|
defaultData(factory: (builder: Builder) => Record<string, unknown>): this;
|
|
43
43
|
getLabel(): string;
|
|
44
44
|
getIcon(): string | null;
|
|
@@ -73,53 +73,53 @@ export declare class Builder extends Field {
|
|
|
73
73
|
blockPickerColumns(columns?: number | null): this;
|
|
74
74
|
blockPickerWidth(width: string | null): this;
|
|
75
75
|
labelBetweenItems(label: string | null): this;
|
|
76
|
-
/**
|
|
76
|
+
/** Without a header, an item only shows its content: its actions are then up to the block. */
|
|
77
77
|
blockHeaders(condition?: boolean): this;
|
|
78
78
|
cloneable(condition?: boolean): this;
|
|
79
|
-
/**
|
|
79
|
+
/** Item number after the block label (on by default). */
|
|
80
80
|
blockNumbers(condition?: boolean): this;
|
|
81
|
-
/**
|
|
81
|
+
/** Block icon in the item header (off by default). */
|
|
82
82
|
blockIcons(condition?: boolean): this;
|
|
83
|
-
/**
|
|
83
|
+
/** Tag of the header label (`h2` by default). */
|
|
84
84
|
blockLabelHeadingTag(tag: 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'): this;
|
|
85
|
-
/**
|
|
85
|
+
/** "Insert between blocks" picker between two items (on by default). */
|
|
86
86
|
addBetweenAction(condition?: boolean): this;
|
|
87
87
|
addBetweenActionLabel(label: string | null): this;
|
|
88
88
|
addActionLabel(label: string | null): this;
|
|
89
89
|
addActionIcon(name: string | null): this;
|
|
90
90
|
addActionAlignment(alignment: 'start' | 'center' | 'end' | null): this;
|
|
91
91
|
maxItems(count: number): this;
|
|
92
|
-
/**
|
|
92
|
+
/** Class added to the root (e.g. `fi-fo-builder-not-contained`). */
|
|
93
93
|
extraClass(name: string | null): this;
|
|
94
94
|
generateUuidUsing(generator: () => string): this;
|
|
95
95
|
getBlocks(): Block[];
|
|
96
96
|
getBlock(name: unknown): Block | undefined;
|
|
97
|
-
/**
|
|
97
|
+
/** Blocks offered in the picker (a subclass may remove some). */
|
|
98
98
|
getBlockPickerBlocks(): Block[];
|
|
99
|
-
/**
|
|
99
|
+
/** Well-formed items only: simple key, declared type, object data. Anything else the browser sends is ignored. */
|
|
100
100
|
getItems(): Record<string, Item>;
|
|
101
101
|
protected setItems(items: Record<string, Item>): void;
|
|
102
102
|
getDefaultState(): unknown;
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
104
|
+
* Stored value (a `[{ type, data }]` list, possibly as JSON) to form state (a map by key).
|
|
105
|
+
* Each block's fields hydrate their own value.
|
|
106
106
|
*/
|
|
107
107
|
hydrateState(value: unknown): unknown;
|
|
108
108
|
private _storedAsList;
|
|
109
|
-
/** `false
|
|
109
|
+
/** `false`: the stored value stays the keyed map of the state (filters) instead of a `[{ type, data }]` list. */
|
|
110
110
|
storedAsList(condition?: boolean): this;
|
|
111
111
|
finalizeDehydratedState(state: unknown): unknown;
|
|
112
112
|
dehydrateState(): unknown;
|
|
113
|
-
/**
|
|
113
|
+
/** Item schema: key `<builder>.<item>.data`, state at the same path. */
|
|
114
114
|
protected itemSchema(itemKey: string, item: Item): Schema;
|
|
115
115
|
getChildSchemas(): Schema[];
|
|
116
116
|
protected newItem(block: Block): Item;
|
|
117
|
-
/**
|
|
117
|
+
/** A subclass may refuse adding (a rule cap, for instance). */
|
|
118
118
|
protected canAdd(_block: Block): boolean;
|
|
119
119
|
getActions(): Action[];
|
|
120
|
-
/**
|
|
120
|
+
/** Copy of an item; a subclass renews the keys of nested items. */
|
|
121
121
|
protected cloneItem(item: Item): Item;
|
|
122
|
-
/**
|
|
122
|
+
/** `mountAction(...)` expression for an action of this component. */
|
|
123
123
|
actionExpression(action: string, args: Record<string, unknown> | null): string;
|
|
124
124
|
private blockPickerHtml;
|
|
125
125
|
private _labelText;
|
package/lib/builder.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// @navette-js/forms/builder —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// en-têtes de bloc (libellé et actions). Non portés : aperçus de bloc, repli, recherche dans le sélecteur, ajout entre éléments.
|
|
1
|
+
// @navette-js/forms/builder — a list of items, each one a "block" picked from a menu.
|
|
2
|
+
// The state is a map `item key → { type, data }`; key order is display order.
|
|
3
|
+
// Not supported yet: block previews, collapsing, searching the block picker.
|
|
5
4
|
import { Attributes, cls, e, icon, js, loadingIndicator, views, configured } from '@navette-js/support';
|
|
6
5
|
import { t } from '@navette-js/support/i18n';
|
|
7
6
|
import { Action } from '@navette-js/actions';
|
|
@@ -10,11 +9,11 @@ import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
|
10
9
|
import { Field, fillSchema } from "./index.js";
|
|
11
10
|
const asset = (name) => views.render('filament-forms::asset', { path: name }).trim();
|
|
12
11
|
const ITEM_KEY = /^[A-Za-z0-9_-]{1,64}$/;
|
|
13
|
-
/**
|
|
12
|
+
/** Whether an item key is acceptable: keys come from the browser. */
|
|
14
13
|
export const isItemKey = (key) => ITEM_KEY.test(key) && key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
|
|
15
|
-
/**
|
|
14
|
+
/** Random four-character alphanumeric key. */
|
|
16
15
|
export const shortKey = () => Array.from(randomBytes(4), (b) => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[b % 62]).join('');
|
|
17
|
-
/**
|
|
16
|
+
/** Writes a value at a state path of the host component. */
|
|
18
17
|
export function writeState(target, segments, value) {
|
|
19
18
|
let node = target;
|
|
20
19
|
for (const key of segments.slice(0, -1)) {
|
|
@@ -24,9 +23,9 @@ export function writeState(target, segments, value) {
|
|
|
24
23
|
}
|
|
25
24
|
node[segments[segments.length - 1]] = value;
|
|
26
25
|
}
|
|
27
|
-
/**
|
|
26
|
+
/** Icon button for a component action: the icon, then a loading indicator. */
|
|
28
27
|
export function iconActionButton(options) {
|
|
29
|
-
//
|
|
28
|
+
// Item header buttons put `tabindex` before `type`, and the class before the extra attributes.
|
|
30
29
|
const { tabindex = null, ...extra } = options.attributes ?? {};
|
|
31
30
|
const attributes = new Attributes(options.inHeader ? {
|
|
32
31
|
title: options.label, 'aria-disabled': options.disabled ? 'true' : null, 'aria-label': options.label, disabled: options.disabled ? 'disabled' : null, tabindex, type: 'button',
|
|
@@ -37,9 +36,9 @@ export function iconActionButton(options) {
|
|
|
37
36
|
}).class(['fi-ac-icon-btn-action', 'fi-icon-btn', { 'fi-disabled': options.disabled === true }, 'fi-size-sm', ...(options.colorClasses ?? [])]);
|
|
38
37
|
return `<button ${attributes.toHtml()}>${icon(options.icon, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': options.expression }) })}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': options.expression }))}</button>`;
|
|
39
38
|
}
|
|
40
|
-
/**
|
|
39
|
+
/** Color manager of the host's panel (pages and widgets expose it through `panel`). */
|
|
41
40
|
export const colorsOf = (livewire) => livewire?.panel?.colors ?? null;
|
|
42
|
-
/**
|
|
41
|
+
/** Item type of a Builder. */
|
|
43
42
|
export class Block {
|
|
44
43
|
name;
|
|
45
44
|
_label = null;
|
|
@@ -50,16 +49,16 @@ export class Block {
|
|
|
50
49
|
static make(name) { return configured(new Block(name)); }
|
|
51
50
|
label(label) { this._label = label; return this; }
|
|
52
51
|
icon(name) { this._icon = name; return this; }
|
|
53
|
-
/**
|
|
52
|
+
/** Item components: a function called for each item (with its current data). */
|
|
54
53
|
schema(factory) { this._schema = factory; return this; }
|
|
55
|
-
/**
|
|
54
|
+
/** Data of a new item when it cannot be derived from the fields' defaults (fields depending on another). */
|
|
56
55
|
defaultData(factory) { this._defaults = factory; return this; }
|
|
57
56
|
getLabel() { return this._label ?? this.name.replace(/[_-]+/g, ' ').replace(/^./, (c) => c.toUpperCase()); }
|
|
58
57
|
getIcon() { return this._icon; }
|
|
59
58
|
getComponents(ctx) { return this._schema(ctx); }
|
|
60
59
|
getDefaults(builder) { return this._defaults ? this._defaults(builder) : null; }
|
|
61
60
|
}
|
|
62
|
-
/**
|
|
61
|
+
/** Precomputed block picker key hashes for common `width|placement|has blocks|searchable` signatures; others hash the signature itself. */
|
|
63
62
|
const PICKER_HASHES = { 'fi-width-md|bottom-start|1|0': '8b2521a32ddb82abb3a5f4fd214c90b1', '||1|0': '9c288f69c73367423cc1eb48b3e9a82b' };
|
|
64
63
|
export class Builder extends Field {
|
|
65
64
|
_blocks = [];
|
|
@@ -85,31 +84,30 @@ export class Builder extends Field {
|
|
|
85
84
|
blockPickerColumns(columns = 2) { this._pickerColumns = columns; return this; }
|
|
86
85
|
blockPickerWidth(width) { this._pickerWidth = width; return this; }
|
|
87
86
|
labelBetweenItems(label) { this._labelBetweenItems = label; return this; }
|
|
88
|
-
/**
|
|
87
|
+
/** Without a header, an item only shows its content: its actions are then up to the block. */
|
|
89
88
|
blockHeaders(condition = true) { this._blockHeaders = condition; return this; }
|
|
90
89
|
cloneable(condition = true) { this._cloneable = condition; return this; }
|
|
91
|
-
/**
|
|
90
|
+
/** Item number after the block label (on by default). */
|
|
92
91
|
blockNumbers(condition = true) { this._blockNumbers = condition; return this; }
|
|
93
|
-
/**
|
|
92
|
+
/** Block icon in the item header (off by default). */
|
|
94
93
|
blockIcons(condition = true) { this._blockIcons = condition; return this; }
|
|
95
|
-
/**
|
|
94
|
+
/** Tag of the header label (`h2` by default). */
|
|
96
95
|
blockLabelHeadingTag(tag) { this._headingTag = tag; return this; }
|
|
97
|
-
/**
|
|
96
|
+
/** "Insert between blocks" picker between two items (on by default). */
|
|
98
97
|
addBetweenAction(condition = true) { this._addBetween = condition; return this; }
|
|
99
98
|
addBetweenActionLabel(label) { this._addBetweenLabel = label; return this; }
|
|
100
99
|
addActionLabel(label) { this._addLabel = label; return this; }
|
|
101
100
|
addActionIcon(name) { this._addIcon = name; return this; }
|
|
102
101
|
addActionAlignment(alignment) { this._addAlignment = alignment; return this; }
|
|
103
102
|
maxItems(count) { this._maxItems = count; return this; }
|
|
104
|
-
/**
|
|
103
|
+
/** Class added to the root (e.g. `fi-fo-builder-not-contained`). */
|
|
105
104
|
extraClass(name) { this._extraClass = name; return this; }
|
|
106
105
|
generateUuidUsing(generator) { this._uuid = generator; return this; }
|
|
107
106
|
getBlocks() { return (this._resolvedBlocks ??= typeof this._blocks === 'function' ? this._blocks() : this._blocks); }
|
|
108
107
|
getBlock(name) { return typeof name === 'string' ? this.getBlocks().find((block) => block.name === name) : undefined; }
|
|
109
|
-
/**
|
|
108
|
+
/** Blocks offered in the picker (a subclass may remove some). */
|
|
110
109
|
getBlockPickerBlocks() { return this.getBlocks(); }
|
|
111
|
-
|
|
112
|
-
/** Éléments bien formés : clé simple, type déclaré, données en objet. Le reste de ce qu'envoie le navigateur est ignoré. */
|
|
110
|
+
/** Well-formed items only: simple key, declared type, object data. Anything else the browser sends is ignored. */
|
|
113
111
|
getItems() {
|
|
114
112
|
const state = this.context().state;
|
|
115
113
|
if (!state || typeof state !== 'object' || Array.isArray(state))
|
|
@@ -125,8 +123,8 @@ export class Builder extends Field {
|
|
|
125
123
|
setItems(items) { writeState(this.livewire, this.getStatePath().split('.'), items); }
|
|
126
124
|
getDefaultState() { return {}; }
|
|
127
125
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
126
|
+
* Stored value (a `[{ type, data }]` list, possibly as JSON) to form state (a map by key).
|
|
127
|
+
* Each block's fields hydrate their own value.
|
|
130
128
|
*/
|
|
131
129
|
hydrateState(value) {
|
|
132
130
|
if (typeof value === 'string') {
|
|
@@ -156,7 +154,7 @@ export class Builder extends Field {
|
|
|
156
154
|
return items;
|
|
157
155
|
}
|
|
158
156
|
_storedAsList = true;
|
|
159
|
-
/** `false
|
|
157
|
+
/** `false`: the stored value stays the keyed map of the state (filters) instead of a `[{ type, data }]` list. */
|
|
160
158
|
storedAsList(condition = true) { this._storedAsList = condition; return this; }
|
|
161
159
|
finalizeDehydratedState(state) {
|
|
162
160
|
if (!this._storedAsList || !state || typeof state !== 'object' || Array.isArray(state))
|
|
@@ -164,7 +162,7 @@ export class Builder extends Field {
|
|
|
164
162
|
return Object.values(state);
|
|
165
163
|
}
|
|
166
164
|
dehydrateState() { return Object.fromEntries(Object.entries(this.getItems()).map(([key, item]) => [key, { type: item.type, data: {} }])); }
|
|
167
|
-
/**
|
|
165
|
+
/** Item schema: key `<builder>.<item>.data`, state at the same path. */
|
|
168
166
|
itemSchema(itemKey, item) {
|
|
169
167
|
const container = this.container;
|
|
170
168
|
return new Schema(container.livewire).key(`${this.getKey()}.${itemKey}.data`).statePath(`${this.getStatePath()}.${itemKey}.data`).operation(container.getOperation()).record(container.getRecord()).root(false)
|
|
@@ -180,9 +178,8 @@ export class Builder extends Field {
|
|
|
180
178
|
fillSchema(this.itemSchema('new', item), data);
|
|
181
179
|
return item;
|
|
182
180
|
}
|
|
183
|
-
/**
|
|
181
|
+
/** A subclass may refuse adding (a rule cap, for instance). */
|
|
184
182
|
canAdd(_block) { return Object.keys(this.getItems()).length < this._maxItems; }
|
|
185
|
-
// --- actions du composant
|
|
186
183
|
getActions() {
|
|
187
184
|
const insert = (items, after, key, item) => {
|
|
188
185
|
if (after === null || !Object.hasOwn(items, after))
|
|
@@ -225,7 +222,7 @@ export class Builder extends Field {
|
|
|
225
222
|
}),
|
|
226
223
|
Action.make('moveUp').action(move(-1)),
|
|
227
224
|
Action.make('moveDown').action(move(1)),
|
|
228
|
-
//
|
|
225
|
+
// Drag and drop: the new order is only accepted if it is a permutation of the existing keys.
|
|
229
226
|
Action.make('reorder').action(({ arguments: args }) => {
|
|
230
227
|
const items = this.getItems();
|
|
231
228
|
const keys = Object.keys(items);
|
|
@@ -236,9 +233,9 @@ export class Builder extends Field {
|
|
|
236
233
|
}),
|
|
237
234
|
];
|
|
238
235
|
}
|
|
239
|
-
/**
|
|
236
|
+
/** Copy of an item; a subclass renews the keys of nested items. */
|
|
240
237
|
cloneItem(item) { return JSON.parse(JSON.stringify(item)); }
|
|
241
|
-
/**
|
|
238
|
+
/** `mountAction(...)` expression for an action of this component. */
|
|
242
239
|
actionExpression(action, args) {
|
|
243
240
|
const context = { ...(this.livewire.getActionMountContext?.() ?? {}), schemaComponent: this.getKey() };
|
|
244
241
|
return `mountAction(${js(action)}, ${args ? js(args) : '{}'}, ${js(context)})`;
|