@navette-js/schemas 1.0.0-beta.3 → 1.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +167 -19
- package/lib/index.js +347 -68
- 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. */
|
|
@@ -28,7 +31,11 @@ export declare abstract class Component {
|
|
|
28
31
|
protected _visible: Lazy<boolean, ComponentContext>;
|
|
29
32
|
protected _columnSpan: ColumnSpan;
|
|
30
33
|
protected _extraAttributes: Record<string, string>;
|
|
31
|
-
protected _columnStart:
|
|
34
|
+
protected _columnStart: ColumnPlacement[];
|
|
35
|
+
protected _columnOrder: ColumnPlacement[];
|
|
36
|
+
protected _gridContainer: Lazy<boolean, ComponentContext>;
|
|
37
|
+
protected _dense: boolean | null;
|
|
38
|
+
protected _gap: boolean | null;
|
|
32
39
|
container: Schema;
|
|
33
40
|
hidden(condition?: Lazy<boolean, ComponentContext>): this;
|
|
34
41
|
visible(condition?: Lazy<boolean, ComponentContext>): this;
|
|
@@ -39,8 +46,27 @@ export declare abstract class Component {
|
|
|
39
46
|
visibleOn(operations: string | string[]): this;
|
|
40
47
|
/** A number applies from the `lg` breakpoint; an object replaces the default entirely. Values may be closures. */
|
|
41
48
|
columnSpan(span: ColumnSpan): this;
|
|
42
|
-
/** Starting grid column
|
|
43
|
-
columnStart(start:
|
|
49
|
+
/** Starting grid column: a number applies from the `lg` breakpoint; an object sets it per breakpoint. Successive calls merge; `null` resets it. */
|
|
50
|
+
columnStart(start: ColumnPlacement): this;
|
|
51
|
+
/** Visual order in the grid, without changing the markup order: a number applies from `lg`; an object sets it per breakpoint. */
|
|
52
|
+
columnOrder(order: ColumnPlacement): this;
|
|
53
|
+
/** Makes the component a CSS container, so that grids inside it may use container breakpoints (`@md`, `@xl`…). */
|
|
54
|
+
gridContainer(condition?: Lazy<boolean, ComponentContext>): this;
|
|
55
|
+
/** Tighter spacing between the components of its child schemas (inherited by nested layouts). */
|
|
56
|
+
dense(condition?: boolean | null): this;
|
|
57
|
+
/** `false`: no space between the components of its child schemas (inherited by nested layouts). */
|
|
58
|
+
gap(condition?: boolean | null): this;
|
|
59
|
+
isDense(): boolean;
|
|
60
|
+
hasGap(): boolean;
|
|
61
|
+
/** Heading level of the component's own headings (`index` for a second heading). */
|
|
62
|
+
getHeadingLevel(index?: number): number;
|
|
63
|
+
/** `h1`…`h6`, or `p` beyond level 6. */
|
|
64
|
+
getHeadingTag(index?: number): string;
|
|
65
|
+
/** Number of heading levels the component adds above its child schemas (1 for a section with a heading). */
|
|
66
|
+
getHeadingsCount(): number;
|
|
67
|
+
getChildSchemaRootHeadingLevel(): number;
|
|
68
|
+
/** Full grid width on every screen size (`columnSpan('full')` only applies from `lg`). */
|
|
69
|
+
columnSpanFull(): this;
|
|
44
70
|
extraAttributes(attributes: Record<string, string>): this;
|
|
45
71
|
get livewire(): SchemaHost;
|
|
46
72
|
isLiberatedFromContainerGrid(): boolean;
|
|
@@ -74,6 +100,8 @@ export declare class Schema {
|
|
|
74
100
|
private _extraClass;
|
|
75
101
|
private _partial;
|
|
76
102
|
private _dense;
|
|
103
|
+
private _parentComponent;
|
|
104
|
+
private _rootHeadingLevel;
|
|
77
105
|
constructor(livewire: SchemaHost);
|
|
78
106
|
static make(livewire: SchemaHost): Schema;
|
|
79
107
|
key(key: string): this;
|
|
@@ -87,8 +115,16 @@ export declare class Schema {
|
|
|
87
115
|
record(record: unknown): this;
|
|
88
116
|
/** Class placed before the grid classes (e.g. `fi-section-content`). */
|
|
89
117
|
extraClass(name: string | null): this;
|
|
90
|
-
/** Tighter spacing between components (`fi-sc-dense`). */
|
|
91
|
-
dense(condition?: boolean): this;
|
|
118
|
+
/** Tighter spacing between components (`fi-sc-dense`). Without a value set, a child schema follows its parent component. */
|
|
119
|
+
dense(condition?: boolean | null): this;
|
|
120
|
+
/** Layout component the schema belongs to: spacing and heading levels are inherited from it. */
|
|
121
|
+
parentComponent(component: Component | null): this;
|
|
122
|
+
getParentComponent(): Component | null;
|
|
123
|
+
/** Heading level of the schema's top-level headings (`2` by default: `<h2>`); nested schemas follow their parent. */
|
|
124
|
+
rootHeadingLevel(level: number): this;
|
|
125
|
+
getRootHeadingLevel(): number;
|
|
126
|
+
isDense(): boolean;
|
|
127
|
+
hasGap(): boolean;
|
|
92
128
|
/** Partially rendered schema: its root carries `wire:partial="schema.<key>"`. */
|
|
93
129
|
partiallyRendered(condition?: boolean): this;
|
|
94
130
|
private _partialName;
|
|
@@ -96,7 +132,7 @@ export declare class Schema {
|
|
|
96
132
|
partialName(name: string | null): this;
|
|
97
133
|
private _gap;
|
|
98
134
|
/** `false`: components touch, with no gap (no `fi-sc-has-gap`), as in a FusedGroup. */
|
|
99
|
-
gap(condition?: boolean): this;
|
|
135
|
+
gap(condition?: boolean | null): this;
|
|
100
136
|
private _fieldWrapperView;
|
|
101
137
|
/**
|
|
102
138
|
* Field wrapper for this schema: `'filament-forms::plain-field-wrapper'` keeps only a screen-reader label
|
|
@@ -159,25 +195,79 @@ export declare class Flex extends Component {
|
|
|
159
195
|
toHtml(): Promise<string>;
|
|
160
196
|
}
|
|
161
197
|
export declare const slug: (text: string) => string;
|
|
162
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* Card grouping components, with an optional header (icon, heading, description, components after the header)
|
|
200
|
+
* and footer (actions or components).
|
|
201
|
+
*/
|
|
163
202
|
export declare class Section extends Component {
|
|
164
203
|
private children;
|
|
165
204
|
private _columns;
|
|
166
205
|
private _heading;
|
|
206
|
+
private _description;
|
|
207
|
+
private _icon;
|
|
208
|
+
private _iconColor;
|
|
209
|
+
private _iconSize;
|
|
167
210
|
private _afterHeader;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
211
|
+
private _headerActions;
|
|
212
|
+
private _footer;
|
|
213
|
+
private _footerActions;
|
|
214
|
+
private _footerActionsAlignment;
|
|
215
|
+
private _aside;
|
|
216
|
+
private _contentBefore;
|
|
217
|
+
private _compact;
|
|
218
|
+
private _secondary;
|
|
219
|
+
private _contained;
|
|
172
220
|
private _collapsible;
|
|
173
221
|
private _collapsed;
|
|
222
|
+
private _persistCollapsed;
|
|
223
|
+
constructor(heading?: Lazy<string | null, ComponentContext>);
|
|
224
|
+
static make(heading?: Lazy<string | null, ComponentContext>): Section;
|
|
225
|
+
schema(components: Component[]): this;
|
|
226
|
+
columns(columns: Record<string, number> | number | null): this;
|
|
227
|
+
/** Heading: a string or a function of the context object. */
|
|
228
|
+
heading(heading: Lazy<string | null, ComponentContext>): this;
|
|
229
|
+
/** Text under the heading. */
|
|
230
|
+
description(description: Lazy<string | null, ComponentContext>): this;
|
|
231
|
+
/** Header icon (Heroicons name). */
|
|
232
|
+
icon(name: Lazy<string | null, ComponentContext>): this;
|
|
233
|
+
/** Header icon color (`gray` by default). */
|
|
234
|
+
iconColor(color: Lazy<string | null, ComponentContext>): this;
|
|
235
|
+
/** Header icon size (`lg` by default). */
|
|
236
|
+
iconSize(size: string | null): this;
|
|
237
|
+
/** Heading and description on the left, the content in a card on the right (from the `md` breakpoint). */
|
|
238
|
+
aside(condition?: Lazy<boolean, ComponentContext>): this;
|
|
239
|
+
/** With `aside()`: the content before the heading and description. */
|
|
240
|
+
contentBefore(condition?: Lazy<boolean, ComponentContext>): this;
|
|
241
|
+
/** Smaller paddings, for nested sections. */
|
|
242
|
+
compact(condition?: Lazy<boolean, ComponentContext>): this;
|
|
243
|
+
/** Less contrasting background, for a section on a background of the same color (nested sections…). */
|
|
244
|
+
secondary(condition?: Lazy<boolean, ComponentContext>): this;
|
|
245
|
+
/** `false`: no card around the section. */
|
|
246
|
+
contained(condition?: Lazy<boolean, ComponentContext>): this;
|
|
174
247
|
/** Collapsible by clicking its header; `collapsed()` starts it collapsed. */
|
|
175
|
-
collapsible(condition?: boolean): this;
|
|
176
|
-
collapsed(condition?: boolean): this;
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
/**
|
|
248
|
+
collapsible(condition?: Lazy<boolean, ComponentContext>): this;
|
|
249
|
+
collapsed(condition?: Lazy<boolean, ComponentContext>): this;
|
|
250
|
+
/** Keeps the collapsed state in the browser's local storage, under the section's key. */
|
|
251
|
+
persistCollapsed(condition?: Lazy<boolean, ComponentContext>): this;
|
|
252
|
+
/** Components and actions shown after the heading; replaces `headerActions()`. */
|
|
253
|
+
afterHeader(items: InlineItem[] | null): this;
|
|
254
|
+
/** Actions shown after the heading (when `afterHeader()` is not used). */
|
|
255
|
+
headerActions(actions: Renderable[]): this;
|
|
256
|
+
/** Components and actions in the footer; replaces `footerActions()`. */
|
|
257
|
+
footer(items: InlineItem[] | null): this;
|
|
258
|
+
/** Actions in the footer (when `footer()` is not used). */
|
|
259
|
+
footerActions(actions: Renderable[]): this;
|
|
260
|
+
/** Footer actions alignment (`start`, `center`, `end`, `between`). */
|
|
261
|
+
footerActionsAlignment(alignment: string): this;
|
|
262
|
+
private getHeaderItems;
|
|
263
|
+
private getFooterItems;
|
|
264
|
+
/** Actions the section exposes for mounting (header and footer). */
|
|
180
265
|
getActions(): Renderable[];
|
|
266
|
+
/** Heading used for the key: a function heading is only used when it returns a string synchronously. */
|
|
267
|
+
private getHeadingForKey;
|
|
268
|
+
/** Key derived from the heading (`rate-limiting::data::section`), unless a `key()` is set. */
|
|
269
|
+
getKey(): string | null;
|
|
270
|
+
getHeadingsCount(): number;
|
|
181
271
|
getChildSchemas(): Schema[];
|
|
182
272
|
toHtml(): Promise<string>;
|
|
183
273
|
}
|
|
@@ -191,35 +281,83 @@ export declare class Html extends Component {
|
|
|
191
281
|
static make(content: string | ((ctx: ComponentContext) => string | Promise<string>)): Html;
|
|
192
282
|
toHtml(): Promise<string>;
|
|
193
283
|
}
|
|
194
|
-
/** A tab of a Tabs component: a label, an optional icon and its own schema. */
|
|
284
|
+
/** A tab of a Tabs component: a label, an optional icon and badge, and its own schema. */
|
|
195
285
|
export declare class Tab extends Component {
|
|
196
286
|
private children;
|
|
197
287
|
private _columns;
|
|
198
288
|
private _icon;
|
|
289
|
+
private _iconPosition;
|
|
290
|
+
private _badge;
|
|
291
|
+
private _badgeColor;
|
|
292
|
+
private _badgeIcon;
|
|
293
|
+
private _badgeIconPosition;
|
|
294
|
+
private _badgeTooltip;
|
|
199
295
|
private label;
|
|
200
296
|
constructor(label: string);
|
|
201
297
|
static make(label: string): Tab;
|
|
202
298
|
schema(components: Component[]): this;
|
|
203
299
|
columns(columns: Record<string, number> | number | null): this;
|
|
204
300
|
icon(name: string | null): this;
|
|
301
|
+
/** Icon before (default) or after the label. */
|
|
302
|
+
iconPosition(position: 'before' | 'after'): this;
|
|
303
|
+
/** Badge after the label (a count…); a function receives the context object. */
|
|
304
|
+
badge(badge: Lazy<string | number | null, ComponentContext>): this;
|
|
305
|
+
/** Badge color (`primary` by default). */
|
|
306
|
+
badgeColor(color: Lazy<string | null, ComponentContext>): this;
|
|
307
|
+
badgeIcon(name: string | null): this;
|
|
308
|
+
badgeIconPosition(position: 'before' | 'after'): this;
|
|
309
|
+
/** Tooltip of the badge (plain text). */
|
|
310
|
+
badgeTooltip(text: Lazy<string | null, ComponentContext>): this;
|
|
205
311
|
getLabel(): string;
|
|
206
312
|
getIcon(): string | null;
|
|
313
|
+
getIconPosition(): 'before' | 'after';
|
|
314
|
+
getBadge(): Promise<{
|
|
315
|
+
label: string;
|
|
316
|
+
color: string;
|
|
317
|
+
icon: string | null;
|
|
318
|
+
iconPosition: 'before' | 'after';
|
|
319
|
+
tooltip: string | null;
|
|
320
|
+
} | null>;
|
|
207
321
|
/** Browser-side tab key (no schema prefix): `::data::` follows the schema state path, and is dropped in infolists. */
|
|
208
322
|
getTabKey(): string;
|
|
209
323
|
getChildSchemas(): Schema[];
|
|
210
324
|
toHtml(): Promise<string>;
|
|
211
325
|
}
|
|
212
|
-
/**
|
|
326
|
+
/**
|
|
327
|
+
* Layout tabs: every panel is rendered, the browser only shows the active one.
|
|
328
|
+
* The active tab may be persisted in local storage (`persistTab()`) or in the URL (`persistTabInQueryString()`):
|
|
329
|
+
* the browser only selects a persisted key when it matches an existing tab.
|
|
330
|
+
*/
|
|
213
331
|
export declare class Tabs extends Component {
|
|
214
332
|
private tabs;
|
|
215
333
|
private label;
|
|
334
|
+
private _activeTab;
|
|
335
|
+
private _persistTab;
|
|
336
|
+
private _tabQueryStringKey;
|
|
337
|
+
private _vertical;
|
|
338
|
+
private _contained;
|
|
339
|
+
private _scrollable;
|
|
216
340
|
constructor(label: string);
|
|
217
341
|
static make(label: string): Tabs;
|
|
218
342
|
schema(tabs: Tab[]): this;
|
|
343
|
+
/** Tab active when the page loads (1-based, among the visible tabs). */
|
|
344
|
+
activeTab(tab: Lazy<number, ComponentContext>): this;
|
|
345
|
+
/** Remembers the active tab in the browser's local storage, under the key of the tabs. */
|
|
346
|
+
persistTab(condition?: Lazy<boolean, ComponentContext>): this;
|
|
347
|
+
/** Keeps the active tab in the URL's query string (`?tab=…`); `null` disables it. */
|
|
348
|
+
persistTabInQueryString(key?: string | null): this;
|
|
349
|
+
/** Tab headers on the side of the panels (from the `md` breakpoint). */
|
|
350
|
+
vertical(condition?: Lazy<boolean, ComponentContext>): this;
|
|
351
|
+
/** `false`: no card around the tabs. */
|
|
352
|
+
contained(condition?: Lazy<boolean, ComponentContext>): this;
|
|
353
|
+
/** `false`: tabs that do not fit are moved to a "more" dropdown instead of scrolling. */
|
|
354
|
+
scrollable(condition?: Lazy<boolean, ComponentContext>): this;
|
|
219
355
|
getExposedMethods(): string[];
|
|
220
356
|
getLazyLoadedTabBadges(): Record<string, unknown>;
|
|
221
357
|
getDeferredTabBadges(): Record<string, unknown>;
|
|
222
358
|
getChildSchemas(): Schema[];
|
|
359
|
+
/** Active tab (1-based): the query string's tab when it names a visible tab, otherwise `activeTab()`. */
|
|
360
|
+
private getActiveTab;
|
|
223
361
|
toHtml(): Promise<string>;
|
|
224
362
|
}
|
|
225
363
|
/** Render hook injection point, using the host's scopes. Liberated from the container grid. */
|
|
@@ -330,6 +468,7 @@ export declare class Text extends Component {
|
|
|
330
468
|
private _copyableState;
|
|
331
469
|
private _copyMessage;
|
|
332
470
|
private _copyMessageDuration;
|
|
471
|
+
private _isJs;
|
|
333
472
|
constructor(content: Lazy<string | HtmlString | null, ComponentContext>);
|
|
334
473
|
static make(content: Lazy<string | HtmlString | null, ComponentContext>): Text;
|
|
335
474
|
content(content: Lazy<string | HtmlString | null, ComponentContext>): this;
|
|
@@ -353,6 +492,11 @@ export declare class Text extends Component {
|
|
|
353
492
|
copyableState(state: Lazy<string | null, ComponentContext>): this;
|
|
354
493
|
copyMessage(message: string | null): this;
|
|
355
494
|
copyMessageDuration(milliseconds: number): this;
|
|
495
|
+
/**
|
|
496
|
+
* The content is a JavaScript expression evaluated in the browser (`$get()` and `$state` are available), re-evaluated when the state changes.
|
|
497
|
+
* Only use it with expressions written by the developer, never with user input.
|
|
498
|
+
*/
|
|
499
|
+
js(condition?: boolean): this;
|
|
356
500
|
getContent(): Promise<string | HtmlString | null>;
|
|
357
501
|
toHtml(): Promise<string>;
|
|
358
502
|
}
|
|
@@ -436,7 +580,8 @@ export declare class EmptyState extends Component {
|
|
|
436
580
|
/** `false`: no surrounding card. */
|
|
437
581
|
contained(condition?: boolean): this;
|
|
438
582
|
/** Heading tag (`h2` by default). */
|
|
439
|
-
|
|
583
|
+
/** Heading tag; by default it follows the heading level of the schema (`h2` at the root). */
|
|
584
|
+
headingTag(tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | null): this;
|
|
440
585
|
/** Footer: inline actions and components. */
|
|
441
586
|
footer(items: InlineItem[]): this;
|
|
442
587
|
/** Footer actions, mounted on the root schema. */
|
|
@@ -460,6 +605,7 @@ export declare class Callout extends Component {
|
|
|
460
605
|
private _footerActionsAlignment;
|
|
461
606
|
private _footer;
|
|
462
607
|
private _controlActions;
|
|
608
|
+
private _controls;
|
|
463
609
|
constructor(heading?: Lazy<string | null, ComponentContext>);
|
|
464
610
|
static make(heading?: Lazy<string | null, ComponentContext>): Callout;
|
|
465
611
|
heading(heading: Lazy<string | null, ComponentContext>): this;
|
|
@@ -485,6 +631,8 @@ export declare class Callout extends Component {
|
|
|
485
631
|
footer(items: InlineItem[] | null): this;
|
|
486
632
|
/** Small actions on the right of the callout (dismiss…). */
|
|
487
633
|
controlActions(actions: Renderable[]): this;
|
|
634
|
+
/** Free-form controls (components and actions) on the right of the callout, replacing the control actions. */
|
|
635
|
+
controls(items: InlineItem[] | null): this;
|
|
488
636
|
getColor(): string | null;
|
|
489
637
|
getIcon(): string | null;
|
|
490
638
|
/** Callout actions, mounted on the root schema. */
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,32 @@ 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. */
|
|
@@ -18,7 +44,11 @@ export class Component {
|
|
|
18
44
|
_visible = true;
|
|
19
45
|
_columnSpan = { default: 1 };
|
|
20
46
|
_extraAttributes = {};
|
|
21
|
-
_columnStart =
|
|
47
|
+
_columnStart = [];
|
|
48
|
+
_columnOrder = [];
|
|
49
|
+
_gridContainer = false;
|
|
50
|
+
_dense = null;
|
|
51
|
+
_gap = null;
|
|
22
52
|
container;
|
|
23
53
|
hidden(condition = true) { this._hidden = condition; return this; }
|
|
24
54
|
visible(condition = true) { this._visible = condition; return this; }
|
|
@@ -29,9 +59,25 @@ export class Component {
|
|
|
29
59
|
visibleOn(operations) { const list = [].concat(operations); return this.visible(({ operation }) => operation !== null && list.includes(operation)); }
|
|
30
60
|
/** A number applies from the `lg` breakpoint; an object replaces the default entirely. Values may be closures. */
|
|
31
61
|
columnSpan(span) { this._columnSpan = typeof span === 'object' ? span : { default: 1, lg: span }; return this; }
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
62
|
+
/** Starting grid column: a number applies from the `lg` breakpoint; an object sets it per breakpoint. Successive calls merge; `null` resets it. */
|
|
63
|
+
columnStart(start) { this._columnStart = start === null ? [] : [...this._columnStart, start]; return this; }
|
|
64
|
+
/** Visual order in the grid, without changing the markup order: a number applies from `lg`; an object sets it per breakpoint. */
|
|
65
|
+
columnOrder(order) { this._columnOrder = order === null ? [] : [...this._columnOrder, order]; return this; }
|
|
66
|
+
/** Makes the component a CSS container, so that grids inside it may use container breakpoints (`@md`, `@xl`…). */
|
|
67
|
+
gridContainer(condition = true) { this._gridContainer = condition; return this; }
|
|
68
|
+
/** Tighter spacing between the components of its child schemas (inherited by nested layouts). */
|
|
69
|
+
dense(condition = true) { this._dense = condition; return this; }
|
|
70
|
+
/** `false`: no space between the components of its child schemas (inherited by nested layouts). */
|
|
71
|
+
gap(condition = true) { this._gap = condition; return this; }
|
|
72
|
+
isDense() { return this._dense ?? this.container.isDense(); }
|
|
73
|
+
hasGap() { return this._gap ?? this.container.hasGap(); }
|
|
74
|
+
/** Heading level of the component's own headings (`index` for a second heading). */
|
|
75
|
+
getHeadingLevel(index = 0) { return Math.max(1, this.container.getRootHeadingLevel() + index); }
|
|
76
|
+
/** `h1`…`h6`, or `p` beyond level 6. */
|
|
77
|
+
getHeadingTag(index = 0) { const level = this.getHeadingLevel(index); return level > 6 ? 'p' : `h${level}`; }
|
|
78
|
+
/** Number of heading levels the component adds above its child schemas (1 for a section with a heading). */
|
|
79
|
+
getHeadingsCount() { return 0; }
|
|
80
|
+
getChildSchemaRootHeadingLevel() { return this.getHeadingLevel() + this.getHeadingsCount(); }
|
|
35
81
|
/** Full grid width on every screen size (`columnSpan('full')` only applies from `lg`). */
|
|
36
82
|
columnSpanFull() { return this.columnSpan({ default: 'full' }); }
|
|
37
83
|
extraAttributes(attributes) { Object.assign(this._extraAttributes, attributes); return this; }
|
|
@@ -69,16 +115,23 @@ export class Component {
|
|
|
69
115
|
const key = this.getKey();
|
|
70
116
|
const span = {};
|
|
71
117
|
const rawSpan = typeof this._columnSpan === 'object' ? this._columnSpan : { default: 1, lg: this._columnSpan };
|
|
118
|
+
const ctx = this.context();
|
|
72
119
|
for (const [breakpoint, value] of Object.entries(rawSpan))
|
|
73
|
-
span[breakpoint] = await evaluate(value,
|
|
120
|
+
span[breakpoint] = await evaluate(value, ctx);
|
|
121
|
+
const start = await resolvePlacement(this._columnStart, ctx, false);
|
|
122
|
+
const order = await resolvePlacement(this._columnOrder, ctx, true);
|
|
123
|
+
const placementClass = (name, values) => Object.keys(values).map((breakpoint) => (breakpoint === 'default' ? `fi-grid-col-${name}` : `${breakpoint}:fi-grid-col-${name}`));
|
|
124
|
+
const placementStyle = (name, values) => Object.entries(values).map(([breakpoint, value]) => `--col-${name}-${breakpointVariable(breakpoint)}: ${value}`);
|
|
74
125
|
const attributes = new Attributes()
|
|
75
|
-
.when(!this.container.isInline(), (a) => a.gridColumn(span, !isVisible)
|
|
76
|
-
.
|
|
126
|
+
.when(!this.container.isInline(), (a) => a.gridColumn(span, !isVisible)
|
|
127
|
+
.class([...placementClass('start', start), ...placementClass('order', order)])
|
|
128
|
+
.style([...placementStyle('start', start), ...placementStyle('order', order)]))
|
|
77
129
|
.merge({ 'wire:key': this.getLivewireKey() });
|
|
78
130
|
return views.render('filament-schemas::component', {
|
|
79
131
|
key, isVisible, attributes: attributes.toHtml(),
|
|
80
132
|
alpine: isVisible ? `filamentSchemaComponent(${js(this.getAlpineScopeConfiguration())})` : null,
|
|
81
133
|
html: isVisible ? await this.toHtml() : '',
|
|
134
|
+
gridContainer: isVisible && Boolean(await evaluate(this._gridContainer, ctx)),
|
|
82
135
|
});
|
|
83
136
|
}
|
|
84
137
|
}
|
|
@@ -94,7 +147,9 @@ export class Schema {
|
|
|
94
147
|
_record = null;
|
|
95
148
|
_extraClass = null;
|
|
96
149
|
_partial = false;
|
|
97
|
-
_dense =
|
|
150
|
+
_dense = null;
|
|
151
|
+
_parentComponent = null;
|
|
152
|
+
_rootHeadingLevel = 2;
|
|
98
153
|
constructor(livewire) { this.livewire = livewire; }
|
|
99
154
|
static make(livewire) { return configured(new Schema(livewire)); }
|
|
100
155
|
key(key) { this._key = key; return this; }
|
|
@@ -109,14 +164,23 @@ export class Schema {
|
|
|
109
164
|
record(record) { this._record = record; return this; }
|
|
110
165
|
/** Class placed before the grid classes (e.g. `fi-section-content`). */
|
|
111
166
|
extraClass(name) { this._extraClass = name; return this; }
|
|
112
|
-
/** Tighter spacing between components (`fi-sc-dense`). */
|
|
167
|
+
/** Tighter spacing between components (`fi-sc-dense`). Without a value set, a child schema follows its parent component. */
|
|
113
168
|
dense(condition = true) { this._dense = condition; return this; }
|
|
169
|
+
/** Layout component the schema belongs to: spacing and heading levels are inherited from it. */
|
|
170
|
+
parentComponent(component) { this._parentComponent = component; return this; }
|
|
171
|
+
getParentComponent() { return this._parentComponent; }
|
|
172
|
+
/** Heading level of the schema's top-level headings (`2` by default: `<h2>`); nested schemas follow their parent. */
|
|
173
|
+
rootHeadingLevel(level) { if (Number.isInteger(level) && level >= 1)
|
|
174
|
+
this._rootHeadingLevel = level; return this; }
|
|
175
|
+
getRootHeadingLevel() { return this._parentComponent ? this._parentComponent.getChildSchemaRootHeadingLevel() : this._rootHeadingLevel; }
|
|
176
|
+
isDense() { return this._dense ?? (this._inline || (this._parentComponent?.isDense() ?? false)); }
|
|
177
|
+
hasGap() { return this._gap ?? this._parentComponent?.hasGap() ?? true; }
|
|
114
178
|
/** Partially rendered schema: its root carries `wire:partial="schema.<key>"`. */
|
|
115
179
|
partiallyRendered(condition = true) { this._partial = condition; return this; }
|
|
116
180
|
_partialName = null;
|
|
117
181
|
/** Root `wire:partial` name when it differs from the schema key (filters form: `table-filters-form`). */
|
|
118
182
|
partialName(name) { this._partialName = name; return this; }
|
|
119
|
-
_gap =
|
|
183
|
+
_gap = null;
|
|
120
184
|
/** `false`: components touch, with no gap (no `fi-sc-has-gap`), as in a FusedGroup. */
|
|
121
185
|
gap(condition = true) { this._gap = condition; return this; }
|
|
122
186
|
_fieldWrapperView = null;
|
|
@@ -161,7 +225,7 @@ export class Schema {
|
|
|
161
225
|
'x-on:form-validation-error.window': this._root ? 'handleFormValidationError' : null,
|
|
162
226
|
'x-on:reset-schema-component-state.window': this._root ? 'handleClientSideStateReset' : null,
|
|
163
227
|
})
|
|
164
|
-
.class(['fi-sc', { 'fi-inline': this._inline }, { 'fi-sc-has-gap': this.
|
|
228
|
+
.class(['fi-sc', { 'fi-inline': this._inline }, { 'fi-sc-has-gap': this.hasGap() }, { 'fi-sc-dense': this.isDense() }]);
|
|
165
229
|
const parts = [];
|
|
166
230
|
for (const [component, isVisible] of withVisibility) {
|
|
167
231
|
if (!component.isLiberatedFromContainerGrid())
|
|
@@ -175,7 +239,7 @@ export class Schema {
|
|
|
175
239
|
/** Child schema of a layout component: shares the parent's key and state path, without being a root. */
|
|
176
240
|
export function childSchema(parent, components) {
|
|
177
241
|
const c = parent.container;
|
|
178
|
-
return new Schema(c.livewire).key(c.getKey()).statePath(c.getStatePath() || null).operation(c.getOperation()).record(c.getRecord()).root(false).components(components);
|
|
242
|
+
return new Schema(c.livewire).key(c.getKey()).statePath(c.getStatePath() || null).operation(c.getOperation()).record(c.getRecord()).root(false).parentComponent(parent).components(components);
|
|
179
243
|
}
|
|
180
244
|
/** Stateful group: its children live under `<container path>.<name>` (used by filter forms). */
|
|
181
245
|
export class StatefulGroup extends Component {
|
|
@@ -185,7 +249,7 @@ export class StatefulGroup extends Component {
|
|
|
185
249
|
hasStatePath() { return true; }
|
|
186
250
|
getChildSchemas() {
|
|
187
251
|
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)];
|
|
252
|
+
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
253
|
}
|
|
190
254
|
async toHtml() { return `<div id="${e(this.getKey())}">${await this.getChildSchemas()[0].toHtml()}</div>`; }
|
|
191
255
|
}
|
|
@@ -198,7 +262,10 @@ export class Group extends Component {
|
|
|
198
262
|
schema(components) { this.children = components; return this; }
|
|
199
263
|
columns(columns) { this._columns = columns; return this; }
|
|
200
264
|
getChildSchemas() { return [childSchema(this, this.children).columns(this._columns)]; }
|
|
201
|
-
async toHtml() {
|
|
265
|
+
async toHtml() {
|
|
266
|
+
const attributes = new Attributes().merge({ ...this._extraAttributes }).toHtml();
|
|
267
|
+
return `<div${attributes ? ` ${attributes}` : ''}>${await this.getChildSchemas()[0].toHtml()}</div>`;
|
|
268
|
+
}
|
|
202
269
|
}
|
|
203
270
|
/** Column grid: two columns from `lg` by default. */
|
|
204
271
|
export class Grid extends Group {
|
|
@@ -228,55 +295,155 @@ export class Flex extends Component {
|
|
|
228
295
|
}
|
|
229
296
|
}
|
|
230
297
|
export const slug = (text) => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
231
|
-
/**
|
|
298
|
+
/** Footer alignment (`start`, `center`, `end`, `between`; `right` and `justify` are aliases) as an inline schema alignment. */
|
|
299
|
+
const footerAlignment = (alignment) => (['right', 'end'].includes(alignment) ? 'end' : alignment === 'justify' ? 'between' : ['start', 'center', 'between'].includes(alignment) ? alignment : 'start');
|
|
300
|
+
/**
|
|
301
|
+
* Card grouping components, with an optional header (icon, heading, description, components after the header)
|
|
302
|
+
* and footer (actions or components).
|
|
303
|
+
*/
|
|
232
304
|
export class Section extends Component {
|
|
233
305
|
children = [];
|
|
234
306
|
_columns = {};
|
|
235
307
|
_heading;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
308
|
+
_description = null;
|
|
309
|
+
_icon = null;
|
|
310
|
+
_iconColor = null;
|
|
311
|
+
_iconSize = null;
|
|
312
|
+
_afterHeader = null;
|
|
313
|
+
_headerActions = [];
|
|
314
|
+
_footer = null;
|
|
315
|
+
_footerActions = [];
|
|
316
|
+
_footerActionsAlignment = 'start';
|
|
317
|
+
_aside = false;
|
|
318
|
+
_contentBefore = false;
|
|
319
|
+
_compact = false;
|
|
320
|
+
_secondary = false;
|
|
321
|
+
_contained = true;
|
|
322
|
+
_collapsible = null;
|
|
323
|
+
_collapsed = false;
|
|
324
|
+
_persistCollapsed = false;
|
|
325
|
+
constructor(heading = null) { super(); this._heading = heading; }
|
|
244
326
|
static make(heading = null) { return configured(new Section(heading)); }
|
|
245
327
|
schema(components) { this.children = components; return this; }
|
|
246
328
|
columns(columns) { this._columns = columns; return this; }
|
|
247
|
-
|
|
248
|
-
|
|
329
|
+
/** Heading: a string or a function of the context object. */
|
|
330
|
+
heading(heading) { this._heading = heading; return this; }
|
|
331
|
+
/** Text under the heading. */
|
|
332
|
+
description(description) { this._description = description; return this; }
|
|
333
|
+
/** Header icon (Heroicons name). */
|
|
334
|
+
icon(name) { this._icon = name; return this; }
|
|
335
|
+
/** Header icon color (`gray` by default). */
|
|
336
|
+
iconColor(color) { this._iconColor = color; return this; }
|
|
337
|
+
/** Header icon size (`lg` by default). */
|
|
338
|
+
iconSize(size) { this._iconSize = size; return this; }
|
|
339
|
+
/** Heading and description on the left, the content in a card on the right (from the `md` breakpoint). */
|
|
340
|
+
aside(condition = true) { this._aside = condition; return this; }
|
|
341
|
+
/** With `aside()`: the content before the heading and description. */
|
|
342
|
+
contentBefore(condition = true) { this._contentBefore = condition; return this; }
|
|
343
|
+
/** Smaller paddings, for nested sections. */
|
|
344
|
+
compact(condition = true) { this._compact = condition; return this; }
|
|
345
|
+
/** Less contrasting background, for a section on a background of the same color (nested sections…). */
|
|
346
|
+
secondary(condition = true) { this._secondary = condition; return this; }
|
|
347
|
+
/** `false`: no card around the section. */
|
|
348
|
+
contained(condition = true) { this._contained = condition; return this; }
|
|
249
349
|
/** Collapsible by clicking its header; `collapsed()` starts it collapsed. */
|
|
250
350
|
collapsible(condition = true) { this._collapsible = condition; return this; }
|
|
251
|
-
collapsed(condition = true) { this._collapsed = condition; if (
|
|
252
|
-
this._collapsible =
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
|
|
351
|
+
collapsed(condition = true) { this._collapsed = condition; if (this._collapsible === null)
|
|
352
|
+
this._collapsible = condition; return this; }
|
|
353
|
+
/** Keeps the collapsed state in the browser's local storage, under the section's key. */
|
|
354
|
+
persistCollapsed(condition = true) { this._persistCollapsed = condition; return this; }
|
|
355
|
+
/** Components and actions shown after the heading; replaces `headerActions()`. */
|
|
356
|
+
afterHeader(items) { this._afterHeader = items; return this; }
|
|
357
|
+
/** Actions shown after the heading (when `afterHeader()` is not used). */
|
|
358
|
+
headerActions(actions) { this._headerActions = [...this._headerActions, ...actions]; return this; }
|
|
359
|
+
/** Components and actions in the footer; replaces `footerActions()`. */
|
|
360
|
+
footer(items) { this._footer = items; return this; }
|
|
361
|
+
/** Actions in the footer (when `footer()` is not used). */
|
|
362
|
+
footerActions(actions) { this._footerActions = [...this._footerActions, ...actions]; return this; }
|
|
363
|
+
/** Footer actions alignment (`start`, `center`, `end`, `between`). */
|
|
364
|
+
footerActionsAlignment(alignment) { this._footerActionsAlignment = alignment; return this; }
|
|
365
|
+
getHeaderItems() { return this._afterHeader ?? this._headerActions; }
|
|
366
|
+
getFooterItems() { return this._footer ?? this._footerActions; }
|
|
367
|
+
/** Actions the section exposes for mounting (header and footer). */
|
|
368
|
+
getActions() { return [...actionsOf(this.getHeaderItems()), ...actionsOf(this.getFooterItems())]; }
|
|
369
|
+
/** Heading used for the key: a function heading is only used when it returns a string synchronously. */
|
|
370
|
+
getHeadingForKey() {
|
|
371
|
+
if (typeof this._heading !== 'function')
|
|
372
|
+
return this._heading;
|
|
373
|
+
let value;
|
|
374
|
+
try {
|
|
375
|
+
value = this._heading(this.context());
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
// An async heading is not awaited here: its failure is reported when the section renders.
|
|
381
|
+
if (value instanceof Promise)
|
|
382
|
+
value.catch(() => { });
|
|
383
|
+
return typeof value === 'string' ? value : null;
|
|
384
|
+
}
|
|
385
|
+
/** Key derived from the heading (`rate-limiting::data::section`), unless a `key()` is set. */
|
|
386
|
+
getKey() {
|
|
387
|
+
if (this._keyName !== null)
|
|
388
|
+
return super.getKey();
|
|
389
|
+
const heading = this.getHeadingForKey();
|
|
390
|
+
if (isBlank(heading))
|
|
391
|
+
return null;
|
|
392
|
+
const statePath = this.container.getStatePath();
|
|
393
|
+
return `${this.container.getKey()}.${slug(heading)}::${statePath ? `${statePath}::` : ''}section`;
|
|
394
|
+
}
|
|
395
|
+
getHeadingsCount() { return typeof this._heading === 'function' || !isBlank(this._heading) ? 1 : 0; }
|
|
257
396
|
getChildSchemas() { return [childSchema(this, this.children).columns(this._columns).extraClass('fi-section-content')]; }
|
|
258
397
|
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
398
|
const ctx = this.context();
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
399
|
+
const [content] = this.getChildSchemas();
|
|
400
|
+
const id = this.getKey();
|
|
401
|
+
const heading = await evaluate(this._heading, ctx);
|
|
402
|
+
const description = await evaluate(this._description, ctx);
|
|
403
|
+
const iconName = await evaluate(this._icon, ctx);
|
|
404
|
+
const isAside = Boolean(await evaluate(this._aside, ctx));
|
|
405
|
+
const isCollapsible = Boolean(await evaluate(this._collapsible ?? false, ctx)) && !isAside;
|
|
406
|
+
const isCollapsed = Boolean(await evaluate(this._collapsed, ctx));
|
|
407
|
+
const shouldPersistCollapsed = Boolean(await evaluate(this._persistCollapsed, ctx));
|
|
408
|
+
const hasHeading = !isBlank(heading), hasDescription = !isBlank(description), hasIcon = !isBlank(iconName);
|
|
409
|
+
const afterHeader = await renderInlineItems(this, this.getHeaderItems(), { alignment: 'end' });
|
|
410
|
+
const footer = await renderInlineItems(this, this.getFooterItems(), { alignment: this._footer === null ? footerAlignment(this._footerActionsAlignment) : null });
|
|
411
|
+
const contentHtml = await content.toHtml();
|
|
412
|
+
const hasHeader = hasIcon || hasHeading || hasDescription || isCollapsible || afterHeader !== '';
|
|
413
|
+
const hasContent = contentHtml !== '' || footer !== '';
|
|
414
|
+
const contentId = id !== null && hasContent ? `${id}-content` : null;
|
|
415
|
+
const headingId = id !== null && hasHeading ? `${id}-heading` : null;
|
|
416
|
+
const descriptionId = id !== null && hasDescription ? `${id}-description` : null;
|
|
417
|
+
const outer = new Attributes().merge({ id, ...this._extraAttributes }).class(['fi-sc-section']);
|
|
273
418
|
// 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
|
-
|
|
419
|
+
// A persisted state lives in local storage, under the section's key (or its element id).
|
|
420
|
+
const idTest = `if ($event.detail.id == (${js(id)} ?? $el.id))`;
|
|
421
|
+
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'"` : '';
|
|
422
|
+
const collapsedState = shouldPersistCollapsed ? e(`$persist(${isCollapsed ? 'true' : 'false'}).as(\`filament:section:collapsed:\${${js(id)} ?? $el.id}\`)`) : (isCollapsed ? 'true' : 'false');
|
|
423
|
+
const sectionClass = cls(['fi-section', {
|
|
424
|
+
'fi-section-not-contained': !(await evaluate(this._contained, ctx)),
|
|
425
|
+
'fi-section-has-content-before': Boolean(await evaluate(this._contentBefore, ctx)),
|
|
426
|
+
'fi-section-has-header': hasHeader,
|
|
427
|
+
'fi-aside': isAside,
|
|
428
|
+
'fi-compact': Boolean(await evaluate(this._compact, ctx)),
|
|
429
|
+
'fi-collapsible': isCollapsible,
|
|
430
|
+
'fi-collapsed': isCollapsible && isCollapsed,
|
|
431
|
+
'fi-secondary': Boolean(await evaluate(this._secondary, ctx)),
|
|
432
|
+
}]);
|
|
433
|
+
let header = '';
|
|
434
|
+
if (hasHeader) {
|
|
435
|
+
const iconColor = (await evaluate(this._iconColor, ctx)) ?? 'gray';
|
|
436
|
+
const iconHtml = hasIcon ? icon(iconName, { size: safeClass(this._iconSize) ?? 'lg', attributes: new Attributes().class(plainColorClasses(iconColor)) }) : '';
|
|
437
|
+
const tag = this.getHeadingTag();
|
|
438
|
+
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>` : '';
|
|
439
|
+
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>` : '';
|
|
440
|
+
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>` : '';
|
|
441
|
+
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>`;
|
|
442
|
+
}
|
|
443
|
+
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>` : '';
|
|
444
|
+
return `<div ${outer.toHtml()}><section x-data="{
|
|
445
|
+
isCollapsed: ${collapsedState},
|
|
446
|
+
}"${collapse}${headingId ? ` aria-labelledby="${e(headingId)}"` : ''}${descriptionId ? ` aria-describedby="${e(descriptionId)}"` : ''} class="${sectionClass}">${header}${body}</section></div>`;
|
|
280
447
|
}
|
|
281
448
|
}
|
|
282
449
|
/**
|
|
@@ -289,71 +456,167 @@ export class Html extends Component {
|
|
|
289
456
|
static make(content) { return new Html(content); }
|
|
290
457
|
async toHtml() { return typeof this.content === 'function' ? this.content(this.context()) : this.content; }
|
|
291
458
|
}
|
|
292
|
-
/** A tab of a Tabs component: a label, an optional icon and its own schema. */
|
|
459
|
+
/** A tab of a Tabs component: a label, an optional icon and badge, and its own schema. */
|
|
293
460
|
export class Tab extends Component {
|
|
294
461
|
children = [];
|
|
295
462
|
_columns = {};
|
|
296
463
|
_icon = null;
|
|
464
|
+
_iconPosition = 'before';
|
|
465
|
+
_badge = null;
|
|
466
|
+
_badgeColor = null;
|
|
467
|
+
_badgeIcon = null;
|
|
468
|
+
_badgeIconPosition = 'before';
|
|
469
|
+
_badgeTooltip = null;
|
|
297
470
|
label;
|
|
298
471
|
constructor(label) { super(); this.label = label; this._keyName = `${slug(label)}::data::tab`; }
|
|
299
472
|
static make(label) { return configured(new Tab(label)); }
|
|
300
473
|
schema(components) { this.children = components; return this; }
|
|
301
474
|
columns(columns) { this._columns = columns; return this; }
|
|
302
475
|
icon(name) { this._icon = name; return this; }
|
|
476
|
+
/** Icon before (default) or after the label. */
|
|
477
|
+
iconPosition(position) { this._iconPosition = position === 'after' ? 'after' : 'before'; return this; }
|
|
478
|
+
/** Badge after the label (a count…); a function receives the context object. */
|
|
479
|
+
badge(badge) { this._badge = badge; return this; }
|
|
480
|
+
/** Badge color (`primary` by default). */
|
|
481
|
+
badgeColor(color) { this._badgeColor = color; return this; }
|
|
482
|
+
badgeIcon(name) { this._badgeIcon = name; return this; }
|
|
483
|
+
badgeIconPosition(position) { this._badgeIconPosition = position === 'after' ? 'after' : 'before'; return this; }
|
|
484
|
+
/** Tooltip of the badge (plain text). */
|
|
485
|
+
badgeTooltip(text) { this._badgeTooltip = text; return this; }
|
|
303
486
|
getLabel() { return this.label; }
|
|
304
487
|
getIcon() { return this._icon; }
|
|
488
|
+
getIconPosition() { return this._iconPosition; }
|
|
489
|
+
async getBadge() {
|
|
490
|
+
const ctx = this.context();
|
|
491
|
+
const badge = await evaluate(this._badge, ctx);
|
|
492
|
+
if (isBlank(badge))
|
|
493
|
+
return null;
|
|
494
|
+
return { label: String(badge), color: (await evaluate(this._badgeColor, ctx)) ?? 'primary', icon: this._badgeIcon, iconPosition: this._badgeIconPosition, tooltip: await evaluate(this._badgeTooltip, ctx) };
|
|
495
|
+
}
|
|
305
496
|
/** Browser-side tab key (no schema prefix): `::data::` follows the schema state path, and is dropped in infolists. */
|
|
306
497
|
getTabKey() { const statePath = this.container.getStatePath(); return this._keyName.replace('::data::', statePath ? `::${statePath}::` : '::'); }
|
|
307
498
|
getChildSchemas() { return [childSchema(this, this.children).columns(this._columns)]; }
|
|
308
499
|
async toHtml() { return this.getChildSchemas()[0].toHtml(); }
|
|
309
500
|
}
|
|
310
|
-
/**
|
|
501
|
+
/** Badge colors of a tab: computed shades when a color manager is available. */
|
|
502
|
+
const badgeColorClasses = (component, color) => colorsFor(component)?.getComponentClasses('badge', color) ?? plainColorClasses(color);
|
|
503
|
+
const badgeTooltipAttribute = (tooltip) => (tooltip ? ` x-tooltip="${e(`{ content: ${js(tooltip)}, theme: $store.theme, allowHTML: false }`)}"` : '');
|
|
504
|
+
/**
|
|
505
|
+
* Layout tabs: every panel is rendered, the browser only shows the active one.
|
|
506
|
+
* The active tab may be persisted in local storage (`persistTab()`) or in the URL (`persistTabInQueryString()`):
|
|
507
|
+
* the browser only selects a persisted key when it matches an existing tab.
|
|
508
|
+
*/
|
|
311
509
|
export class Tabs extends Component {
|
|
312
510
|
tabs = [];
|
|
313
511
|
label;
|
|
512
|
+
_activeTab = 1;
|
|
513
|
+
_persistTab = false;
|
|
514
|
+
_tabQueryStringKey = null;
|
|
515
|
+
_vertical = false;
|
|
516
|
+
_contained = true;
|
|
517
|
+
_scrollable = true;
|
|
314
518
|
constructor(label) { super(); this.label = label; this._keyName = `${slug(label)}::data::tabs`; }
|
|
315
519
|
static make(label) { return configured(new Tabs(label)); }
|
|
316
520
|
schema(tabs) { this.tabs = tabs; return this; }
|
|
521
|
+
/** Tab active when the page loads (1-based, among the visible tabs). */
|
|
522
|
+
activeTab(tab) { this._activeTab = tab; return this; }
|
|
523
|
+
/** Remembers the active tab in the browser's local storage, under the key of the tabs. */
|
|
524
|
+
persistTab(condition = true) { this._persistTab = condition; return this; }
|
|
525
|
+
/** Keeps the active tab in the URL's query string (`?tab=…`); `null` disables it. */
|
|
526
|
+
persistTabInQueryString(key = 'tab') { this._tabQueryStringKey = key === null || key.trim() === '' ? null : key; return this; }
|
|
527
|
+
/** Tab headers on the side of the panels (from the `md` breakpoint). */
|
|
528
|
+
vertical(condition = true) { this._vertical = condition; return this; }
|
|
529
|
+
/** `false`: no card around the tabs. */
|
|
530
|
+
contained(condition = true) { this._contained = condition; return this; }
|
|
531
|
+
/** `false`: tabs that do not fit are moved to a "more" dropdown instead of scrolling. */
|
|
532
|
+
scrollable(condition = true) { this._scrollable = condition; return this; }
|
|
317
533
|
getExposedMethods() { return ['getLazyLoadedTabBadges', 'getDeferredTabBadges']; }
|
|
318
534
|
getLazyLoadedTabBadges() { return {}; }
|
|
319
535
|
getDeferredTabBadges() { return {}; }
|
|
320
536
|
getChildSchemas() { return [childSchema(this, this.tabs)]; }
|
|
537
|
+
/** Active tab (1-based): the query string's tab when it names a visible tab, otherwise `activeTab()`. */
|
|
538
|
+
async getActiveTab(visible) {
|
|
539
|
+
if (this._tabQueryStringKey !== null) {
|
|
540
|
+
const requested = this.livewire._ctx?.req?.query?.[this._tabQueryStringKey];
|
|
541
|
+
const index = typeof requested === 'string' ? visible.findIndex((tab) => tab.getTabKey() === requested) : -1;
|
|
542
|
+
if (index !== -1)
|
|
543
|
+
return index + 1;
|
|
544
|
+
}
|
|
545
|
+
const active = await evaluate(this._activeTab, this.context());
|
|
546
|
+
return Number.isInteger(active) && active >= 1 ? active : 1;
|
|
547
|
+
}
|
|
321
548
|
async toHtml() {
|
|
322
549
|
// Building the child schema attaches the tabs to their container, which is what gives them their keys.
|
|
323
550
|
this.getChildSchemas();
|
|
551
|
+
const ctx = this.context();
|
|
324
552
|
const key = this.getKey();
|
|
325
553
|
const id = this.livewire.id;
|
|
326
|
-
const q = (k) =>
|
|
554
|
+
const q = (k) => js(k);
|
|
327
555
|
const tabId = (tab) => `fi-sc-tab-${createHash('md5').update(tab.getKey()).digest('hex')}`;
|
|
328
556
|
const visible = [];
|
|
329
557
|
for (const tab of this.tabs)
|
|
330
558
|
if (await tab.isVisible())
|
|
331
559
|
visible.push(tab);
|
|
332
|
-
const
|
|
560
|
+
const isContained = Boolean(await evaluate(this._contained, ctx));
|
|
561
|
+
const isVertical = Boolean(await evaluate(this._vertical, ctx));
|
|
562
|
+
const isScrollable = Boolean(await evaluate(this._scrollable, ctx));
|
|
563
|
+
const isTabPersisted = Boolean(await evaluate(this._persistTab, ctx));
|
|
564
|
+
const activeTab = await this.getActiveTab(visible);
|
|
565
|
+
const badges = await Promise.all(visible.map((tab) => tab.getBadge()));
|
|
566
|
+
const badgeHtml = (badge, withIcon) => {
|
|
567
|
+
const badgeIcon = withIcon && badge.icon ? icon(badge.icon, { size: 'sm' }) : '';
|
|
568
|
+
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>`;
|
|
569
|
+
};
|
|
570
|
+
const buttons = visible.map((tab, index) => {
|
|
333
571
|
const k = tab.getTabKey();
|
|
572
|
+
const tabIcon = tab.getIcon() ? icon(tab.getIcon()) : '';
|
|
573
|
+
const badge = badges[index];
|
|
334
574
|
return `<button type="button" x-bind:class="{
|
|
335
575
|
'fi-active': isTabSelected(${q(k)}),
|
|
336
576
|
'fi-sc-tabs-header-overflow': overflowTabs.includes(${q(k)}),
|
|
337
577
|
'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.
|
|
578
|
+
}" 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
579
|
});
|
|
580
|
+
// Not scrollable: tabs that do not fit move to an overflow dropdown, opened from a "more" trigger.
|
|
581
|
+
let overflow = '';
|
|
582
|
+
if (!isScrollable) {
|
|
583
|
+
const more = t('filament-schemas::components.tabs.actions.more.label');
|
|
584
|
+
const triggers = visible.map((tab, index) => {
|
|
585
|
+
const k = tab.getTabKey();
|
|
586
|
+
const badge = badges[index];
|
|
587
|
+
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>`;
|
|
588
|
+
});
|
|
589
|
+
const items = visible.map((tab, index) => {
|
|
590
|
+
const k = tab.getTabKey();
|
|
591
|
+
const badge = badges[index];
|
|
592
|
+
const listBadge = badge ? `<span${badgeTooltipAttribute(badge.tooltip)} class="${cls(['fi-badge', ...badgeColorClasses(this, badge.color)])}">${e(badge.label)}</span>` : '';
|
|
593
|
+
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>`;
|
|
594
|
+
});
|
|
595
|
+
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>`;
|
|
596
|
+
}
|
|
340
597
|
const panels = [];
|
|
341
598
|
for (const tab of visible) {
|
|
342
599
|
const k = tab.getTabKey();
|
|
343
600
|
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
601
|
}
|
|
602
|
+
const outer = new Attributes().merge({ id: key, 'wire:key': `${id}.${key}.container`, ...this._extraAttributes }).class(['fi-sc-tabs', { 'fi-contained': isContained, 'fi-vertical': isVertical }]);
|
|
603
|
+
const nav = new Attributes({ 'x-ref': 'tabsHeader' }).class(['fi-tabs', { 'fi-contained': isContained, 'fi-vertical': isVertical }])
|
|
604
|
+
.when(!isScrollable, (a) => a.merge({ 'x-bind:class': "{ 'fi-invisible': ! overflowReady }" }))
|
|
605
|
+
.merge({ 'x-cloak': true });
|
|
606
|
+
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)"`}`;
|
|
607
|
+
const persistedTab = isTabPersisted ? e(`$persist(null).as(${js(`filament:tabs:active:${key}`)})`) : 'null';
|
|
345
608
|
return `<div x-data="tabsSchemaComponent({
|
|
346
|
-
activeTab:
|
|
609
|
+
activeTab: ${activeTab},
|
|
347
610
|
hasTabPanels: true,
|
|
348
|
-
isScrollable: true,
|
|
349
|
-
isVertical: false,
|
|
350
|
-
isTabPersisted: false,
|
|
351
|
-
isTabPersistedInQueryString: false,
|
|
611
|
+
isScrollable: ${isScrollable ? 'true' : 'false'},
|
|
612
|
+
isVertical: ${isVertical ? 'true' : 'false'},
|
|
613
|
+
isTabPersisted: ${isTabPersisted ? 'true' : 'false'},
|
|
614
|
+
isTabPersistedInQueryString: ${this._tabQueryStringKey !== null ? 'true' : 'false'},
|
|
352
615
|
livewireId: ${e(js(id))},
|
|
353
616
|
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())}"
|
|
617
|
+
tab: ${persistedTab},
|
|
618
|
+
tabQueryStringKey: ${e(js(this._tabQueryStringKey))},
|
|
619
|
+
})" 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
620
|
}
|
|
358
621
|
}
|
|
359
622
|
/** Render hook injection point, using the host's scopes. Liberated from the container grid. */
|
|
@@ -533,6 +796,7 @@ export class Text extends Component {
|
|
|
533
796
|
_copyableState = null;
|
|
534
797
|
_copyMessage = null;
|
|
535
798
|
_copyMessageDuration = 2000;
|
|
799
|
+
_isJs = false;
|
|
536
800
|
constructor(content) { super(); this._content = content; }
|
|
537
801
|
static make(content) { return configured(new Text(content)); }
|
|
538
802
|
content(content) { this._content = content; return this; }
|
|
@@ -556,7 +820,17 @@ export class Text extends Component {
|
|
|
556
820
|
copyableState(state) { this._copyableState = state; return this; }
|
|
557
821
|
copyMessage(message) { this._copyMessage = message; return this; }
|
|
558
822
|
copyMessageDuration(milliseconds) { this._copyMessageDuration = milliseconds; return this; }
|
|
559
|
-
|
|
823
|
+
/**
|
|
824
|
+
* The content is a JavaScript expression evaluated in the browser (`$get()` and `$state` are available), re-evaluated when the state changes.
|
|
825
|
+
* Only use it with expressions written by the developer, never with user input.
|
|
826
|
+
*/
|
|
827
|
+
js(condition = true) { this._isJs = condition; return this; }
|
|
828
|
+
async getContent() {
|
|
829
|
+
const content = await evaluate(this._content, this.context());
|
|
830
|
+
if (!this._isJs || content === null)
|
|
831
|
+
return content;
|
|
832
|
+
return new HtmlString(`<span x-text="() => eval(${js(content instanceof HtmlString ? content.html : content)})"></span>`);
|
|
833
|
+
}
|
|
560
834
|
async toHtml() {
|
|
561
835
|
const ctx = this.context();
|
|
562
836
|
const content = await this.getContent();
|
|
@@ -683,7 +957,7 @@ export class EmptyState extends Component {
|
|
|
683
957
|
_iconSize = null;
|
|
684
958
|
_compact = false;
|
|
685
959
|
_contained = true;
|
|
686
|
-
_headingTag =
|
|
960
|
+
_headingTag = null;
|
|
687
961
|
_footer = [];
|
|
688
962
|
constructor(heading) { super(); this._heading = heading; }
|
|
689
963
|
static make(heading) { return configured(new EmptyState(heading)); }
|
|
@@ -698,6 +972,7 @@ export class EmptyState extends Component {
|
|
|
698
972
|
/** `false`: no surrounding card. */
|
|
699
973
|
contained(condition = true) { this._contained = condition; return this; }
|
|
700
974
|
/** Heading tag (`h2` by default). */
|
|
975
|
+
/** Heading tag; by default it follows the heading level of the schema (`h2` at the root). */
|
|
701
976
|
headingTag(tag) { this._headingTag = tag; return this; }
|
|
702
977
|
/** Footer: inline actions and components. */
|
|
703
978
|
footer(items) { this._footer = items; return this; }
|
|
@@ -708,8 +983,9 @@ export class EmptyState extends Component {
|
|
|
708
983
|
const description = await evaluate(this._description, ctx);
|
|
709
984
|
const footer = await renderInlineItems(this, this._footer);
|
|
710
985
|
const iconColor = this._iconColor ?? 'primary';
|
|
986
|
+
const headingTag = this._headingTag ?? this.getHeadingTag();
|
|
711
987
|
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"><${
|
|
988
|
+
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
989
|
}
|
|
714
990
|
}
|
|
715
991
|
const CALLOUT_ICONS = { danger: 'o-x-circle', info: 'o-information-circle', success: 'o-check-circle', warning: 'o-exclamation-circle' };
|
|
@@ -730,6 +1006,7 @@ export class Callout extends Component {
|
|
|
730
1006
|
_footerActionsAlignment = 'start';
|
|
731
1007
|
_footer = null;
|
|
732
1008
|
_controlActions = [];
|
|
1009
|
+
_controls = null;
|
|
733
1010
|
constructor(heading = null) { super(); this._heading = heading; }
|
|
734
1011
|
static make(heading = null) { return configured(new Callout(heading)); }
|
|
735
1012
|
heading(heading) { this._heading = heading; return this; }
|
|
@@ -755,10 +1032,12 @@ export class Callout extends Component {
|
|
|
755
1032
|
footer(items) { this._footer = items; return this; }
|
|
756
1033
|
/** Small actions on the right of the callout (dismiss…). */
|
|
757
1034
|
controlActions(actions) { this._controlActions = [...this._controlActions, ...actions]; return this; }
|
|
1035
|
+
/** Free-form controls (components and actions) on the right of the callout, replacing the control actions. */
|
|
1036
|
+
controls(items) { this._controls = items; return this; }
|
|
758
1037
|
getColor() { return this._hasColor ? this._color : (this._color ?? this._status); }
|
|
759
1038
|
getIcon() { return this._icon ?? (this._status ? (CALLOUT_ICONS[this._status] ?? null) : null); }
|
|
760
1039
|
/** Callout actions, mounted on the root schema. */
|
|
761
|
-
getActions() { return [...(this._footer ? actionsOf(this._footer) : this._footerActions), ...this._controlActions]; }
|
|
1040
|
+
getActions() { return [...(this._footer ? actionsOf(this._footer) : this._footerActions), ...(this._controls ? actionsOf(this._controls) : this._controlActions)]; }
|
|
762
1041
|
async toHtml() {
|
|
763
1042
|
const ctx = this.context();
|
|
764
1043
|
const heading = await evaluate(this._heading, ctx);
|
|
@@ -767,7 +1046,7 @@ export class Callout extends Component {
|
|
|
767
1046
|
const iconName = this.getIcon();
|
|
768
1047
|
const iconColor = this._iconColor ?? this._status ?? color;
|
|
769
1048
|
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' });
|
|
1049
|
+
const controls = this._controls ? await renderInlineItems(this, this._controls, { actionView: 'auto' }) : await renderInlineItems(this, this._controlActions, { alignment: 'start', actionView: 'auto' });
|
|
771
1050
|
const statusKey = this._status ? `filament-schemas::components.callout.statuses.${this._status}` : null;
|
|
772
1051
|
const statusLabel = statusKey && t(statusKey) !== statusKey ? t(statusKey) : null;
|
|
773
1052
|
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.4",
|
|
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.4"
|
|
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>
|