@navette-js/schemas 1.0.0-beta.4 → 1.0.0-beta.5
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/index.d.ts +44 -0
- package/lib/index.js +80 -9
- package/package.json +2 -2
package/lib/index.d.ts
CHANGED
|
@@ -29,6 +29,13 @@ export declare abstract class Component {
|
|
|
29
29
|
protected _keyName: string | null;
|
|
30
30
|
protected _hidden: Lazy<boolean, ComponentContext>;
|
|
31
31
|
protected _visible: Lazy<boolean, ComponentContext>;
|
|
32
|
+
protected _lazyLoaded: Lazy<boolean, ComponentContext>;
|
|
33
|
+
/**
|
|
34
|
+
* Renders the component's content (its child schema) only when it enters the viewport, in a separate request: a loading
|
|
35
|
+
* indicator is shown meanwhile. The component needs a unique key (a heading, or `key()`).
|
|
36
|
+
*/
|
|
37
|
+
lazyLoaded(condition?: Lazy<boolean, ComponentContext>): this;
|
|
38
|
+
getLazyLoadedCondition(): Lazy<boolean, ComponentContext>;
|
|
32
39
|
protected _columnSpan: ColumnSpan;
|
|
33
40
|
protected _extraAttributes: Record<string, string>;
|
|
34
41
|
protected _columnStart: ColumnPlacement[];
|
|
@@ -104,6 +111,14 @@ export declare class Schema {
|
|
|
104
111
|
private _rootHeadingLevel;
|
|
105
112
|
constructor(livewire: SchemaHost);
|
|
106
113
|
static make(livewire: SchemaHost): Schema;
|
|
114
|
+
private _lazy;
|
|
115
|
+
private _lazyKey;
|
|
116
|
+
/** Renders the components only when the schema enters the viewport (see `Component.lazyLoaded()`). */
|
|
117
|
+
lazyLoaded(condition?: boolean | (() => boolean | Promise<boolean>)): this;
|
|
118
|
+
isLazyLoaded(): Promise<boolean>;
|
|
119
|
+
/** Key identifying a lazy schema (its own key by default; a layout component's child schema uses the component's key). */
|
|
120
|
+
lazyKey(key: string | null): this;
|
|
121
|
+
getLazyKey(): string;
|
|
107
122
|
key(key: string): this;
|
|
108
123
|
components(components: Component[]): this;
|
|
109
124
|
schema(components: Component[]): this;
|
|
@@ -152,6 +167,8 @@ export declare class Schema {
|
|
|
152
167
|
/** Same, without hidden components or their descendants: a field inside a hidden section is neither validated nor saved. */
|
|
153
168
|
getVisibleFlatComponents(): Promise<Component[]>;
|
|
154
169
|
toHtml(): Promise<string>;
|
|
170
|
+
/** Placeholder of a lazy schema: a loading indicator that asks for the schema when it enters the viewport. */
|
|
171
|
+
toDeferredLoadingHtml(): string;
|
|
155
172
|
}
|
|
156
173
|
/** Child schema of a layout component: shares the parent's key and state path, without being a root. */
|
|
157
174
|
export declare function childSchema(parent: Component, components: Component[]): Schema;
|
|
@@ -172,6 +189,9 @@ export declare class Group extends Component {
|
|
|
172
189
|
static make(children?: Component[]): Group;
|
|
173
190
|
schema(components: Component[]): this;
|
|
174
191
|
columns(columns: Record<string, number> | number | null): this;
|
|
192
|
+
/** State path of the components, relative to the container (`address`: their state lives under `address.*`). */
|
|
193
|
+
statePath(path: string | null): this;
|
|
194
|
+
hasStatePath(): boolean;
|
|
175
195
|
getChildSchemas(): Schema[];
|
|
176
196
|
toHtml(): Promise<string>;
|
|
177
197
|
}
|
|
@@ -216,6 +236,7 @@ export declare class Section extends Component {
|
|
|
216
236
|
private _contentBefore;
|
|
217
237
|
private _compact;
|
|
218
238
|
private _secondary;
|
|
239
|
+
private _divided;
|
|
219
240
|
private _contained;
|
|
220
241
|
private _collapsible;
|
|
221
242
|
private _collapsed;
|
|
@@ -242,6 +263,8 @@ export declare class Section extends Component {
|
|
|
242
263
|
compact(condition?: Lazy<boolean, ComponentContext>): this;
|
|
243
264
|
/** Less contrasting background, for a section on a background of the same color (nested sections…). */
|
|
244
265
|
secondary(condition?: Lazy<boolean, ComponentContext>): this;
|
|
266
|
+
/** Dividing lines between the components of the content. */
|
|
267
|
+
divided(condition?: Lazy<boolean, ComponentContext>): this;
|
|
245
268
|
/** `false`: no card around the section. */
|
|
246
269
|
contained(condition?: Lazy<boolean, ComponentContext>): this;
|
|
247
270
|
/** Collapsible by clicking its header; `collapsed()` starts it collapsed. */
|
|
@@ -395,16 +418,37 @@ export interface Renderable {
|
|
|
395
418
|
toHtml(ctx: ComponentContext): Promise<string>;
|
|
396
419
|
}
|
|
397
420
|
/** Row of actions in a schema. */
|
|
421
|
+
/** Content placed around a component: text, components and actions, or a function of the context returning them. */
|
|
422
|
+
export type ComponentSlot = Lazy<string | HtmlString | InlineItem | (string | HtmlString | InlineItem)[] | null, ComponentContext>;
|
|
398
423
|
export declare class Actions extends Component {
|
|
399
424
|
private actions;
|
|
400
425
|
private _fullWidth;
|
|
401
426
|
private _alignment;
|
|
427
|
+
private _label;
|
|
428
|
+
private _beforeLabel;
|
|
429
|
+
private _afterLabel;
|
|
430
|
+
private _aboveContent;
|
|
431
|
+
private _belowContent;
|
|
402
432
|
constructor(actions: Renderable[]);
|
|
403
433
|
static make(actions: Renderable[]): Actions;
|
|
404
434
|
fullWidth(condition?: boolean): this;
|
|
405
435
|
alignment(alignment: string | null): this;
|
|
436
|
+
/** Label shown above the actions, like a field label. */
|
|
437
|
+
label(label: Lazy<string | null, ComponentContext>): this;
|
|
438
|
+
/** Text, components or actions before the label. */
|
|
439
|
+
beforeLabel(content: ComponentSlot): this;
|
|
440
|
+
/** Text, components or actions after the label (a status badge…). */
|
|
441
|
+
afterLabel(content: ComponentSlot): this;
|
|
442
|
+
/** Text, components or actions between the label and the actions. */
|
|
443
|
+
aboveContent(content: ComponentSlot): this;
|
|
444
|
+
/** Text, components or actions below the actions. */
|
|
445
|
+
belowContent(content: ComponentSlot): this;
|
|
446
|
+
/** Actions this component exposes for mounting (its key is sent with each of them). */
|
|
447
|
+
getActions(): Renderable[];
|
|
406
448
|
toHtml(): Promise<string>;
|
|
407
449
|
}
|
|
450
|
+
/** Renders a slot of a component as an inline row: strings become `Text` components. */
|
|
451
|
+
export declare function renderSlot(parent: Component, content: ComponentSlot, mountKey?: string | null): Promise<string>;
|
|
408
452
|
/** Color manager used outside a panel; inside a panel, the host panel's manager takes precedence. */
|
|
409
453
|
export declare const useColorManager: (manager: ColorManager) => void;
|
|
410
454
|
/** Item in an inline row: a schema component or an action. */
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @navette-js/schemas — Schema, Component and the layout components.
|
|
2
2
|
// Each component renders in two steps: async property resolution, then a synchronous EJS view (ADR 0003).
|
|
3
|
-
import { Attributes, HtmlString, cls, e, evaluate, icon, js, renderHook, views, configured, configureUsing } from '@navette-js/support';
|
|
3
|
+
import { Attributes, HtmlString, cls, e, evaluate, icon, js, loadingIndicator, renderHook, views, configured, configureUsing } from '@navette-js/support';
|
|
4
4
|
import { t } from '@navette-js/support/i18n';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
@@ -42,6 +42,13 @@ export class Component {
|
|
|
42
42
|
_keyName = null;
|
|
43
43
|
_hidden = false;
|
|
44
44
|
_visible = true;
|
|
45
|
+
_lazyLoaded = false;
|
|
46
|
+
/**
|
|
47
|
+
* Renders the component's content (its child schema) only when it enters the viewport, in a separate request: a loading
|
|
48
|
+
* indicator is shown meanwhile. The component needs a unique key (a heading, or `key()`).
|
|
49
|
+
*/
|
|
50
|
+
lazyLoaded(condition = true) { this._lazyLoaded = condition; return this; }
|
|
51
|
+
getLazyLoadedCondition() { return this._lazyLoaded; }
|
|
45
52
|
_columnSpan = { default: 1 };
|
|
46
53
|
_extraAttributes = {};
|
|
47
54
|
_columnStart = [];
|
|
@@ -152,6 +159,14 @@ export class Schema {
|
|
|
152
159
|
_rootHeadingLevel = 2;
|
|
153
160
|
constructor(livewire) { this.livewire = livewire; }
|
|
154
161
|
static make(livewire) { return configured(new Schema(livewire)); }
|
|
162
|
+
_lazy = false;
|
|
163
|
+
_lazyKey = null;
|
|
164
|
+
/** Renders the components only when the schema enters the viewport (see `Component.lazyLoaded()`). */
|
|
165
|
+
lazyLoaded(condition = true) { this._lazy = condition; return this; }
|
|
166
|
+
async isLazyLoaded() { return Boolean(typeof this._lazy === 'function' ? await this._lazy() : this._lazy); }
|
|
167
|
+
/** Key identifying a lazy schema (its own key by default; a layout component's child schema uses the component's key). */
|
|
168
|
+
lazyKey(key) { this._lazyKey = key; return this; }
|
|
169
|
+
getLazyKey() { return this._lazyKey ?? this._key; }
|
|
155
170
|
key(key) { this._key = key; return this; }
|
|
156
171
|
components(components) { this._components = components; for (const c of components)
|
|
157
172
|
c.container = this; return this; }
|
|
@@ -214,13 +229,16 @@ export class Schema {
|
|
|
214
229
|
return out;
|
|
215
230
|
}
|
|
216
231
|
async toHtml() {
|
|
232
|
+
const isLazy = await this.isLazyLoaded();
|
|
233
|
+
if (isLazy && typeof this.livewire.isDeferredSchemaLoaded === 'function' && !(await this.livewire.isDeferredSchemaLoaded(this)))
|
|
234
|
+
return this.toDeferredLoadingHtml();
|
|
217
235
|
const withVisibility = await Promise.all(this._components.map(async (c) => [c, await c.isVisible()]));
|
|
218
236
|
if (!withVisibility.some(([, v]) => v))
|
|
219
237
|
return '';
|
|
220
238
|
const attributes = new Attributes(this._extraClass ? { class: this._extraClass } : {})
|
|
221
239
|
.when(!this._inline, (a) => a.grid(this._columns))
|
|
222
240
|
.merge({
|
|
223
|
-
'wire:partial': this._partialName ?? (this._partial ? `schema.${this._key}` : null),
|
|
241
|
+
'wire:partial': this._partialName ?? (this._partial ? `schema.${this._key}` : isLazy ? `schema.${this.getLazyKey()}` : null),
|
|
224
242
|
'x-data': this._root ? `filamentSchema({ livewireId: ${js(this.livewire.id)}, schemaKey: ${js(this._key)} })` : null,
|
|
225
243
|
'x-on:form-validation-error.window': this._root ? 'handleFormValidationError' : null,
|
|
226
244
|
'x-on:reset-schema-component-state.window': this._root ? 'handleClientSideStateReset' : null,
|
|
@@ -235,11 +253,21 @@ export class Schema {
|
|
|
235
253
|
}
|
|
236
254
|
return `<div ${attributes.toHtml()}>${parts.join('')}</div>`;
|
|
237
255
|
}
|
|
256
|
+
/** Placeholder of a lazy schema: a loading indicator that asks for the schema when it enters the viewport. */
|
|
257
|
+
toDeferredLoadingHtml() {
|
|
258
|
+
const key = this.getLazyKey();
|
|
259
|
+
const attributes = new Attributes(this._extraClass ? { class: this._extraClass } : {}).merge({ 'wire:partial': `schema.${key}`, 'x-data': `filamentSchema({ livewireId: ${js(this.livewire.id)}, schemaKey: ${js(key)}, isLoadingDeferred: true })`, role: 'status', 'aria-busy': 'true' }).class(['fi-sc', 'fi-sc-loading']);
|
|
260
|
+
return `<div ${attributes.toHtml()}>${loadingIndicator(new Attributes(), 'lg')}<span class="fi-sr-only">${e(t('filament::components/loading-section.label'))}</span></div>`;
|
|
261
|
+
}
|
|
238
262
|
}
|
|
239
263
|
/** Child schema of a layout component: shares the parent's key and state path, without being a root. */
|
|
240
264
|
export function childSchema(parent, components) {
|
|
241
265
|
const c = parent.container;
|
|
242
|
-
|
|
266
|
+
const schema = new Schema(c.livewire).key(c.getKey()).statePath(c.getStatePath() || null).operation(c.getOperation()).record(c.getRecord()).root(false).parentComponent(parent).components(components);
|
|
267
|
+
const lazy = parent.getLazyLoadedCondition();
|
|
268
|
+
if (lazy !== false)
|
|
269
|
+
schema.lazyLoaded(typeof lazy === 'function' ? () => evaluate(lazy, parent.context()) : lazy).lazyKey(parent.getKey());
|
|
270
|
+
return schema;
|
|
243
271
|
}
|
|
244
272
|
/** Stateful group: its children live under `<container path>.<name>` (used by filter forms). */
|
|
245
273
|
export class StatefulGroup extends Component {
|
|
@@ -261,7 +289,15 @@ export class Group extends Component {
|
|
|
261
289
|
static make(children = []) { return configured(new Group(children)); }
|
|
262
290
|
schema(components) { this.children = components; return this; }
|
|
263
291
|
columns(columns) { this._columns = columns; return this; }
|
|
264
|
-
|
|
292
|
+
/** State path of the components, relative to the container (`address`: their state lives under `address.*`). */
|
|
293
|
+
statePath(path) { this._name = path; return this; }
|
|
294
|
+
hasStatePath() { return this._name !== null; }
|
|
295
|
+
getChildSchemas() {
|
|
296
|
+
const schema = childSchema(this, this.children).columns(this._columns);
|
|
297
|
+
if (this.hasStatePath())
|
|
298
|
+
schema.key(this.getKey()).statePath(this.getStatePath());
|
|
299
|
+
return [schema];
|
|
300
|
+
}
|
|
265
301
|
async toHtml() {
|
|
266
302
|
const attributes = new Attributes().merge({ ...this._extraAttributes }).toHtml();
|
|
267
303
|
return `<div${attributes ? ` ${attributes}` : ''}>${await this.getChildSchemas()[0].toHtml()}</div>`;
|
|
@@ -318,6 +354,7 @@ export class Section extends Component {
|
|
|
318
354
|
_contentBefore = false;
|
|
319
355
|
_compact = false;
|
|
320
356
|
_secondary = false;
|
|
357
|
+
_divided = false;
|
|
321
358
|
_contained = true;
|
|
322
359
|
_collapsible = null;
|
|
323
360
|
_collapsed = false;
|
|
@@ -344,6 +381,8 @@ export class Section extends Component {
|
|
|
344
381
|
compact(condition = true) { this._compact = condition; return this; }
|
|
345
382
|
/** Less contrasting background, for a section on a background of the same color (nested sections…). */
|
|
346
383
|
secondary(condition = true) { this._secondary = condition; return this; }
|
|
384
|
+
/** Dividing lines between the components of the content. */
|
|
385
|
+
divided(condition = true) { this._divided = condition; return this; }
|
|
347
386
|
/** `false`: no card around the section. */
|
|
348
387
|
contained(condition = true) { this._contained = condition; return this; }
|
|
349
388
|
/** Collapsible by clicking its header; `collapsed()` starts it collapsed. */
|
|
@@ -428,6 +467,7 @@ export class Section extends Component {
|
|
|
428
467
|
'fi-compact': Boolean(await evaluate(this._compact, ctx)),
|
|
429
468
|
'fi-collapsible': isCollapsible,
|
|
430
469
|
'fi-collapsed': isCollapsible && isCollapsed,
|
|
470
|
+
'fi-divided': Boolean(await evaluate(this._divided, ctx)),
|
|
431
471
|
'fi-secondary': Boolean(await evaluate(this._secondary, ctx)),
|
|
432
472
|
}]);
|
|
433
473
|
let header = '';
|
|
@@ -652,27 +692,57 @@ export class Form extends Component {
|
|
|
652
692
|
return `<form ${attributes.toHtml()}>${await content.toHtml()}${await footer.toHtml()}</form>`;
|
|
653
693
|
}
|
|
654
694
|
}
|
|
655
|
-
/** Row of actions in a schema. */
|
|
656
695
|
export class Actions extends Component {
|
|
657
696
|
actions;
|
|
658
697
|
_fullWidth = false;
|
|
659
698
|
_alignment = null;
|
|
699
|
+
_label = null;
|
|
700
|
+
_beforeLabel = null;
|
|
701
|
+
_afterLabel = null;
|
|
702
|
+
_aboveContent = null;
|
|
703
|
+
_belowContent = null;
|
|
660
704
|
constructor(actions) { super(); this.actions = actions; }
|
|
661
705
|
static make(actions) { return configured(new Actions(actions)); }
|
|
662
706
|
fullWidth(condition = true) { this._fullWidth = condition; return this; }
|
|
663
707
|
alignment(alignment) { this._alignment = alignment; return this; }
|
|
708
|
+
/** Label shown above the actions, like a field label. */
|
|
709
|
+
label(label) { this._label = label; return this; }
|
|
710
|
+
/** Text, components or actions before the label. */
|
|
711
|
+
beforeLabel(content) { this._beforeLabel = content; return this; }
|
|
712
|
+
/** Text, components or actions after the label (a status badge…). */
|
|
713
|
+
afterLabel(content) { this._afterLabel = content; return this; }
|
|
714
|
+
/** Text, components or actions between the label and the actions. */
|
|
715
|
+
aboveContent(content) { this._aboveContent = content; return this; }
|
|
716
|
+
/** Text, components or actions below the actions. */
|
|
717
|
+
belowContent(content) { this._belowContent = content; return this; }
|
|
718
|
+
/** Actions this component exposes for mounting (its key is sent with each of them). */
|
|
719
|
+
getActions() { return this.actions; }
|
|
664
720
|
async toHtml() {
|
|
665
721
|
const ctx = this.context();
|
|
722
|
+
const key = this.getKey();
|
|
723
|
+
const mountContext = key !== null ? { ...(this.livewire?.getActionMountContext?.() ?? {}), schemaComponent: key } : null;
|
|
666
724
|
const visible = [];
|
|
667
725
|
for (const a of this.actions)
|
|
668
726
|
if (await a.isVisible(ctx))
|
|
669
|
-
visible.push(await a.toHtml(ctx));
|
|
670
|
-
|
|
727
|
+
visible.push(await a.toHtml(ctx, mountContext));
|
|
728
|
+
const label = await evaluate(this._label, ctx);
|
|
729
|
+
const slot = (content) => renderSlot(this, content, key);
|
|
730
|
+
const labelHtml = isBlank(label) ? '' : `<div class="fi-sc-actions-label-ctn">${await slot(this._beforeLabel)}<div class="fi-sc-actions-label">${e(label)}</div>${await slot(this._afterLabel)}</div>`;
|
|
731
|
+
const above = await slot(this._aboveContent), below = await slot(this._belowContent);
|
|
732
|
+
if (!visible.length && !labelHtml && !above && !below)
|
|
671
733
|
return '';
|
|
672
734
|
const inner = cls(['fi-ac', { 'fi-width-full': this._fullWidth }, this._alignment ? `fi-align-${this._alignment}` : null]);
|
|
673
|
-
return `<div ${new Attributes().merge({ id: this.
|
|
735
|
+
return `<div ${new Attributes().merge({ id: key, ...this._extraAttributes }).class(['fi-sc-actions']).toHtml()}>${labelHtml}${above}${visible.length ? `<div class="${inner}">${visible.join('')}</div>` : ''}${below}</div>`;
|
|
674
736
|
}
|
|
675
737
|
}
|
|
738
|
+
/** Renders a slot of a component as an inline row: strings become `Text` components. */
|
|
739
|
+
export async function renderSlot(parent, content, mountKey = null) {
|
|
740
|
+
const value = await evaluate(content, parent.context());
|
|
741
|
+
if (value === null || value === undefined || value === '')
|
|
742
|
+
return '';
|
|
743
|
+
const items = (Array.isArray(value) ? value : [value]).map((item) => (typeof item === 'string' || item instanceof HtmlString ? Text.make(item) : item));
|
|
744
|
+
return renderInlineItems(parent, items, { mountKey, actionView: 'link' });
|
|
745
|
+
}
|
|
676
746
|
let colorManager = null;
|
|
677
747
|
/** Color manager used outside a panel; inside a panel, the host panel's manager takes precedence. */
|
|
678
748
|
export const useColorManager = (manager) => { colorManager = manager; };
|
|
@@ -917,7 +987,8 @@ export class Image extends Component {
|
|
|
917
987
|
async toHtml() {
|
|
918
988
|
const ctx = this.context();
|
|
919
989
|
const url = String((await evaluate(this._url, ctx)) ?? '');
|
|
920
|
-
|
|
990
|
+
// Relative and http(s) URLs, or an inline image (`data:image/…;base64,`: scripts never run in an <img>).
|
|
991
|
+
if (!/^(\/(?!\/)|https?:\/\/|data:image\/(png|jpeg|gif|webp|svg\+xml);base64,[A-Za-z0-9+/=]+$)/i.test(url))
|
|
921
992
|
return '';
|
|
922
993
|
const tooltip = tooltipExpression(await evaluate(this._tooltip, ctx));
|
|
923
994
|
const attributes = new Attributes(this._extraAttributes)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navette-js/schemas",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
4
|
"description": "Layout components and schemas for Navette admin panels.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"node"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@navette-js/support": "1.0.0-beta.
|
|
41
|
+
"@navette-js/support": "1.0.0-beta.5"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public",
|