@navette-js/schemas 1.0.0-beta.3 → 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 +211 -19
- package/lib/index.js +426 -76
- package/package.json +2 -2
- package/views/component.ejs +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ export type ComponentContext = {
|
|
|
15
15
|
};
|
|
16
16
|
type SpanValue = number | string | ((ctx: ComponentContext) => number | string | Promise<number | string>);
|
|
17
17
|
type ColumnSpan = Record<string, SpanValue> | number | string;
|
|
18
|
+
type GridValue = number | null | ((ctx: ComponentContext) => number | null | Promise<number | null>);
|
|
19
|
+
/** Column start or order: a number (from `lg`), an object of breakpoints, or a function returning either (a number then applies to every screen size). */
|
|
20
|
+
export type ColumnPlacement = Record<string, GridValue> | number | null | ((ctx: ComponentContext) => Record<string, number | null> | number | null | Promise<Record<string, number | null> | number | null>);
|
|
18
21
|
export declare abstract class Component {
|
|
19
22
|
private _grow;
|
|
20
23
|
/** Inside a Flex: `false` keeps the natural width instead of filling the available space. */
|
|
@@ -26,9 +29,20 @@ export declare abstract class Component {
|
|
|
26
29
|
protected _keyName: string | null;
|
|
27
30
|
protected _hidden: Lazy<boolean, ComponentContext>;
|
|
28
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>;
|
|
29
39
|
protected _columnSpan: ColumnSpan;
|
|
30
40
|
protected _extraAttributes: Record<string, string>;
|
|
31
|
-
protected _columnStart:
|
|
41
|
+
protected _columnStart: ColumnPlacement[];
|
|
42
|
+
protected _columnOrder: ColumnPlacement[];
|
|
43
|
+
protected _gridContainer: Lazy<boolean, ComponentContext>;
|
|
44
|
+
protected _dense: boolean | null;
|
|
45
|
+
protected _gap: boolean | null;
|
|
32
46
|
container: Schema;
|
|
33
47
|
hidden(condition?: Lazy<boolean, ComponentContext>): this;
|
|
34
48
|
visible(condition?: Lazy<boolean, ComponentContext>): this;
|
|
@@ -39,8 +53,27 @@ export declare abstract class Component {
|
|
|
39
53
|
visibleOn(operations: string | string[]): this;
|
|
40
54
|
/** A number applies from the `lg` breakpoint; an object replaces the default entirely. Values may be closures. */
|
|
41
55
|
columnSpan(span: ColumnSpan): this;
|
|
42
|
-
/** Starting grid column
|
|
43
|
-
columnStart(start:
|
|
56
|
+
/** Starting grid column: a number applies from the `lg` breakpoint; an object sets it per breakpoint. Successive calls merge; `null` resets it. */
|
|
57
|
+
columnStart(start: ColumnPlacement): this;
|
|
58
|
+
/** Visual order in the grid, without changing the markup order: a number applies from `lg`; an object sets it per breakpoint. */
|
|
59
|
+
columnOrder(order: ColumnPlacement): this;
|
|
60
|
+
/** Makes the component a CSS container, so that grids inside it may use container breakpoints (`@md`, `@xl`…). */
|
|
61
|
+
gridContainer(condition?: Lazy<boolean, ComponentContext>): this;
|
|
62
|
+
/** Tighter spacing between the components of its child schemas (inherited by nested layouts). */
|
|
63
|
+
dense(condition?: boolean | null): this;
|
|
64
|
+
/** `false`: no space between the components of its child schemas (inherited by nested layouts). */
|
|
65
|
+
gap(condition?: boolean | null): this;
|
|
66
|
+
isDense(): boolean;
|
|
67
|
+
hasGap(): boolean;
|
|
68
|
+
/** Heading level of the component's own headings (`index` for a second heading). */
|
|
69
|
+
getHeadingLevel(index?: number): number;
|
|
70
|
+
/** `h1`…`h6`, or `p` beyond level 6. */
|
|
71
|
+
getHeadingTag(index?: number): string;
|
|
72
|
+
/** Number of heading levels the component adds above its child schemas (1 for a section with a heading). */
|
|
73
|
+
getHeadingsCount(): number;
|
|
74
|
+
getChildSchemaRootHeadingLevel(): number;
|
|
75
|
+
/** Full grid width on every screen size (`columnSpan('full')` only applies from `lg`). */
|
|
76
|
+
columnSpanFull(): this;
|
|
44
77
|
extraAttributes(attributes: Record<string, string>): this;
|
|
45
78
|
get livewire(): SchemaHost;
|
|
46
79
|
isLiberatedFromContainerGrid(): boolean;
|
|
@@ -74,8 +107,18 @@ export declare class Schema {
|
|
|
74
107
|
private _extraClass;
|
|
75
108
|
private _partial;
|
|
76
109
|
private _dense;
|
|
110
|
+
private _parentComponent;
|
|
111
|
+
private _rootHeadingLevel;
|
|
77
112
|
constructor(livewire: SchemaHost);
|
|
78
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;
|
|
79
122
|
key(key: string): this;
|
|
80
123
|
components(components: Component[]): this;
|
|
81
124
|
schema(components: Component[]): this;
|
|
@@ -87,8 +130,16 @@ export declare class Schema {
|
|
|
87
130
|
record(record: unknown): this;
|
|
88
131
|
/** Class placed before the grid classes (e.g. `fi-section-content`). */
|
|
89
132
|
extraClass(name: string | null): this;
|
|
90
|
-
/** Tighter spacing between components (`fi-sc-dense`). */
|
|
91
|
-
dense(condition?: boolean): this;
|
|
133
|
+
/** Tighter spacing between components (`fi-sc-dense`). Without a value set, a child schema follows its parent component. */
|
|
134
|
+
dense(condition?: boolean | null): this;
|
|
135
|
+
/** Layout component the schema belongs to: spacing and heading levels are inherited from it. */
|
|
136
|
+
parentComponent(component: Component | null): this;
|
|
137
|
+
getParentComponent(): Component | null;
|
|
138
|
+
/** Heading level of the schema's top-level headings (`2` by default: `<h2>`); nested schemas follow their parent. */
|
|
139
|
+
rootHeadingLevel(level: number): this;
|
|
140
|
+
getRootHeadingLevel(): number;
|
|
141
|
+
isDense(): boolean;
|
|
142
|
+
hasGap(): boolean;
|
|
92
143
|
/** Partially rendered schema: its root carries `wire:partial="schema.<key>"`. */
|
|
93
144
|
partiallyRendered(condition?: boolean): this;
|
|
94
145
|
private _partialName;
|
|
@@ -96,7 +147,7 @@ export declare class Schema {
|
|
|
96
147
|
partialName(name: string | null): this;
|
|
97
148
|
private _gap;
|
|
98
149
|
/** `false`: components touch, with no gap (no `fi-sc-has-gap`), as in a FusedGroup. */
|
|
99
|
-
gap(condition?: boolean): this;
|
|
150
|
+
gap(condition?: boolean | null): this;
|
|
100
151
|
private _fieldWrapperView;
|
|
101
152
|
/**
|
|
102
153
|
* Field wrapper for this schema: `'filament-forms::plain-field-wrapper'` keeps only a screen-reader label
|
|
@@ -116,6 +167,8 @@ export declare class Schema {
|
|
|
116
167
|
/** Same, without hidden components or their descendants: a field inside a hidden section is neither validated nor saved. */
|
|
117
168
|
getVisibleFlatComponents(): Promise<Component[]>;
|
|
118
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;
|
|
119
172
|
}
|
|
120
173
|
/** Child schema of a layout component: shares the parent's key and state path, without being a root. */
|
|
121
174
|
export declare function childSchema(parent: Component, components: Component[]): Schema;
|
|
@@ -136,6 +189,9 @@ export declare class Group extends Component {
|
|
|
136
189
|
static make(children?: Component[]): Group;
|
|
137
190
|
schema(components: Component[]): this;
|
|
138
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;
|
|
139
195
|
getChildSchemas(): Schema[];
|
|
140
196
|
toHtml(): Promise<string>;
|
|
141
197
|
}
|
|
@@ -159,25 +215,82 @@ export declare class Flex extends Component {
|
|
|
159
215
|
toHtml(): Promise<string>;
|
|
160
216
|
}
|
|
161
217
|
export declare const slug: (text: string) => string;
|
|
162
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* Card grouping components, with an optional header (icon, heading, description, components after the header)
|
|
220
|
+
* and footer (actions or components).
|
|
221
|
+
*/
|
|
163
222
|
export declare class Section extends Component {
|
|
164
223
|
private children;
|
|
165
224
|
private _columns;
|
|
166
225
|
private _heading;
|
|
226
|
+
private _description;
|
|
227
|
+
private _icon;
|
|
228
|
+
private _iconColor;
|
|
229
|
+
private _iconSize;
|
|
167
230
|
private _afterHeader;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
231
|
+
private _headerActions;
|
|
232
|
+
private _footer;
|
|
233
|
+
private _footerActions;
|
|
234
|
+
private _footerActionsAlignment;
|
|
235
|
+
private _aside;
|
|
236
|
+
private _contentBefore;
|
|
237
|
+
private _compact;
|
|
238
|
+
private _secondary;
|
|
239
|
+
private _divided;
|
|
240
|
+
private _contained;
|
|
172
241
|
private _collapsible;
|
|
173
242
|
private _collapsed;
|
|
243
|
+
private _persistCollapsed;
|
|
244
|
+
constructor(heading?: Lazy<string | null, ComponentContext>);
|
|
245
|
+
static make(heading?: Lazy<string | null, ComponentContext>): Section;
|
|
246
|
+
schema(components: Component[]): this;
|
|
247
|
+
columns(columns: Record<string, number> | number | null): this;
|
|
248
|
+
/** Heading: a string or a function of the context object. */
|
|
249
|
+
heading(heading: Lazy<string | null, ComponentContext>): this;
|
|
250
|
+
/** Text under the heading. */
|
|
251
|
+
description(description: Lazy<string | null, ComponentContext>): this;
|
|
252
|
+
/** Header icon (Heroicons name). */
|
|
253
|
+
icon(name: Lazy<string | null, ComponentContext>): this;
|
|
254
|
+
/** Header icon color (`gray` by default). */
|
|
255
|
+
iconColor(color: Lazy<string | null, ComponentContext>): this;
|
|
256
|
+
/** Header icon size (`lg` by default). */
|
|
257
|
+
iconSize(size: string | null): this;
|
|
258
|
+
/** Heading and description on the left, the content in a card on the right (from the `md` breakpoint). */
|
|
259
|
+
aside(condition?: Lazy<boolean, ComponentContext>): this;
|
|
260
|
+
/** With `aside()`: the content before the heading and description. */
|
|
261
|
+
contentBefore(condition?: Lazy<boolean, ComponentContext>): this;
|
|
262
|
+
/** Smaller paddings, for nested sections. */
|
|
263
|
+
compact(condition?: Lazy<boolean, ComponentContext>): this;
|
|
264
|
+
/** Less contrasting background, for a section on a background of the same color (nested sections…). */
|
|
265
|
+
secondary(condition?: Lazy<boolean, ComponentContext>): this;
|
|
266
|
+
/** Dividing lines between the components of the content. */
|
|
267
|
+
divided(condition?: Lazy<boolean, ComponentContext>): this;
|
|
268
|
+
/** `false`: no card around the section. */
|
|
269
|
+
contained(condition?: Lazy<boolean, ComponentContext>): this;
|
|
174
270
|
/** Collapsible by clicking its header; `collapsed()` starts it collapsed. */
|
|
175
|
-
collapsible(condition?: boolean): this;
|
|
176
|
-
collapsed(condition?: boolean): this;
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
/**
|
|
271
|
+
collapsible(condition?: Lazy<boolean, ComponentContext>): this;
|
|
272
|
+
collapsed(condition?: Lazy<boolean, ComponentContext>): this;
|
|
273
|
+
/** Keeps the collapsed state in the browser's local storage, under the section's key. */
|
|
274
|
+
persistCollapsed(condition?: Lazy<boolean, ComponentContext>): this;
|
|
275
|
+
/** Components and actions shown after the heading; replaces `headerActions()`. */
|
|
276
|
+
afterHeader(items: InlineItem[] | null): this;
|
|
277
|
+
/** Actions shown after the heading (when `afterHeader()` is not used). */
|
|
278
|
+
headerActions(actions: Renderable[]): this;
|
|
279
|
+
/** Components and actions in the footer; replaces `footerActions()`. */
|
|
280
|
+
footer(items: InlineItem[] | null): this;
|
|
281
|
+
/** Actions in the footer (when `footer()` is not used). */
|
|
282
|
+
footerActions(actions: Renderable[]): this;
|
|
283
|
+
/** Footer actions alignment (`start`, `center`, `end`, `between`). */
|
|
284
|
+
footerActionsAlignment(alignment: string): this;
|
|
285
|
+
private getHeaderItems;
|
|
286
|
+
private getFooterItems;
|
|
287
|
+
/** Actions the section exposes for mounting (header and footer). */
|
|
180
288
|
getActions(): Renderable[];
|
|
289
|
+
/** Heading used for the key: a function heading is only used when it returns a string synchronously. */
|
|
290
|
+
private getHeadingForKey;
|
|
291
|
+
/** Key derived from the heading (`rate-limiting::data::section`), unless a `key()` is set. */
|
|
292
|
+
getKey(): string | null;
|
|
293
|
+
getHeadingsCount(): number;
|
|
181
294
|
getChildSchemas(): Schema[];
|
|
182
295
|
toHtml(): Promise<string>;
|
|
183
296
|
}
|
|
@@ -191,35 +304,83 @@ export declare class Html extends Component {
|
|
|
191
304
|
static make(content: string | ((ctx: ComponentContext) => string | Promise<string>)): Html;
|
|
192
305
|
toHtml(): Promise<string>;
|
|
193
306
|
}
|
|
194
|
-
/** A tab of a Tabs component: a label, an optional icon and its own schema. */
|
|
307
|
+
/** A tab of a Tabs component: a label, an optional icon and badge, and its own schema. */
|
|
195
308
|
export declare class Tab extends Component {
|
|
196
309
|
private children;
|
|
197
310
|
private _columns;
|
|
198
311
|
private _icon;
|
|
312
|
+
private _iconPosition;
|
|
313
|
+
private _badge;
|
|
314
|
+
private _badgeColor;
|
|
315
|
+
private _badgeIcon;
|
|
316
|
+
private _badgeIconPosition;
|
|
317
|
+
private _badgeTooltip;
|
|
199
318
|
private label;
|
|
200
319
|
constructor(label: string);
|
|
201
320
|
static make(label: string): Tab;
|
|
202
321
|
schema(components: Component[]): this;
|
|
203
322
|
columns(columns: Record<string, number> | number | null): this;
|
|
204
323
|
icon(name: string | null): this;
|
|
324
|
+
/** Icon before (default) or after the label. */
|
|
325
|
+
iconPosition(position: 'before' | 'after'): this;
|
|
326
|
+
/** Badge after the label (a count…); a function receives the context object. */
|
|
327
|
+
badge(badge: Lazy<string | number | null, ComponentContext>): this;
|
|
328
|
+
/** Badge color (`primary` by default). */
|
|
329
|
+
badgeColor(color: Lazy<string | null, ComponentContext>): this;
|
|
330
|
+
badgeIcon(name: string | null): this;
|
|
331
|
+
badgeIconPosition(position: 'before' | 'after'): this;
|
|
332
|
+
/** Tooltip of the badge (plain text). */
|
|
333
|
+
badgeTooltip(text: Lazy<string | null, ComponentContext>): this;
|
|
205
334
|
getLabel(): string;
|
|
206
335
|
getIcon(): string | null;
|
|
336
|
+
getIconPosition(): 'before' | 'after';
|
|
337
|
+
getBadge(): Promise<{
|
|
338
|
+
label: string;
|
|
339
|
+
color: string;
|
|
340
|
+
icon: string | null;
|
|
341
|
+
iconPosition: 'before' | 'after';
|
|
342
|
+
tooltip: string | null;
|
|
343
|
+
} | null>;
|
|
207
344
|
/** Browser-side tab key (no schema prefix): `::data::` follows the schema state path, and is dropped in infolists. */
|
|
208
345
|
getTabKey(): string;
|
|
209
346
|
getChildSchemas(): Schema[];
|
|
210
347
|
toHtml(): Promise<string>;
|
|
211
348
|
}
|
|
212
|
-
/**
|
|
349
|
+
/**
|
|
350
|
+
* Layout tabs: every panel is rendered, the browser only shows the active one.
|
|
351
|
+
* The active tab may be persisted in local storage (`persistTab()`) or in the URL (`persistTabInQueryString()`):
|
|
352
|
+
* the browser only selects a persisted key when it matches an existing tab.
|
|
353
|
+
*/
|
|
213
354
|
export declare class Tabs extends Component {
|
|
214
355
|
private tabs;
|
|
215
356
|
private label;
|
|
357
|
+
private _activeTab;
|
|
358
|
+
private _persistTab;
|
|
359
|
+
private _tabQueryStringKey;
|
|
360
|
+
private _vertical;
|
|
361
|
+
private _contained;
|
|
362
|
+
private _scrollable;
|
|
216
363
|
constructor(label: string);
|
|
217
364
|
static make(label: string): Tabs;
|
|
218
365
|
schema(tabs: Tab[]): this;
|
|
366
|
+
/** Tab active when the page loads (1-based, among the visible tabs). */
|
|
367
|
+
activeTab(tab: Lazy<number, ComponentContext>): this;
|
|
368
|
+
/** Remembers the active tab in the browser's local storage, under the key of the tabs. */
|
|
369
|
+
persistTab(condition?: Lazy<boolean, ComponentContext>): this;
|
|
370
|
+
/** Keeps the active tab in the URL's query string (`?tab=…`); `null` disables it. */
|
|
371
|
+
persistTabInQueryString(key?: string | null): this;
|
|
372
|
+
/** Tab headers on the side of the panels (from the `md` breakpoint). */
|
|
373
|
+
vertical(condition?: Lazy<boolean, ComponentContext>): this;
|
|
374
|
+
/** `false`: no card around the tabs. */
|
|
375
|
+
contained(condition?: Lazy<boolean, ComponentContext>): this;
|
|
376
|
+
/** `false`: tabs that do not fit are moved to a "more" dropdown instead of scrolling. */
|
|
377
|
+
scrollable(condition?: Lazy<boolean, ComponentContext>): this;
|
|
219
378
|
getExposedMethods(): string[];
|
|
220
379
|
getLazyLoadedTabBadges(): Record<string, unknown>;
|
|
221
380
|
getDeferredTabBadges(): Record<string, unknown>;
|
|
222
381
|
getChildSchemas(): Schema[];
|
|
382
|
+
/** Active tab (1-based): the query string's tab when it names a visible tab, otherwise `activeTab()`. */
|
|
383
|
+
private getActiveTab;
|
|
223
384
|
toHtml(): Promise<string>;
|
|
224
385
|
}
|
|
225
386
|
/** Render hook injection point, using the host's scopes. Liberated from the container grid. */
|
|
@@ -257,16 +418,37 @@ export interface Renderable {
|
|
|
257
418
|
toHtml(ctx: ComponentContext): Promise<string>;
|
|
258
419
|
}
|
|
259
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>;
|
|
260
423
|
export declare class Actions extends Component {
|
|
261
424
|
private actions;
|
|
262
425
|
private _fullWidth;
|
|
263
426
|
private _alignment;
|
|
427
|
+
private _label;
|
|
428
|
+
private _beforeLabel;
|
|
429
|
+
private _afterLabel;
|
|
430
|
+
private _aboveContent;
|
|
431
|
+
private _belowContent;
|
|
264
432
|
constructor(actions: Renderable[]);
|
|
265
433
|
static make(actions: Renderable[]): Actions;
|
|
266
434
|
fullWidth(condition?: boolean): this;
|
|
267
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[];
|
|
268
448
|
toHtml(): Promise<string>;
|
|
269
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>;
|
|
270
452
|
/** Color manager used outside a panel; inside a panel, the host panel's manager takes precedence. */
|
|
271
453
|
export declare const useColorManager: (manager: ColorManager) => void;
|
|
272
454
|
/** Item in an inline row: a schema component or an action. */
|
|
@@ -330,6 +512,7 @@ export declare class Text extends Component {
|
|
|
330
512
|
private _copyableState;
|
|
331
513
|
private _copyMessage;
|
|
332
514
|
private _copyMessageDuration;
|
|
515
|
+
private _isJs;
|
|
333
516
|
constructor(content: Lazy<string | HtmlString | null, ComponentContext>);
|
|
334
517
|
static make(content: Lazy<string | HtmlString | null, ComponentContext>): Text;
|
|
335
518
|
content(content: Lazy<string | HtmlString | null, ComponentContext>): this;
|
|
@@ -353,6 +536,11 @@ export declare class Text extends Component {
|
|
|
353
536
|
copyableState(state: Lazy<string | null, ComponentContext>): this;
|
|
354
537
|
copyMessage(message: string | null): this;
|
|
355
538
|
copyMessageDuration(milliseconds: number): this;
|
|
539
|
+
/**
|
|
540
|
+
* The content is a JavaScript expression evaluated in the browser (`$get()` and `$state` are available), re-evaluated when the state changes.
|
|
541
|
+
* Only use it with expressions written by the developer, never with user input.
|
|
542
|
+
*/
|
|
543
|
+
js(condition?: boolean): this;
|
|
356
544
|
getContent(): Promise<string | HtmlString | null>;
|
|
357
545
|
toHtml(): Promise<string>;
|
|
358
546
|
}
|
|
@@ -436,7 +624,8 @@ export declare class EmptyState extends Component {
|
|
|
436
624
|
/** `false`: no surrounding card. */
|
|
437
625
|
contained(condition?: boolean): this;
|
|
438
626
|
/** Heading tag (`h2` by default). */
|
|
439
|
-
|
|
627
|
+
/** Heading tag; by default it follows the heading level of the schema (`h2` at the root). */
|
|
628
|
+
headingTag(tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | null): this;
|
|
440
629
|
/** Footer: inline actions and components. */
|
|
441
630
|
footer(items: InlineItem[]): this;
|
|
442
631
|
/** Footer actions, mounted on the root schema. */
|
|
@@ -460,6 +649,7 @@ export declare class Callout extends Component {
|
|
|
460
649
|
private _footerActionsAlignment;
|
|
461
650
|
private _footer;
|
|
462
651
|
private _controlActions;
|
|
652
|
+
private _controls;
|
|
463
653
|
constructor(heading?: Lazy<string | null, ComponentContext>);
|
|
464
654
|
static make(heading?: Lazy<string | null, ComponentContext>): Callout;
|
|
465
655
|
heading(heading: Lazy<string | null, ComponentContext>): this;
|
|
@@ -485,6 +675,8 @@ export declare class Callout extends Component {
|
|
|
485
675
|
footer(items: InlineItem[] | null): this;
|
|
486
676
|
/** Small actions on the right of the callout (dismiss…). */
|
|
487
677
|
controlActions(actions: Renderable[]): this;
|
|
678
|
+
/** Free-form controls (components and actions) on the right of the callout, replacing the control actions. */
|
|
679
|
+
controls(items: InlineItem[] | null): this;
|
|
488
680
|
getColor(): string | null;
|
|
489
681
|
getIcon(): string | null;
|
|
490
682
|
/** Callout actions, mounted on the root schema. */
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
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';
|
|
7
7
|
views.namespace('filament-schemas', fileURLToPath(new URL('../views', import.meta.url)));
|
|
8
|
+
/** Breakpoints accepted in grid objects: viewport (`sm`…`2xl`), container (`@3xs`…`@7xl`) and fallback (`!@sm`…`!@2xl`). */
|
|
9
|
+
const GRID_BREAKPOINT = /^(default|sm|md|lg|xl|2xl|@(3xs|2xs|xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl)|!@(sm|md|lg|xl|2xl))$/;
|
|
10
|
+
/** CSS variable suffix of a breakpoint (`@md` → `cmd`, `!@md` → `ncmd`). */
|
|
11
|
+
const breakpointVariable = (breakpoint) => breakpoint.replace(/@/g, 'c').replace(/!/g, 'n');
|
|
12
|
+
/** Resolves a column start or order: successive calls merge, a function's number result applies to `default`. */
|
|
13
|
+
async function resolvePlacement(entries, ctx, keepZero) {
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const value = typeof entry === 'function' ? await entry(ctx) : entry;
|
|
17
|
+
if (value === null || value === undefined)
|
|
18
|
+
continue;
|
|
19
|
+
if (typeof value === 'number') {
|
|
20
|
+
out[typeof entry === 'function' ? 'default' : 'lg'] = value;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
for (const [breakpoint, item] of Object.entries(value))
|
|
24
|
+
out[breakpoint] = await evaluate(item, ctx);
|
|
25
|
+
}
|
|
26
|
+
const result = {};
|
|
27
|
+
for (const [breakpoint, value] of Object.entries(out)) {
|
|
28
|
+
if (!GRID_BREAKPOINT.test(breakpoint) || typeof value !== 'number' || !Number.isInteger(value) || (!keepZero && value === 0))
|
|
29
|
+
continue;
|
|
30
|
+
result[breakpoint] = value;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
8
34
|
export class Component {
|
|
9
35
|
_grow = true;
|
|
10
36
|
/** Inside a Flex: `false` keeps the natural width instead of filling the available space. */
|
|
@@ -16,9 +42,20 @@ export class Component {
|
|
|
16
42
|
_keyName = null;
|
|
17
43
|
_hidden = false;
|
|
18
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; }
|
|
19
52
|
_columnSpan = { default: 1 };
|
|
20
53
|
_extraAttributes = {};
|
|
21
|
-
_columnStart =
|
|
54
|
+
_columnStart = [];
|
|
55
|
+
_columnOrder = [];
|
|
56
|
+
_gridContainer = false;
|
|
57
|
+
_dense = null;
|
|
58
|
+
_gap = null;
|
|
22
59
|
container;
|
|
23
60
|
hidden(condition = true) { this._hidden = condition; return this; }
|
|
24
61
|
visible(condition = true) { this._visible = condition; return this; }
|
|
@@ -29,9 +66,25 @@ export class Component {
|
|
|
29
66
|
visibleOn(operations) { const list = [].concat(operations); return this.visible(({ operation }) => operation !== null && list.includes(operation)); }
|
|
30
67
|
/** A number applies from the `lg` breakpoint; an object replaces the default entirely. Values may be closures. */
|
|
31
68
|
columnSpan(span) { this._columnSpan = typeof span === 'object' ? span : { default: 1, lg: span }; return this; }
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
69
|
+
/** Starting grid column: a number applies from the `lg` breakpoint; an object sets it per breakpoint. Successive calls merge; `null` resets it. */
|
|
70
|
+
columnStart(start) { this._columnStart = start === null ? [] : [...this._columnStart, start]; return this; }
|
|
71
|
+
/** Visual order in the grid, without changing the markup order: a number applies from `lg`; an object sets it per breakpoint. */
|
|
72
|
+
columnOrder(order) { this._columnOrder = order === null ? [] : [...this._columnOrder, order]; return this; }
|
|
73
|
+
/** Makes the component a CSS container, so that grids inside it may use container breakpoints (`@md`, `@xl`…). */
|
|
74
|
+
gridContainer(condition = true) { this._gridContainer = condition; return this; }
|
|
75
|
+
/** Tighter spacing between the components of its child schemas (inherited by nested layouts). */
|
|
76
|
+
dense(condition = true) { this._dense = condition; return this; }
|
|
77
|
+
/** `false`: no space between the components of its child schemas (inherited by nested layouts). */
|
|
78
|
+
gap(condition = true) { this._gap = condition; return this; }
|
|
79
|
+
isDense() { return this._dense ?? this.container.isDense(); }
|
|
80
|
+
hasGap() { return this._gap ?? this.container.hasGap(); }
|
|
81
|
+
/** Heading level of the component's own headings (`index` for a second heading). */
|
|
82
|
+
getHeadingLevel(index = 0) { return Math.max(1, this.container.getRootHeadingLevel() + index); }
|
|
83
|
+
/** `h1`…`h6`, or `p` beyond level 6. */
|
|
84
|
+
getHeadingTag(index = 0) { const level = this.getHeadingLevel(index); return level > 6 ? 'p' : `h${level}`; }
|
|
85
|
+
/** Number of heading levels the component adds above its child schemas (1 for a section with a heading). */
|
|
86
|
+
getHeadingsCount() { return 0; }
|
|
87
|
+
getChildSchemaRootHeadingLevel() { return this.getHeadingLevel() + this.getHeadingsCount(); }
|
|
35
88
|
/** Full grid width on every screen size (`columnSpan('full')` only applies from `lg`). */
|
|
36
89
|
columnSpanFull() { return this.columnSpan({ default: 'full' }); }
|
|
37
90
|
extraAttributes(attributes) { Object.assign(this._extraAttributes, attributes); return this; }
|
|
@@ -69,16 +122,23 @@ export class Component {
|
|
|
69
122
|
const key = this.getKey();
|
|
70
123
|
const span = {};
|
|
71
124
|
const rawSpan = typeof this._columnSpan === 'object' ? this._columnSpan : { default: 1, lg: this._columnSpan };
|
|
125
|
+
const ctx = this.context();
|
|
72
126
|
for (const [breakpoint, value] of Object.entries(rawSpan))
|
|
73
|
-
span[breakpoint] = await evaluate(value,
|
|
127
|
+
span[breakpoint] = await evaluate(value, ctx);
|
|
128
|
+
const start = await resolvePlacement(this._columnStart, ctx, false);
|
|
129
|
+
const order = await resolvePlacement(this._columnOrder, ctx, true);
|
|
130
|
+
const placementClass = (name, values) => Object.keys(values).map((breakpoint) => (breakpoint === 'default' ? `fi-grid-col-${name}` : `${breakpoint}:fi-grid-col-${name}`));
|
|
131
|
+
const placementStyle = (name, values) => Object.entries(values).map(([breakpoint, value]) => `--col-${name}-${breakpointVariable(breakpoint)}: ${value}`);
|
|
74
132
|
const attributes = new Attributes()
|
|
75
|
-
.when(!this.container.isInline(), (a) => a.gridColumn(span, !isVisible)
|
|
76
|
-
.
|
|
133
|
+
.when(!this.container.isInline(), (a) => a.gridColumn(span, !isVisible)
|
|
134
|
+
.class([...placementClass('start', start), ...placementClass('order', order)])
|
|
135
|
+
.style([...placementStyle('start', start), ...placementStyle('order', order)]))
|
|
77
136
|
.merge({ 'wire:key': this.getLivewireKey() });
|
|
78
137
|
return views.render('filament-schemas::component', {
|
|
79
138
|
key, isVisible, attributes: attributes.toHtml(),
|
|
80
139
|
alpine: isVisible ? `filamentSchemaComponent(${js(this.getAlpineScopeConfiguration())})` : null,
|
|
81
140
|
html: isVisible ? await this.toHtml() : '',
|
|
141
|
+
gridContainer: isVisible && Boolean(await evaluate(this._gridContainer, ctx)),
|
|
82
142
|
});
|
|
83
143
|
}
|
|
84
144
|
}
|
|
@@ -94,9 +154,19 @@ export class Schema {
|
|
|
94
154
|
_record = null;
|
|
95
155
|
_extraClass = null;
|
|
96
156
|
_partial = false;
|
|
97
|
-
_dense =
|
|
157
|
+
_dense = null;
|
|
158
|
+
_parentComponent = null;
|
|
159
|
+
_rootHeadingLevel = 2;
|
|
98
160
|
constructor(livewire) { this.livewire = livewire; }
|
|
99
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; }
|
|
100
170
|
key(key) { this._key = key; return this; }
|
|
101
171
|
components(components) { this._components = components; for (const c of components)
|
|
102
172
|
c.container = this; return this; }
|
|
@@ -109,14 +179,23 @@ export class Schema {
|
|
|
109
179
|
record(record) { this._record = record; return this; }
|
|
110
180
|
/** Class placed before the grid classes (e.g. `fi-section-content`). */
|
|
111
181
|
extraClass(name) { this._extraClass = name; return this; }
|
|
112
|
-
/** Tighter spacing between components (`fi-sc-dense`). */
|
|
182
|
+
/** Tighter spacing between components (`fi-sc-dense`). Without a value set, a child schema follows its parent component. */
|
|
113
183
|
dense(condition = true) { this._dense = condition; return this; }
|
|
184
|
+
/** Layout component the schema belongs to: spacing and heading levels are inherited from it. */
|
|
185
|
+
parentComponent(component) { this._parentComponent = component; return this; }
|
|
186
|
+
getParentComponent() { return this._parentComponent; }
|
|
187
|
+
/** Heading level of the schema's top-level headings (`2` by default: `<h2>`); nested schemas follow their parent. */
|
|
188
|
+
rootHeadingLevel(level) { if (Number.isInteger(level) && level >= 1)
|
|
189
|
+
this._rootHeadingLevel = level; return this; }
|
|
190
|
+
getRootHeadingLevel() { return this._parentComponent ? this._parentComponent.getChildSchemaRootHeadingLevel() : this._rootHeadingLevel; }
|
|
191
|
+
isDense() { return this._dense ?? (this._inline || (this._parentComponent?.isDense() ?? false)); }
|
|
192
|
+
hasGap() { return this._gap ?? this._parentComponent?.hasGap() ?? true; }
|
|
114
193
|
/** Partially rendered schema: its root carries `wire:partial="schema.<key>"`. */
|
|
115
194
|
partiallyRendered(condition = true) { this._partial = condition; return this; }
|
|
116
195
|
_partialName = null;
|
|
117
196
|
/** Root `wire:partial` name when it differs from the schema key (filters form: `table-filters-form`). */
|
|
118
197
|
partialName(name) { this._partialName = name; return this; }
|
|
119
|
-
_gap =
|
|
198
|
+
_gap = null;
|
|
120
199
|
/** `false`: components touch, with no gap (no `fi-sc-has-gap`), as in a FusedGroup. */
|
|
121
200
|
gap(condition = true) { this._gap = condition; return this; }
|
|
122
201
|
_fieldWrapperView = null;
|
|
@@ -150,18 +229,21 @@ export class Schema {
|
|
|
150
229
|
return out;
|
|
151
230
|
}
|
|
152
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();
|
|
153
235
|
const withVisibility = await Promise.all(this._components.map(async (c) => [c, await c.isVisible()]));
|
|
154
236
|
if (!withVisibility.some(([, v]) => v))
|
|
155
237
|
return '';
|
|
156
238
|
const attributes = new Attributes(this._extraClass ? { class: this._extraClass } : {})
|
|
157
239
|
.when(!this._inline, (a) => a.grid(this._columns))
|
|
158
240
|
.merge({
|
|
159
|
-
'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),
|
|
160
242
|
'x-data': this._root ? `filamentSchema({ livewireId: ${js(this.livewire.id)}, schemaKey: ${js(this._key)} })` : null,
|
|
161
243
|
'x-on:form-validation-error.window': this._root ? 'handleFormValidationError' : null,
|
|
162
244
|
'x-on:reset-schema-component-state.window': this._root ? 'handleClientSideStateReset' : null,
|
|
163
245
|
})
|
|
164
|
-
.class(['fi-sc', { 'fi-inline': this._inline }, { 'fi-sc-has-gap': this.
|
|
246
|
+
.class(['fi-sc', { 'fi-inline': this._inline }, { 'fi-sc-has-gap': this.hasGap() }, { 'fi-sc-dense': this.isDense() }]);
|
|
165
247
|
const parts = [];
|
|
166
248
|
for (const [component, isVisible] of withVisibility) {
|
|
167
249
|
if (!component.isLiberatedFromContainerGrid())
|
|
@@ -171,11 +253,21 @@ export class Schema {
|
|
|
171
253
|
}
|
|
172
254
|
return `<div ${attributes.toHtml()}>${parts.join('')}</div>`;
|
|
173
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
|
+
}
|
|
174
262
|
}
|
|
175
263
|
/** Child schema of a layout component: shares the parent's key and state path, without being a root. */
|
|
176
264
|
export function childSchema(parent, components) {
|
|
177
265
|
const c = parent.container;
|
|
178
|
-
|
|
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;
|
|
179
271
|
}
|
|
180
272
|
/** Stateful group: its children live under `<container path>.<name>` (used by filter forms). */
|
|
181
273
|
export class StatefulGroup extends Component {
|
|
@@ -185,7 +277,7 @@ export class StatefulGroup extends Component {
|
|
|
185
277
|
hasStatePath() { return true; }
|
|
186
278
|
getChildSchemas() {
|
|
187
279
|
const c = this.container;
|
|
188
|
-
return [new Schema(c.livewire).key(this.getKey()).statePath(this.getStatePath()).operation(c.getOperation()).record(c.getRecord()).root(false).components(this.children)];
|
|
280
|
+
return [new Schema(c.livewire).key(this.getKey()).statePath(this.getStatePath()).operation(c.getOperation()).record(c.getRecord()).root(false).parentComponent(this).components(this.children)];
|
|
189
281
|
}
|
|
190
282
|
async toHtml() { return `<div id="${e(this.getKey())}">${await this.getChildSchemas()[0].toHtml()}</div>`; }
|
|
191
283
|
}
|
|
@@ -197,8 +289,19 @@ export class Group extends Component {
|
|
|
197
289
|
static make(children = []) { return configured(new Group(children)); }
|
|
198
290
|
schema(components) { this.children = components; return this; }
|
|
199
291
|
columns(columns) { this._columns = columns; return this; }
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
}
|
|
301
|
+
async toHtml() {
|
|
302
|
+
const attributes = new Attributes().merge({ ...this._extraAttributes }).toHtml();
|
|
303
|
+
return `<div${attributes ? ` ${attributes}` : ''}>${await this.getChildSchemas()[0].toHtml()}</div>`;
|
|
304
|
+
}
|
|
202
305
|
}
|
|
203
306
|
/** Column grid: two columns from `lg` by default. */
|
|
204
307
|
export class Grid extends Group {
|
|
@@ -228,55 +331,159 @@ export class Flex extends Component {
|
|
|
228
331
|
}
|
|
229
332
|
}
|
|
230
333
|
export const slug = (text) => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
231
|
-
/**
|
|
334
|
+
/** Footer alignment (`start`, `center`, `end`, `between`; `right` and `justify` are aliases) as an inline schema alignment. */
|
|
335
|
+
const footerAlignment = (alignment) => (['right', 'end'].includes(alignment) ? 'end' : alignment === 'justify' ? 'between' : ['start', 'center', 'between'].includes(alignment) ? alignment : 'start');
|
|
336
|
+
/**
|
|
337
|
+
* Card grouping components, with an optional header (icon, heading, description, components after the header)
|
|
338
|
+
* and footer (actions or components).
|
|
339
|
+
*/
|
|
232
340
|
export class Section extends Component {
|
|
233
341
|
children = [];
|
|
234
342
|
_columns = {};
|
|
235
343
|
_heading;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
344
|
+
_description = null;
|
|
345
|
+
_icon = null;
|
|
346
|
+
_iconColor = null;
|
|
347
|
+
_iconSize = null;
|
|
348
|
+
_afterHeader = null;
|
|
349
|
+
_headerActions = [];
|
|
350
|
+
_footer = null;
|
|
351
|
+
_footerActions = [];
|
|
352
|
+
_footerActionsAlignment = 'start';
|
|
353
|
+
_aside = false;
|
|
354
|
+
_contentBefore = false;
|
|
355
|
+
_compact = false;
|
|
356
|
+
_secondary = false;
|
|
357
|
+
_divided = false;
|
|
358
|
+
_contained = true;
|
|
359
|
+
_collapsible = null;
|
|
360
|
+
_collapsed = false;
|
|
361
|
+
_persistCollapsed = false;
|
|
362
|
+
constructor(heading = null) { super(); this._heading = heading; }
|
|
244
363
|
static make(heading = null) { return configured(new Section(heading)); }
|
|
245
364
|
schema(components) { this.children = components; return this; }
|
|
246
365
|
columns(columns) { this._columns = columns; return this; }
|
|
247
|
-
|
|
248
|
-
|
|
366
|
+
/** Heading: a string or a function of the context object. */
|
|
367
|
+
heading(heading) { this._heading = heading; return this; }
|
|
368
|
+
/** Text under the heading. */
|
|
369
|
+
description(description) { this._description = description; return this; }
|
|
370
|
+
/** Header icon (Heroicons name). */
|
|
371
|
+
icon(name) { this._icon = name; return this; }
|
|
372
|
+
/** Header icon color (`gray` by default). */
|
|
373
|
+
iconColor(color) { this._iconColor = color; return this; }
|
|
374
|
+
/** Header icon size (`lg` by default). */
|
|
375
|
+
iconSize(size) { this._iconSize = size; return this; }
|
|
376
|
+
/** Heading and description on the left, the content in a card on the right (from the `md` breakpoint). */
|
|
377
|
+
aside(condition = true) { this._aside = condition; return this; }
|
|
378
|
+
/** With `aside()`: the content before the heading and description. */
|
|
379
|
+
contentBefore(condition = true) { this._contentBefore = condition; return this; }
|
|
380
|
+
/** Smaller paddings, for nested sections. */
|
|
381
|
+
compact(condition = true) { this._compact = condition; return this; }
|
|
382
|
+
/** Less contrasting background, for a section on a background of the same color (nested sections…). */
|
|
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; }
|
|
386
|
+
/** `false`: no card around the section. */
|
|
387
|
+
contained(condition = true) { this._contained = condition; return this; }
|
|
249
388
|
/** Collapsible by clicking its header; `collapsed()` starts it collapsed. */
|
|
250
389
|
collapsible(condition = true) { this._collapsible = condition; return this; }
|
|
251
|
-
collapsed(condition = true) { this._collapsed = condition; if (
|
|
252
|
-
this._collapsible =
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
|
|
390
|
+
collapsed(condition = true) { this._collapsed = condition; if (this._collapsible === null)
|
|
391
|
+
this._collapsible = condition; return this; }
|
|
392
|
+
/** Keeps the collapsed state in the browser's local storage, under the section's key. */
|
|
393
|
+
persistCollapsed(condition = true) { this._persistCollapsed = condition; return this; }
|
|
394
|
+
/** Components and actions shown after the heading; replaces `headerActions()`. */
|
|
395
|
+
afterHeader(items) { this._afterHeader = items; return this; }
|
|
396
|
+
/** Actions shown after the heading (when `afterHeader()` is not used). */
|
|
397
|
+
headerActions(actions) { this._headerActions = [...this._headerActions, ...actions]; return this; }
|
|
398
|
+
/** Components and actions in the footer; replaces `footerActions()`. */
|
|
399
|
+
footer(items) { this._footer = items; return this; }
|
|
400
|
+
/** Actions in the footer (when `footer()` is not used). */
|
|
401
|
+
footerActions(actions) { this._footerActions = [...this._footerActions, ...actions]; return this; }
|
|
402
|
+
/** Footer actions alignment (`start`, `center`, `end`, `between`). */
|
|
403
|
+
footerActionsAlignment(alignment) { this._footerActionsAlignment = alignment; return this; }
|
|
404
|
+
getHeaderItems() { return this._afterHeader ?? this._headerActions; }
|
|
405
|
+
getFooterItems() { return this._footer ?? this._footerActions; }
|
|
406
|
+
/** Actions the section exposes for mounting (header and footer). */
|
|
407
|
+
getActions() { return [...actionsOf(this.getHeaderItems()), ...actionsOf(this.getFooterItems())]; }
|
|
408
|
+
/** Heading used for the key: a function heading is only used when it returns a string synchronously. */
|
|
409
|
+
getHeadingForKey() {
|
|
410
|
+
if (typeof this._heading !== 'function')
|
|
411
|
+
return this._heading;
|
|
412
|
+
let value;
|
|
413
|
+
try {
|
|
414
|
+
value = this._heading(this.context());
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
// An async heading is not awaited here: its failure is reported when the section renders.
|
|
420
|
+
if (value instanceof Promise)
|
|
421
|
+
value.catch(() => { });
|
|
422
|
+
return typeof value === 'string' ? value : null;
|
|
423
|
+
}
|
|
424
|
+
/** Key derived from the heading (`rate-limiting::data::section`), unless a `key()` is set. */
|
|
425
|
+
getKey() {
|
|
426
|
+
if (this._keyName !== null)
|
|
427
|
+
return super.getKey();
|
|
428
|
+
const heading = this.getHeadingForKey();
|
|
429
|
+
if (isBlank(heading))
|
|
430
|
+
return null;
|
|
431
|
+
const statePath = this.container.getStatePath();
|
|
432
|
+
return `${this.container.getKey()}.${slug(heading)}::${statePath ? `${statePath}::` : ''}section`;
|
|
433
|
+
}
|
|
434
|
+
getHeadingsCount() { return typeof this._heading === 'function' || !isBlank(this._heading) ? 1 : 0; }
|
|
257
435
|
getChildSchemas() { return [childSchema(this, this.children).columns(this._columns).extraClass('fi-section-content')]; }
|
|
258
436
|
async toHtml() {
|
|
259
|
-
const [content] = this.getChildSchemas();
|
|
260
|
-
if (this._heading === null) {
|
|
261
|
-
return `<div class="fi-sc-section"><section x-data="{
|
|
262
|
-
isCollapsed: false,
|
|
263
|
-
}" class="fi-section"><div class="fi-section-content-ctn">${await content.toHtml()}</div></section></div>`;
|
|
264
|
-
}
|
|
265
|
-
const key = this.getKey();
|
|
266
437
|
const ctx = this.context();
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
438
|
+
const [content] = this.getChildSchemas();
|
|
439
|
+
const id = this.getKey();
|
|
440
|
+
const heading = await evaluate(this._heading, ctx);
|
|
441
|
+
const description = await evaluate(this._description, ctx);
|
|
442
|
+
const iconName = await evaluate(this._icon, ctx);
|
|
443
|
+
const isAside = Boolean(await evaluate(this._aside, ctx));
|
|
444
|
+
const isCollapsible = Boolean(await evaluate(this._collapsible ?? false, ctx)) && !isAside;
|
|
445
|
+
const isCollapsed = Boolean(await evaluate(this._collapsed, ctx));
|
|
446
|
+
const shouldPersistCollapsed = Boolean(await evaluate(this._persistCollapsed, ctx));
|
|
447
|
+
const hasHeading = !isBlank(heading), hasDescription = !isBlank(description), hasIcon = !isBlank(iconName);
|
|
448
|
+
const afterHeader = await renderInlineItems(this, this.getHeaderItems(), { alignment: 'end' });
|
|
449
|
+
const footer = await renderInlineItems(this, this.getFooterItems(), { alignment: this._footer === null ? footerAlignment(this._footerActionsAlignment) : null });
|
|
450
|
+
const contentHtml = await content.toHtml();
|
|
451
|
+
const hasHeader = hasIcon || hasHeading || hasDescription || isCollapsible || afterHeader !== '';
|
|
452
|
+
const hasContent = contentHtml !== '' || footer !== '';
|
|
453
|
+
const contentId = id !== null && hasContent ? `${id}-content` : null;
|
|
454
|
+
const headingId = id !== null && hasHeading ? `${id}-heading` : null;
|
|
455
|
+
const descriptionId = id !== null && hasDescription ? `${id}-description` : null;
|
|
456
|
+
const outer = new Attributes().merge({ id, ...this._extraAttributes }).class(['fi-sc-section']);
|
|
273
457
|
// Collapsible section: listens to `collapse-section` / `expand-section`… window events, with a toggle button in the header.
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
458
|
+
// A persisted state lives in local storage, under the section's key (or its element id).
|
|
459
|
+
const idTest = `if ($event.detail.id == (${js(id)} ?? $el.id))`;
|
|
460
|
+
const collapse = isCollapsible ? ` x-on:collapse-section.window="${e(`${idTest} isCollapsed = true`)}" x-on:expand="isCollapsed = false" x-on:expand-section.window="${e(`${idTest} isCollapsed = false`)}" x-on:open-section.window="${e(`${idTest} isCollapsed = false`)}" x-on:toggle-section.window="${e(`${idTest} isCollapsed = ! isCollapsed`)}"${shouldPersistCollapsed ? '' : ` x-on:reset-schema-component-state.window="${e(`if (($event.detail.livewireId === ${js(this.livewire.id)}) && ($event.detail.schemaKey === ${js(this.container.getKey().split('.')[0])})) $nextTick(() => isCollapsed = ${isCollapsed ? 'true' : 'false'})`)}"`} x-bind:class="isCollapsed && 'fi-collapsed'"` : '';
|
|
461
|
+
const collapsedState = shouldPersistCollapsed ? e(`$persist(${isCollapsed ? 'true' : 'false'}).as(\`filament:section:collapsed:\${${js(id)} ?? $el.id}\`)`) : (isCollapsed ? 'true' : 'false');
|
|
462
|
+
const sectionClass = cls(['fi-section', {
|
|
463
|
+
'fi-section-not-contained': !(await evaluate(this._contained, ctx)),
|
|
464
|
+
'fi-section-has-content-before': Boolean(await evaluate(this._contentBefore, ctx)),
|
|
465
|
+
'fi-section-has-header': hasHeader,
|
|
466
|
+
'fi-aside': isAside,
|
|
467
|
+
'fi-compact': Boolean(await evaluate(this._compact, ctx)),
|
|
468
|
+
'fi-collapsible': isCollapsible,
|
|
469
|
+
'fi-collapsed': isCollapsible && isCollapsed,
|
|
470
|
+
'fi-divided': Boolean(await evaluate(this._divided, ctx)),
|
|
471
|
+
'fi-secondary': Boolean(await evaluate(this._secondary, ctx)),
|
|
472
|
+
}]);
|
|
473
|
+
let header = '';
|
|
474
|
+
if (hasHeader) {
|
|
475
|
+
const iconColor = (await evaluate(this._iconColor, ctx)) ?? 'gray';
|
|
476
|
+
const iconHtml = hasIcon ? icon(iconName, { size: safeClass(this._iconSize) ?? 'lg', attributes: new Attributes().class(plainColorClasses(iconColor)) }) : '';
|
|
477
|
+
const tag = this.getHeadingTag();
|
|
478
|
+
const text = hasHeading || hasDescription ? `<div class="fi-section-header-text-ctn">${hasHeading ? `<${tag}${headingId ? ` id="${e(headingId)}"` : ''} class="fi-section-header-heading">${e(heading)}</${tag}>` : ''}${hasDescription ? `<p${descriptionId ? ` id="${e(descriptionId)}"` : ''} class="fi-section-header-description">${e(description)}</p>` : ''}</div>` : '';
|
|
479
|
+
const headerContent = hasHeading || hasDescription || afterHeader ? `<div class="${cls(['fi-section-header-content', { 'fi-section-header-content-has-heading': hasHeading || hasDescription }])}">${text}${afterHeader ? `<div class="fi-section-header-after-ctn">${afterHeader}</div>` : ''}</div>` : '';
|
|
480
|
+
const collapseButton = isCollapsible ? `<button type="button" x-on:click.stop="isCollapsed = ! isCollapsed" aria-label="${e(t(isCollapsed ? 'filament::components/section.actions.expand.label' : 'filament::components/section.actions.collapse.label'))}" x-bind:aria-label="isCollapsed ? ${e(js(t('filament::components/section.actions.expand.label')))} : ${e(js(t('filament::components/section.actions.collapse.label')))}" aria-expanded="${isCollapsed ? 'false' : 'true'}" x-bind:aria-expanded="(! isCollapsed).toString()"${contentId ? ` aria-controls="${e(contentId)}"` : ''} class="fi-icon-btn fi-size-md fi-section-collapse-btn">${icon('m-chevron-up')}</button>` : '';
|
|
481
|
+
header = `<header${isCollapsible ? ` x-on:click="if (! $event.target.closest('.fi-section-header-after-ctn')) isCollapsed = ! isCollapsed"` : ''} class="fi-section-header">${iconHtml}${headerContent}${collapseButton}</header>`;
|
|
482
|
+
}
|
|
483
|
+
const body = hasContent ? `<div${contentId ? ` id="${e(contentId)}"` : ''}${isCollapsible && (isCollapsed || shouldPersistCollapsed) ? ' x-cloak' : ''} class="fi-section-content-ctn">${contentHtml}${footer ? `<footer class="fi-section-footer">${footer}</footer>` : ''}</div>` : '';
|
|
484
|
+
return `<div ${outer.toHtml()}><section x-data="{
|
|
485
|
+
isCollapsed: ${collapsedState},
|
|
486
|
+
}"${collapse}${headingId ? ` aria-labelledby="${e(headingId)}"` : ''}${descriptionId ? ` aria-describedby="${e(descriptionId)}"` : ''} class="${sectionClass}">${header}${body}</section></div>`;
|
|
280
487
|
}
|
|
281
488
|
}
|
|
282
489
|
/**
|
|
@@ -289,71 +496,167 @@ export class Html extends Component {
|
|
|
289
496
|
static make(content) { return new Html(content); }
|
|
290
497
|
async toHtml() { return typeof this.content === 'function' ? this.content(this.context()) : this.content; }
|
|
291
498
|
}
|
|
292
|
-
/** A tab of a Tabs component: a label, an optional icon and its own schema. */
|
|
499
|
+
/** A tab of a Tabs component: a label, an optional icon and badge, and its own schema. */
|
|
293
500
|
export class Tab extends Component {
|
|
294
501
|
children = [];
|
|
295
502
|
_columns = {};
|
|
296
503
|
_icon = null;
|
|
504
|
+
_iconPosition = 'before';
|
|
505
|
+
_badge = null;
|
|
506
|
+
_badgeColor = null;
|
|
507
|
+
_badgeIcon = null;
|
|
508
|
+
_badgeIconPosition = 'before';
|
|
509
|
+
_badgeTooltip = null;
|
|
297
510
|
label;
|
|
298
511
|
constructor(label) { super(); this.label = label; this._keyName = `${slug(label)}::data::tab`; }
|
|
299
512
|
static make(label) { return configured(new Tab(label)); }
|
|
300
513
|
schema(components) { this.children = components; return this; }
|
|
301
514
|
columns(columns) { this._columns = columns; return this; }
|
|
302
515
|
icon(name) { this._icon = name; return this; }
|
|
516
|
+
/** Icon before (default) or after the label. */
|
|
517
|
+
iconPosition(position) { this._iconPosition = position === 'after' ? 'after' : 'before'; return this; }
|
|
518
|
+
/** Badge after the label (a count…); a function receives the context object. */
|
|
519
|
+
badge(badge) { this._badge = badge; return this; }
|
|
520
|
+
/** Badge color (`primary` by default). */
|
|
521
|
+
badgeColor(color) { this._badgeColor = color; return this; }
|
|
522
|
+
badgeIcon(name) { this._badgeIcon = name; return this; }
|
|
523
|
+
badgeIconPosition(position) { this._badgeIconPosition = position === 'after' ? 'after' : 'before'; return this; }
|
|
524
|
+
/** Tooltip of the badge (plain text). */
|
|
525
|
+
badgeTooltip(text) { this._badgeTooltip = text; return this; }
|
|
303
526
|
getLabel() { return this.label; }
|
|
304
527
|
getIcon() { return this._icon; }
|
|
528
|
+
getIconPosition() { return this._iconPosition; }
|
|
529
|
+
async getBadge() {
|
|
530
|
+
const ctx = this.context();
|
|
531
|
+
const badge = await evaluate(this._badge, ctx);
|
|
532
|
+
if (isBlank(badge))
|
|
533
|
+
return null;
|
|
534
|
+
return { label: String(badge), color: (await evaluate(this._badgeColor, ctx)) ?? 'primary', icon: this._badgeIcon, iconPosition: this._badgeIconPosition, tooltip: await evaluate(this._badgeTooltip, ctx) };
|
|
535
|
+
}
|
|
305
536
|
/** Browser-side tab key (no schema prefix): `::data::` follows the schema state path, and is dropped in infolists. */
|
|
306
537
|
getTabKey() { const statePath = this.container.getStatePath(); return this._keyName.replace('::data::', statePath ? `::${statePath}::` : '::'); }
|
|
307
538
|
getChildSchemas() { return [childSchema(this, this.children).columns(this._columns)]; }
|
|
308
539
|
async toHtml() { return this.getChildSchemas()[0].toHtml(); }
|
|
309
540
|
}
|
|
310
|
-
/**
|
|
541
|
+
/** Badge colors of a tab: computed shades when a color manager is available. */
|
|
542
|
+
const badgeColorClasses = (component, color) => colorsFor(component)?.getComponentClasses('badge', color) ?? plainColorClasses(color);
|
|
543
|
+
const badgeTooltipAttribute = (tooltip) => (tooltip ? ` x-tooltip="${e(`{ content: ${js(tooltip)}, theme: $store.theme, allowHTML: false }`)}"` : '');
|
|
544
|
+
/**
|
|
545
|
+
* Layout tabs: every panel is rendered, the browser only shows the active one.
|
|
546
|
+
* The active tab may be persisted in local storage (`persistTab()`) or in the URL (`persistTabInQueryString()`):
|
|
547
|
+
* the browser only selects a persisted key when it matches an existing tab.
|
|
548
|
+
*/
|
|
311
549
|
export class Tabs extends Component {
|
|
312
550
|
tabs = [];
|
|
313
551
|
label;
|
|
552
|
+
_activeTab = 1;
|
|
553
|
+
_persistTab = false;
|
|
554
|
+
_tabQueryStringKey = null;
|
|
555
|
+
_vertical = false;
|
|
556
|
+
_contained = true;
|
|
557
|
+
_scrollable = true;
|
|
314
558
|
constructor(label) { super(); this.label = label; this._keyName = `${slug(label)}::data::tabs`; }
|
|
315
559
|
static make(label) { return configured(new Tabs(label)); }
|
|
316
560
|
schema(tabs) { this.tabs = tabs; return this; }
|
|
561
|
+
/** Tab active when the page loads (1-based, among the visible tabs). */
|
|
562
|
+
activeTab(tab) { this._activeTab = tab; return this; }
|
|
563
|
+
/** Remembers the active tab in the browser's local storage, under the key of the tabs. */
|
|
564
|
+
persistTab(condition = true) { this._persistTab = condition; return this; }
|
|
565
|
+
/** Keeps the active tab in the URL's query string (`?tab=…`); `null` disables it. */
|
|
566
|
+
persistTabInQueryString(key = 'tab') { this._tabQueryStringKey = key === null || key.trim() === '' ? null : key; return this; }
|
|
567
|
+
/** Tab headers on the side of the panels (from the `md` breakpoint). */
|
|
568
|
+
vertical(condition = true) { this._vertical = condition; return this; }
|
|
569
|
+
/** `false`: no card around the tabs. */
|
|
570
|
+
contained(condition = true) { this._contained = condition; return this; }
|
|
571
|
+
/** `false`: tabs that do not fit are moved to a "more" dropdown instead of scrolling. */
|
|
572
|
+
scrollable(condition = true) { this._scrollable = condition; return this; }
|
|
317
573
|
getExposedMethods() { return ['getLazyLoadedTabBadges', 'getDeferredTabBadges']; }
|
|
318
574
|
getLazyLoadedTabBadges() { return {}; }
|
|
319
575
|
getDeferredTabBadges() { return {}; }
|
|
320
576
|
getChildSchemas() { return [childSchema(this, this.tabs)]; }
|
|
577
|
+
/** Active tab (1-based): the query string's tab when it names a visible tab, otherwise `activeTab()`. */
|
|
578
|
+
async getActiveTab(visible) {
|
|
579
|
+
if (this._tabQueryStringKey !== null) {
|
|
580
|
+
const requested = this.livewire._ctx?.req?.query?.[this._tabQueryStringKey];
|
|
581
|
+
const index = typeof requested === 'string' ? visible.findIndex((tab) => tab.getTabKey() === requested) : -1;
|
|
582
|
+
if (index !== -1)
|
|
583
|
+
return index + 1;
|
|
584
|
+
}
|
|
585
|
+
const active = await evaluate(this._activeTab, this.context());
|
|
586
|
+
return Number.isInteger(active) && active >= 1 ? active : 1;
|
|
587
|
+
}
|
|
321
588
|
async toHtml() {
|
|
322
589
|
// Building the child schema attaches the tabs to their container, which is what gives them their keys.
|
|
323
590
|
this.getChildSchemas();
|
|
591
|
+
const ctx = this.context();
|
|
324
592
|
const key = this.getKey();
|
|
325
593
|
const id = this.livewire.id;
|
|
326
|
-
const q = (k) =>
|
|
594
|
+
const q = (k) => js(k);
|
|
327
595
|
const tabId = (tab) => `fi-sc-tab-${createHash('md5').update(tab.getKey()).digest('hex')}`;
|
|
328
596
|
const visible = [];
|
|
329
597
|
for (const tab of this.tabs)
|
|
330
598
|
if (await tab.isVisible())
|
|
331
599
|
visible.push(tab);
|
|
332
|
-
const
|
|
600
|
+
const isContained = Boolean(await evaluate(this._contained, ctx));
|
|
601
|
+
const isVertical = Boolean(await evaluate(this._vertical, ctx));
|
|
602
|
+
const isScrollable = Boolean(await evaluate(this._scrollable, ctx));
|
|
603
|
+
const isTabPersisted = Boolean(await evaluate(this._persistTab, ctx));
|
|
604
|
+
const activeTab = await this.getActiveTab(visible);
|
|
605
|
+
const badges = await Promise.all(visible.map((tab) => tab.getBadge()));
|
|
606
|
+
const badgeHtml = (badge, withIcon) => {
|
|
607
|
+
const badgeIcon = withIcon && badge.icon ? icon(badge.icon, { size: 'sm' }) : '';
|
|
608
|
+
return `<span${badgeTooltipAttribute(badge.tooltip)} class="${cls(['fi-badge', 'fi-size-sm', ...badgeColorClasses(this, badge.color)])}">${badge.iconPosition === 'before' ? badgeIcon : ''}<span class="fi-badge-label-ctn"><span class="fi-badge-label">${e(badge.label)}</span></span>${badge.iconPosition === 'after' ? badgeIcon : ''}</span>`;
|
|
609
|
+
};
|
|
610
|
+
const buttons = visible.map((tab, index) => {
|
|
333
611
|
const k = tab.getTabKey();
|
|
612
|
+
const tabIcon = tab.getIcon() ? icon(tab.getIcon()) : '';
|
|
613
|
+
const badge = badges[index];
|
|
334
614
|
return `<button type="button" x-bind:class="{
|
|
335
615
|
'fi-active': isTabSelected(${q(k)}),
|
|
336
616
|
'fi-sc-tabs-header-overflow': overflowTabs.includes(${q(k)}),
|
|
337
617
|
'fi-sc-tabs-header-overflow-open': isOverflowOpen && overflowTabs.includes(${q(k)}),
|
|
338
|
-
}" id="${tabId(tab)}" role="tab" aria-controls="${e(tab.getKey())}" tabindex="-1" x-bind:tabindex="getTabIndex(${q(k)})" data-tab-key="${e(k)}" x-bind:data-tab-available="Boolean(true).toString()" x-bind:aria-selected="isTabSelected(${q(k)})" x-on:click="selectTab(${q(k)}, $event)" x-show="true" class="fi-tabs-item">${tab.
|
|
618
|
+
}" id="${tabId(tab)}" role="tab" aria-controls="${e(tab.getKey())}" tabindex="-1" x-bind:tabindex="getTabIndex(${q(k)})" data-tab-key="${e(k)}" x-bind:data-tab-available="Boolean(true).toString()" x-bind:aria-selected="isTabSelected(${q(k)})" x-on:click="selectTab(${q(k)}, $event)" x-show="true" class="fi-tabs-item">${tab.getIconPosition() === 'before' ? tabIcon : ''}<span class="fi-tabs-item-label">${e(tab.getLabel())}</span>${tab.getIconPosition() === 'after' ? tabIcon : ''}${badge ? badgeHtml(badge, true) : ''}</button>`;
|
|
339
619
|
});
|
|
620
|
+
// Not scrollable: tabs that do not fit move to an overflow dropdown, opened from a "more" trigger.
|
|
621
|
+
let overflow = '';
|
|
622
|
+
if (!isScrollable) {
|
|
623
|
+
const more = t('filament-schemas::components.tabs.actions.more.label');
|
|
624
|
+
const triggers = visible.map((tab, index) => {
|
|
625
|
+
const k = tab.getTabKey();
|
|
626
|
+
const badge = badges[index];
|
|
627
|
+
return `<button type="button" tabindex="-1" aria-hidden="true" data-tabs-overflow-trigger="data-tabs-overflow-trigger" data-tab-trigger-key="${e(k)}" aria-label="${e(`${more}: ${tab.getLabel()}`)}" x-show="overflowReady && overflowTabs.includes(${q(k)}) && isTabSelected(${q(k)})" class="fi-tabs-item fi-active">${icon('m-chevron-down')}<span class="fi-tabs-item-label">${e(tab.getLabel())}</span>${badge ? badgeHtml(badge, false) : ''}</button>`;
|
|
628
|
+
});
|
|
629
|
+
const items = visible.map((tab, index) => {
|
|
630
|
+
const k = tab.getTabKey();
|
|
631
|
+
const badge = badges[index];
|
|
632
|
+
const listBadge = badge ? `<span${badgeTooltipAttribute(badge.tooltip)} class="${cls(['fi-badge', ...badgeColorClasses(this, badge.color)])}">${e(badge.label)}</span>` : '';
|
|
633
|
+
return `<div data-tab-menu-key="${e(k)}" x-bind:aria-disabled="availableTabs.includes(${q(k)}) ? 'false' : 'true'" x-show="(true) && overflowTabs.includes(${q(k)})" class="fi-dropdown-list-item fi-sc-tabs-overflow-placeholder">${tab.getIcon() ? icon(tab.getIcon()) : ''}<span class="fi-dropdown-list-item-label">${e(tab.getLabel())}</span>${listBadge}</div>`;
|
|
634
|
+
});
|
|
635
|
+
overflow = `<div aria-hidden="true" class="fi-dropdown"><div x-on:mousedown="if ($event.button === 0) { $event.preventDefault(); toggleOverflow() }" class="fi-dropdown-trigger">${triggers.join('')}<button type="button" data-tabs-overflow-trigger data-tabs-overflow-ellipsis tabindex="-1" aria-hidden="true" aria-label="${e(more)}" class="fi-tabs-item" x-show="! overflowReady || (overflowTabs.some(key => availableTabs.includes(key)) && ! overflowTabs.includes(tab))"><span class="fi-tabs-item-label">${icon('m-ellipsis-horizontal')}</span></button></div><div x-cloak x-ref="overflowPanel" x-show="isOverflowOpen" aria-hidden="true" x-on:mousedown.self.prevent x-on:scroll="scheduleUpdate()" x-on:click.outside="if (! $el.closest('.fi-sc-tabs').querySelector('[x-ref=tablist]').contains($event.target) && ! getOverflowTrigger()?.contains($event.target)) dismissOverflow()" class="fi-dropdown-panel fi-sc-tabs-overflow-panel"><div class="fi-dropdown-list">${items.join('')}</div></div></div>`;
|
|
636
|
+
}
|
|
340
637
|
const panels = [];
|
|
341
638
|
for (const tab of visible) {
|
|
342
639
|
const k = tab.getTabKey();
|
|
343
640
|
panels.push(`<div aria-labelledby="${tabId(tab)}" id="${e(tab.getKey())}" role="tabpanel" tabindex="0" wire:key="${e(`${id}.${tab.getKey()}`)}.container" x-show="true" x-bind:class="{ 'fi-active': isTabSelected(${q(k)}) }" x-on:expand="revealTab(${q(k)})" x-data="filamentSchemaComponent(${e(js(tab.getAlpineScopeConfiguration()))})" class="fi-sc-tabs-tab">${await tab.toHtml()}</div>`);
|
|
344
641
|
}
|
|
642
|
+
const outer = new Attributes().merge({ id: key, 'wire:key': `${id}.${key}.container`, ...this._extraAttributes }).class(['fi-sc-tabs', { 'fi-contained': isContained, 'fi-vertical': isVertical }]);
|
|
643
|
+
const nav = new Attributes({ 'x-ref': 'tabsHeader' }).class(['fi-tabs', { 'fi-contained': isContained, 'fi-vertical': isVertical }])
|
|
644
|
+
.when(!isScrollable, (a) => a.merge({ 'x-bind:class': "{ 'fi-invisible': ! overflowReady }" }))
|
|
645
|
+
.merge({ 'x-cloak': true });
|
|
646
|
+
const tablistExtra = `${isVertical ? ` x-bind:aria-orientation="hasVisibleTabHeaders ? 'vertical' : null"` : ''} x-on:keydown="handleKeydown($event)"${isScrollable ? '' : ` x-on:dropdown-escape="if (isOverflowOpen) { $event.preventDefault(); $event.stopPropagation(); dismissOverflow() }" x-on:wheel="scrollOverflow($event)" x-on:touchstart="scrollOverflow($event)" x-on:touchmove="scrollOverflow($event)"`}`;
|
|
647
|
+
const persistedTab = isTabPersisted ? e(`$persist(null).as(${js(`filament:tabs:active:${key}`)})`) : 'null';
|
|
345
648
|
return `<div x-data="tabsSchemaComponent({
|
|
346
|
-
activeTab:
|
|
649
|
+
activeTab: ${activeTab},
|
|
347
650
|
hasTabPanels: true,
|
|
348
|
-
isScrollable: true,
|
|
349
|
-
isVertical: false,
|
|
350
|
-
isTabPersisted: false,
|
|
351
|
-
isTabPersistedInQueryString: false,
|
|
651
|
+
isScrollable: ${isScrollable ? 'true' : 'false'},
|
|
652
|
+
isVertical: ${isVertical ? 'true' : 'false'},
|
|
653
|
+
isTabPersisted: ${isTabPersisted ? 'true' : 'false'},
|
|
654
|
+
isTabPersistedInQueryString: ${this._tabQueryStringKey !== null ? 'true' : 'false'},
|
|
352
655
|
livewireId: ${e(js(id))},
|
|
353
656
|
schemaKey: ${e(js(key.split('.')[0]))},
|
|
354
|
-
tab:
|
|
355
|
-
tabQueryStringKey:
|
|
356
|
-
})" x-load x-load-src="${e(views.render('filament-schemas::asset', { path: 'tabs' }).trim())}"
|
|
657
|
+
tab: ${persistedTab},
|
|
658
|
+
tabQueryStringKey: ${e(js(this._tabQueryStringKey))},
|
|
659
|
+
})" x-load x-load-src="${e(views.render('filament-schemas::asset', { path: 'tabs' }).trim())}"${isScrollable ? '' : ' x-on:resize.window="scheduleUpdate(true)"'} wire:ignore.self ${outer.toHtml()}><div ${nav.toHtml()}><div x-ref="tablist" role="tablist" x-bind:role="hasVisibleTabHeaders ? 'tablist' : 'group'" aria-label="${e(this.label)}"${tablistExtra} class="fi-sc-tabs-tablist">${buttons.join('')}</div>${overflow}</div>${panels.join('')}</div>`;
|
|
357
660
|
}
|
|
358
661
|
}
|
|
359
662
|
/** Render hook injection point, using the host's scopes. Liberated from the container grid. */
|
|
@@ -389,27 +692,57 @@ export class Form extends Component {
|
|
|
389
692
|
return `<form ${attributes.toHtml()}>${await content.toHtml()}${await footer.toHtml()}</form>`;
|
|
390
693
|
}
|
|
391
694
|
}
|
|
392
|
-
/** Row of actions in a schema. */
|
|
393
695
|
export class Actions extends Component {
|
|
394
696
|
actions;
|
|
395
697
|
_fullWidth = false;
|
|
396
698
|
_alignment = null;
|
|
699
|
+
_label = null;
|
|
700
|
+
_beforeLabel = null;
|
|
701
|
+
_afterLabel = null;
|
|
702
|
+
_aboveContent = null;
|
|
703
|
+
_belowContent = null;
|
|
397
704
|
constructor(actions) { super(); this.actions = actions; }
|
|
398
705
|
static make(actions) { return configured(new Actions(actions)); }
|
|
399
706
|
fullWidth(condition = true) { this._fullWidth = condition; return this; }
|
|
400
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; }
|
|
401
720
|
async toHtml() {
|
|
402
721
|
const ctx = this.context();
|
|
722
|
+
const key = this.getKey();
|
|
723
|
+
const mountContext = key !== null ? { ...(this.livewire?.getActionMountContext?.() ?? {}), schemaComponent: key } : null;
|
|
403
724
|
const visible = [];
|
|
404
725
|
for (const a of this.actions)
|
|
405
726
|
if (await a.isVisible(ctx))
|
|
406
|
-
visible.push(await a.toHtml(ctx));
|
|
407
|
-
|
|
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)
|
|
408
733
|
return '';
|
|
409
734
|
const inner = cls(['fi-ac', { 'fi-width-full': this._fullWidth }, this._alignment ? `fi-align-${this._alignment}` : null]);
|
|
410
|
-
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>`;
|
|
411
736
|
}
|
|
412
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
|
+
}
|
|
413
746
|
let colorManager = null;
|
|
414
747
|
/** Color manager used outside a panel; inside a panel, the host panel's manager takes precedence. */
|
|
415
748
|
export const useColorManager = (manager) => { colorManager = manager; };
|
|
@@ -533,6 +866,7 @@ export class Text extends Component {
|
|
|
533
866
|
_copyableState = null;
|
|
534
867
|
_copyMessage = null;
|
|
535
868
|
_copyMessageDuration = 2000;
|
|
869
|
+
_isJs = false;
|
|
536
870
|
constructor(content) { super(); this._content = content; }
|
|
537
871
|
static make(content) { return configured(new Text(content)); }
|
|
538
872
|
content(content) { this._content = content; return this; }
|
|
@@ -556,7 +890,17 @@ export class Text extends Component {
|
|
|
556
890
|
copyableState(state) { this._copyableState = state; return this; }
|
|
557
891
|
copyMessage(message) { this._copyMessage = message; return this; }
|
|
558
892
|
copyMessageDuration(milliseconds) { this._copyMessageDuration = milliseconds; return this; }
|
|
559
|
-
|
|
893
|
+
/**
|
|
894
|
+
* The content is a JavaScript expression evaluated in the browser (`$get()` and `$state` are available), re-evaluated when the state changes.
|
|
895
|
+
* Only use it with expressions written by the developer, never with user input.
|
|
896
|
+
*/
|
|
897
|
+
js(condition = true) { this._isJs = condition; return this; }
|
|
898
|
+
async getContent() {
|
|
899
|
+
const content = await evaluate(this._content, this.context());
|
|
900
|
+
if (!this._isJs || content === null)
|
|
901
|
+
return content;
|
|
902
|
+
return new HtmlString(`<span x-text="() => eval(${js(content instanceof HtmlString ? content.html : content)})"></span>`);
|
|
903
|
+
}
|
|
560
904
|
async toHtml() {
|
|
561
905
|
const ctx = this.context();
|
|
562
906
|
const content = await this.getContent();
|
|
@@ -643,7 +987,8 @@ export class Image extends Component {
|
|
|
643
987
|
async toHtml() {
|
|
644
988
|
const ctx = this.context();
|
|
645
989
|
const url = String((await evaluate(this._url, ctx)) ?? '');
|
|
646
|
-
|
|
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))
|
|
647
992
|
return '';
|
|
648
993
|
const tooltip = tooltipExpression(await evaluate(this._tooltip, ctx));
|
|
649
994
|
const attributes = new Attributes(this._extraAttributes)
|
|
@@ -683,7 +1028,7 @@ export class EmptyState extends Component {
|
|
|
683
1028
|
_iconSize = null;
|
|
684
1029
|
_compact = false;
|
|
685
1030
|
_contained = true;
|
|
686
|
-
_headingTag =
|
|
1031
|
+
_headingTag = null;
|
|
687
1032
|
_footer = [];
|
|
688
1033
|
constructor(heading) { super(); this._heading = heading; }
|
|
689
1034
|
static make(heading) { return configured(new EmptyState(heading)); }
|
|
@@ -698,6 +1043,7 @@ export class EmptyState extends Component {
|
|
|
698
1043
|
/** `false`: no surrounding card. */
|
|
699
1044
|
contained(condition = true) { this._contained = condition; return this; }
|
|
700
1045
|
/** Heading tag (`h2` by default). */
|
|
1046
|
+
/** Heading tag; by default it follows the heading level of the schema (`h2` at the root). */
|
|
701
1047
|
headingTag(tag) { this._headingTag = tag; return this; }
|
|
702
1048
|
/** Footer: inline actions and components. */
|
|
703
1049
|
footer(items) { this._footer = items; return this; }
|
|
@@ -708,8 +1054,9 @@ export class EmptyState extends Component {
|
|
|
708
1054
|
const description = await evaluate(this._description, ctx);
|
|
709
1055
|
const footer = await renderInlineItems(this, this._footer);
|
|
710
1056
|
const iconColor = this._iconColor ?? 'primary';
|
|
1057
|
+
const headingTag = this._headingTag ?? this.getHeadingTag();
|
|
711
1058
|
const iconHtml = this._icon ? `<div class="${cls(['fi-empty-state-icon-bg', ...plainColorClasses(iconColor)])}">${icon(this._icon, { size: safeClass(this._iconSize) ?? 'lg', attributes: new Attributes().class(plainColorClasses(iconColor)) })}</div>` : '';
|
|
712
|
-
return `<div ${new Attributes(this._extraAttributes).class(['fi-sc-empty-state']).toHtml()}><section class="${cls(['fi-empty-state', { 'fi-compact': this._compact, 'fi-empty-state-not-contained': !this._contained }])}"><div class="fi-empty-state-content">${iconHtml}<div class="fi-empty-state-text-ctn"><${
|
|
1059
|
+
return `<div ${new Attributes(this._extraAttributes).class(['fi-sc-empty-state']).toHtml()}><section class="${cls(['fi-empty-state', { 'fi-compact': this._compact, 'fi-empty-state-not-contained': !this._contained }])}"><div class="fi-empty-state-content">${iconHtml}<div class="fi-empty-state-text-ctn"><${headingTag} class="fi-empty-state-heading">${e(await evaluate(this._heading, ctx))}</${headingTag}>${isBlank(description) ? '' : `<p class="fi-empty-state-description">${e(description)}</p>`}${footer ? `<footer class="fi-empty-state-footer">${footer}</footer>` : ''}</div></div></section></div>`;
|
|
713
1060
|
}
|
|
714
1061
|
}
|
|
715
1062
|
const CALLOUT_ICONS = { danger: 'o-x-circle', info: 'o-information-circle', success: 'o-check-circle', warning: 'o-exclamation-circle' };
|
|
@@ -730,6 +1077,7 @@ export class Callout extends Component {
|
|
|
730
1077
|
_footerActionsAlignment = 'start';
|
|
731
1078
|
_footer = null;
|
|
732
1079
|
_controlActions = [];
|
|
1080
|
+
_controls = null;
|
|
733
1081
|
constructor(heading = null) { super(); this._heading = heading; }
|
|
734
1082
|
static make(heading = null) { return configured(new Callout(heading)); }
|
|
735
1083
|
heading(heading) { this._heading = heading; return this; }
|
|
@@ -755,10 +1103,12 @@ export class Callout extends Component {
|
|
|
755
1103
|
footer(items) { this._footer = items; return this; }
|
|
756
1104
|
/** Small actions on the right of the callout (dismiss…). */
|
|
757
1105
|
controlActions(actions) { this._controlActions = [...this._controlActions, ...actions]; return this; }
|
|
1106
|
+
/** Free-form controls (components and actions) on the right of the callout, replacing the control actions. */
|
|
1107
|
+
controls(items) { this._controls = items; return this; }
|
|
758
1108
|
getColor() { return this._hasColor ? this._color : (this._color ?? this._status); }
|
|
759
1109
|
getIcon() { return this._icon ?? (this._status ? (CALLOUT_ICONS[this._status] ?? null) : null); }
|
|
760
1110
|
/** Callout actions, mounted on the root schema. */
|
|
761
|
-
getActions() { return [...(this._footer ? actionsOf(this._footer) : this._footerActions), ...this._controlActions]; }
|
|
1111
|
+
getActions() { return [...(this._footer ? actionsOf(this._footer) : this._footerActions), ...(this._controls ? actionsOf(this._controls) : this._controlActions)]; }
|
|
762
1112
|
async toHtml() {
|
|
763
1113
|
const ctx = this.context();
|
|
764
1114
|
const heading = await evaluate(this._heading, ctx);
|
|
@@ -767,7 +1117,7 @@ export class Callout extends Component {
|
|
|
767
1117
|
const iconName = this.getIcon();
|
|
768
1118
|
const iconColor = this._iconColor ?? this._status ?? color;
|
|
769
1119
|
const footer = this._footer ? await renderInlineItems(this, this._footer, { actionView: 'link' }) : await renderInlineItems(this, this._footerActions, { alignment: ['right', 'end'].includes(this._footerActionsAlignment) ? 'end' : this._footerActionsAlignment === 'justify' ? 'between' : this._footerActionsAlignment, actionView: 'link' });
|
|
770
|
-
const controls = await renderInlineItems(this, this._controlActions, { alignment: 'start', actionView: 'auto' });
|
|
1120
|
+
const controls = this._controls ? await renderInlineItems(this, this._controls, { actionView: 'auto' }) : await renderInlineItems(this, this._controlActions, { alignment: 'start', actionView: 'auto' });
|
|
771
1121
|
const statusKey = this._status ? `filament-schemas::components.callout.statuses.${this._status}` : null;
|
|
772
1122
|
const statusLabel = statusKey && t(statusKey) !== statusKey ? t(statusKey) : null;
|
|
773
1123
|
const statusHtml = statusLabel ? `<span class="fi-sr-only">${e(statusLabel)} </span>` : '';
|
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",
|
package/views/component.ejs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
<%# Grid column wrapper of a schema component. %>
|
|
2
|
-
<div<% if (key !== null) { %> wire:partial="schema-component::<%- key %>"<% } %><% if (isVisible) { %> x-data="<%- alpine %>"<% } %> <%- attributes %>><% if (isVisible) { %><div class="fi-sc-component"><%- html %></div><% } %></div>
|
|
2
|
+
<div<% if (key !== null) { %> wire:partial="schema-component::<%- key %>"<% } %><% if (isVisible) { %> x-data="<%- alpine %>"<% } %> <%- attributes %>><% if (isVisible) { %><div class="fi-sc-component<% if (locals.gridContainer) { %> fi-grid-ctn<% } %>"><%- html %></div><% } %></div>
|