@navette-js/forms 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 CHANGED
@@ -1,12 +1,14 @@
1
1
  import { HtmlString, type ColorManager, type Lazy } from '@navette-js/support';
2
- import { Component, Schema, type ComponentContext } from '@navette-js/schemas';
2
+ import { Component, Schema, type ComponentContext, type ComponentSlot } from '@navette-js/schemas';
3
3
  import { Action } from '@navette-js/actions';
4
+ import { TextColor, MentionProvider, type CustomBlockClass } from './rich-content.ts';
5
+ export { CustomBlockNode, GridColumnNode, GridNode, LeadNode, MentionNode, MentionProvider, MergeTagNode, registerRichContentExtensions, RichContentCustomBlock, RichContentRenderer, sanitizeCssColor, SizedImage, SmallMark, TextColor, TextColorMark, type CustomBlockClass } from './rich-content.ts';
6
+ import { RichEditorTool } from './rich-editor-tool.ts';
7
+ export { RichEditorTool, type RichEditorToolDefinition } from './rich-editor-tool.ts';
8
+ export { RICH_EDITOR_TOOL_DEFINITIONS } from './rich-editor-tools.ts';
4
9
  export declare const useColorManager: (manager: ColorManager) => void;
5
- type Rule = {
6
- name: string;
7
- passes: (value: unknown, ctx: ComponentContext) => boolean | Promise<boolean>;
8
- message: (attribute: string) => string;
9
- };
10
+ import { type Rule } from './rules.ts';
11
+ export type { Rule } from './rules.ts';
10
12
  export declare abstract class Field extends Component {
11
13
  protected _label: Lazy<string | null, ComponentContext>;
12
14
  protected _required: Lazy<boolean, ComponentContext>;
@@ -20,7 +22,18 @@ export declare abstract class Field extends Component {
20
22
  protected _dehydrated: boolean | null;
21
23
  protected _hiddenLabel: boolean;
22
24
  protected _helperText: string | null;
25
+ protected _liveMode: 'live' | 'blur' | null;
26
+ protected _validationAttribute: string | null;
27
+ protected _validationMessages: Record<string, string | ((attribute: string) => string)>;
28
+ protected _markedAsRequired: boolean | null;
23
29
  constructor(name: string);
30
+ /** Sends the state to the server as soon as it changes (`onBlur`: when the field loses focus). `false` turns it off. */
31
+ live(condition?: boolean | {
32
+ onBlur?: boolean;
33
+ }): this;
34
+ isLive(): boolean;
35
+ /** `wire:model` directive of the field, with its live modifiers. */
36
+ protected wireModel(): string;
24
37
  hasStatePath(): boolean;
25
38
  getName(): string;
26
39
  getId(): string;
@@ -31,6 +44,9 @@ export declare abstract class Field extends Component {
31
44
  default(value: unknown): this;
32
45
  /** Helper text shown under the field. */
33
46
  helperText(text: string | null): this;
47
+ protected _belowContent: ComponentSlot;
48
+ /** Text, components or actions under the control (after the helper text). Strings are escaped. */
49
+ belowContent(content: ComponentSlot): this;
34
50
  /** Helper text id, linked to the control through `aria-describedby`. */
35
51
  protected getHelperTextId(): string | null;
36
52
  /** Keeps the label for screen readers only. */
@@ -71,7 +87,100 @@ export declare abstract class Field extends Component {
71
87
  getDefaultState(): unknown;
72
88
  /** Shapes a record value as it enters the form (hydration). */
73
89
  hydrateState(value: unknown): unknown;
74
- rule(rule: Rule): this;
90
+ /** Adds a rule: an object `{ name, passes, message }`, or a Laravel-style string (`'max:255'`, `'regex:/^[a-z]+$/'`). */
91
+ rule(rule: Rule | string, condition?: boolean | ((ctx: ComponentContext) => boolean | Promise<boolean>)): this;
92
+ /** Several rules at once: `rules(['required', 'max:255'])` or `rules('required|max:255')`. */
93
+ rules(rules: (Rule | string)[] | string, condition?: boolean | ((ctx: ComponentContext) => boolean | Promise<boolean>)): this;
94
+ /** Value checked by the rules (a subclass may trim it, for instance). */
95
+ protected stateForValidation(state: unknown): unknown;
96
+ /** Sizes (`min`, `max`, `size`, `between`) compare the number, not the text length. */
97
+ protected hasNumericValidation(): boolean;
98
+ /** Name of the field in validation messages (the lowercased label by default). */
99
+ validationAttribute(name: string | null): this;
100
+ /** Custom messages by rule name (`{ required: 'Tell us your name.', max: 'Too long.' }`); `:attribute` is replaced by the field name. */
101
+ validationMessages(messages: Record<string, string | ((attribute: string) => string)>): this;
102
+ protected _loadStateFromRelationships: ((record: any, ctx: ComponentContext) => unknown) | null;
103
+ protected _saveRelationships: ((args: {
104
+ record: any;
105
+ state: unknown;
106
+ ctx: ComponentContext;
107
+ }) => void | Promise<void>) | null;
108
+ /** Fills the field from the record's relationships (after the record's own values): `(record, ctx) => state`. */
109
+ loadStateFromRelationshipsUsing(callback: ((record: any, ctx: ComponentContext) => unknown) | null): this;
110
+ /** Saves the field into the record's relationships, once the record is created or updated. Such a field is not in the saved data. */
111
+ saveRelationshipsUsing(callback: ((args: {
112
+ record: any;
113
+ state: unknown;
114
+ ctx: ComponentContext;
115
+ }) => void | Promise<void>) | null): this;
116
+ /** Whether the field saves itself through `saveRelationshipsUsing()` (and is then left out of the saved data). */
117
+ savesRelationships(): Promise<boolean>;
118
+ loadStateFromRelationships(record: any): Promise<{
119
+ state: unknown;
120
+ } | null>;
121
+ saveRelationships(record: any): Promise<void>;
122
+ /** Shows (or hides, with `false`) the required asterisk, regardless of `required()`. */
123
+ markAsRequired(condition?: boolean): this;
124
+ isMarkedAsRequired(): Promise<boolean>;
125
+ private stringRuleWith;
126
+ /** The value must match the pattern (a RegExp, or a `/pattern/flags` string). */
127
+ regex(pattern: RegExp | string): this;
128
+ notRegex(pattern: RegExp | string): this;
129
+ alpha(): this;
130
+ alphaDash(): this;
131
+ alphaNum(): this;
132
+ ascii(): this;
133
+ uuid(): this;
134
+ ulid(): this;
135
+ json(): this;
136
+ ip(): this;
137
+ ipv4(): this;
138
+ ipv6(): this;
139
+ macAddress(): this;
140
+ hexColor(): this;
141
+ multipleOf(value: number): this;
142
+ /** The value must be one of these (a list, or the values of an enum-like object). */
143
+ in(values: readonly (string | number)[] | Record<string, string | number>): this;
144
+ notIn(values: readonly (string | number)[]): this;
145
+ /** `in()` with the values of an enum-like object. */
146
+ enum(values: Record<string, string | number>): this;
147
+ startsWith(prefixes: string | string[]): this;
148
+ endsWith(suffixes: string | string[]): this;
149
+ doesntStartWith(prefixes: string | string[]): this;
150
+ doesntEndWith(suffixes: string | string[]): this;
151
+ /** The field must equal `<name>_confirmation` (or the given field) of the same form. */
152
+ confirmed(field?: string | null): this;
153
+ same(field: string): this;
154
+ different(field: string): this;
155
+ /** Compares with a number or another field of the same form. */
156
+ gt(fieldOrValue: string | number): this;
157
+ gte(fieldOrValue: string | number): this;
158
+ lt(fieldOrValue: string | number): this;
159
+ lte(fieldOrValue: string | number): this;
160
+ /** No duplicates in a list value (tags, multiple select, checkbox list). */
161
+ distinct(): this;
162
+ /** Required when another field of the form has one of these values (`requiredIf('type', 'company')`). */
163
+ requiredIf(field: string, values: unknown): this;
164
+ requiredUnless(field: string, values: unknown): this;
165
+ requiredIfAccepted(field: string): this;
166
+ requiredWith(fields: string | string[]): this;
167
+ requiredWithAll(fields: string | string[]): this;
168
+ requiredWithout(fields: string | string[]): this;
169
+ requiredWithoutAll(fields: string | string[]): this;
170
+ prohibited(condition?: boolean): this;
171
+ prohibitedIf(field: string, values: unknown): this;
172
+ prohibitedUnless(field: string, values: unknown): this;
173
+ prohibits(fields: string | string[]): this;
174
+ /**
175
+ * The value must exist: in a column of the host's data adapter (`exists()`, `exists({ column })`), in another adapter
176
+ * (`exists({ adapter: new ObjectionAdapter(Brand), column: 'id' })`), or according to a function.
177
+ */
178
+ exists(check?: {
179
+ adapter?: {
180
+ exists(column: string, value: unknown, exceptKey?: string | number | null): Promise<boolean>;
181
+ };
182
+ column?: string;
183
+ } | ((value: unknown, ctx: ComponentContext) => boolean | Promise<boolean>)): this;
75
184
  /** `unique` rule: the value must not exist in the column of the same name; `ignoreRecord` excludes the record being edited. */
76
185
  unique(options?: {
77
186
  ignoreRecord?: boolean;
@@ -111,6 +220,7 @@ export declare class TextInput extends Field {
111
220
  private _prefix;
112
221
  private _live;
113
222
  static make(name: string): TextInput;
223
+ protected hasNumericValidation(): boolean;
114
224
  /** Decimal number: `type="number"`, `step="any"`. */
115
225
  numeric(): this;
116
226
  /** Integer: `type="number"`, `step="1"`. */
@@ -120,11 +230,57 @@ export declare class TextInput extends Field {
120
230
  step(step: number | 'any' | null): this;
121
231
  minValue(value: number): this;
122
232
  maxValue(value: number): this;
123
- /** Label shown before the input (e.g. `$`). */
124
- prefix(label: string | null): this;
233
+ /** Label shown before the input (e.g. `$`); `isInline`: inside the input instead of a separate box. */
234
+ prefix(label: string | null, isInline?: boolean): this;
125
235
  private _suffix;
126
236
  /** Label shown after the input ("hours", "kg"…). */
127
- suffix(label: string | null): this;
237
+ suffix(label: string | null, isInline?: boolean): this;
238
+ private _prefixInline;
239
+ private _suffixInline;
240
+ private _prefixIcon;
241
+ private _suffixIcon;
242
+ private _prefixIconColor;
243
+ private _suffixIconColor;
244
+ private _prefixActions;
245
+ private _suffixActions;
246
+ prefixIcon(icon: string | null, isInline?: boolean): this;
247
+ suffixIcon(icon: string | null, isInline?: boolean): this;
248
+ prefixIconColor(color: string | null): this;
249
+ suffixIconColor(color: string | null): this;
250
+ /**
251
+ * Icon button before the input, running an action. The action gets the field's `state`, and `set(name, value)` to change a
252
+ * field of the same form.
253
+ */
254
+ prefixAction(action: Action, isInline?: boolean): this;
255
+ prefixActions(actions: Action[]): this;
256
+ suffixAction(action: Action, isInline?: boolean): this;
257
+ suffixActions(actions: Action[]): this;
258
+ /** Affix actions, mounted through the schema component. */
259
+ getActions(): Action[];
260
+ /** Exact length: `minlength` and `maxlength`, and the matching rule. */
261
+ length(length: number): this;
262
+ private _datalist;
263
+ /** Suggestions offered by the browser while typing (`<datalist>`). */
264
+ datalist(options: string[]): this;
265
+ private _readOnly;
266
+ /** The value is shown and sent, but cannot be changed (`readonly`). */
267
+ readOnly(condition?: boolean): this;
268
+ private _copyable;
269
+ /** A button after the input copies its value (the browser only allows it on HTTPS or `localhost`). */
270
+ copyable(condition?: boolean, copyMessage?: string | null, copyMessageDuration?: number): this;
271
+ private _trim;
272
+ /** Removes the spaces around the value before it is validated and saved. */
273
+ trim(condition?: boolean): this;
274
+ hydrateState(value: unknown): unknown;
275
+ /** `trim()`: the submitted value is trimmed before it is validated and saved. */
276
+ protected stateForValidation(state: unknown): unknown;
277
+ dehydrateState(state: unknown): unknown;
278
+ private _inputMode;
279
+ private _autocapitalize;
280
+ /** Virtual keyboard: `none`, `text`, `decimal`, `numeric`, `tel`, `search`, `email` or `url`. */
281
+ inputMode(mode: string | null): this;
282
+ /** `none`, `sentences`, `words` or `characters` (`true` / `false` for `on` / `off`). */
283
+ autocapitalize(mode?: string | boolean | null): this;
128
284
  /** Sends the state to the server on every keystroke, or on blur with `onBlur`. */
129
285
  live(options?: {
130
286
  onBlur?: boolean;
@@ -140,33 +296,25 @@ export declare class TextInput extends Field {
140
296
  mask(pattern: string | null): this;
141
297
  /** Phone number: digits, spaces and common punctuation. */
142
298
  tel(): this;
299
+ email(condition?: boolean): this;
143
300
  /** Web address: `url` input type, a valid http(s) value is required. */
144
301
  url(): this;
145
302
  maxLength(length: number): this;
146
303
  minLength(length: number): this;
147
304
  toHtml(): Promise<string>;
148
305
  }
149
- /** Native date picker (`<input type="date">`). */
150
- export declare class DatePicker extends Field {
151
- private _maxDate;
152
- private _minDate;
153
- private _live;
154
- static make(name: string): DatePicker;
155
- private _datePlaceholder;
156
- /** Placeholder attribute (browsers do not display it on a native `date` input). */
157
- placeholder(text: string | null): this;
158
- /** Sends the state to the server as soon as it changes (`wire:model.live`). */
159
- live(condition?: boolean): this;
160
- /** The state is a `YYYY-MM-DD` date, whatever the stored precision. */
161
- hydrateState(value: unknown): unknown;
162
- private static resolve;
163
- private bound;
164
- /** Latest accepted date: `'today'`, a `YYYY-MM-DD` date, or a context function (`({ get }) => get('endDate') || 'today'`). */
165
- maxDate(date: Lazy<string | null, ComponentContext>): this;
166
- minDate(date: Lazy<string | null, ComponentContext>): this;
167
- toHtml(): Promise<string>;
168
- }
169
306
  type Options = [string, string][] | Record<string, string>;
307
+ /** Options, possibly grouped: `{ 'In process': { draft: 'Draft', reviewing: 'Reviewing' }, Reviewed: { … } }`. */
308
+ type GroupedOptions = Options | Record<string, string | Record<string, string>> | [string, string | [string, string][]][];
309
+ type OptionGroup = {
310
+ label: string;
311
+ options: [string, string][];
312
+ };
313
+ /**
314
+ * Copy of a component and of the components it holds (layout children, item schemas), so that each repeated item gets its own
315
+ * instances: components keep a reference to their container.
316
+ */
317
+ export declare function cloneComponentTree<T extends Component>(component: T): T;
170
318
  /**
171
319
  * Dropdown. Native (`<select>`) by default; `searchable()` switches to the JavaScript select,
172
320
  * which queries the server through the exposed methods (`getSearchResultsForJs`…).
@@ -175,6 +323,15 @@ export declare class Select extends Field {
175
323
  private _options;
176
324
  private _placeholder;
177
325
  private _searchable;
326
+ private _native;
327
+ private _allowHtml;
328
+ private _maxItems;
329
+ private _minItems;
330
+ private _optionsLimit;
331
+ private _searchDebounce;
332
+ private _messages;
333
+ private _relationship;
334
+ private _affixes;
178
335
  private _dynamic;
179
336
  private _searchOnServer;
180
337
  private _live;
@@ -194,10 +351,55 @@ export declare class Select extends Field {
194
351
  * `dynamic: false`: options come from a function but stay fixed for the page's lifetime (announced as static to the browser).
195
352
  * `searchOnServer: false`: search runs in the browser, among the options already sent (for a complete list).
196
353
  */
197
- options(options: Options | ((ctx: ComponentContext) => Options | Promise<Options>), config?: {
354
+ options(options: GroupedOptions | ((ctx: ComponentContext) => GroupedOptions | Promise<GroupedOptions>), config?: {
198
355
  dynamic?: boolean;
199
356
  searchOnServer?: boolean;
200
357
  }): this;
358
+ /** "Yes" / "No" options, saved as `1` / `0`. */
359
+ boolean(trueLabel?: string | null, falseLabel?: string | null, placeholder?: string | null): this;
360
+ /** `false`: the JavaScript select, even without search. */
361
+ native(condition?: boolean): this;
362
+ /** Option labels are HTML (sanitized on the server; JavaScript select only). */
363
+ allowHtml(condition?: boolean): this;
364
+ /** Maximum (and minimum) number of selected items of a multiple select, checked on the server. */
365
+ maxItems(count: number | null): this;
366
+ minItems(count: number | null): this;
367
+ /** Number of options shown at once in the JavaScript select (50 by default). */
368
+ optionsLimit(limit: number): this;
369
+ searchDebounce(ms: number): this;
370
+ loadingMessage(message: string): this;
371
+ noSearchResultsMessage(message: string): this;
372
+ searchPrompt(message: string): this;
373
+ searchingMessage(message: string): this;
374
+ noOptionsMessage(message: string): this;
375
+ prefix(label: string | null, isInline?: boolean): this;
376
+ suffix(label: string | null, isInline?: boolean): this;
377
+ prefixIcon(name: string | null, isInline?: boolean): this;
378
+ suffixIcon(name: string | null, isInline?: boolean): this;
379
+ prefixIconColor(color: string | null): this;
380
+ suffixIconColor(color: string | null): this;
381
+ prefixAction(action: Action, isInline?: boolean): this;
382
+ suffixAction(action: Action, isInline?: boolean): this;
383
+ prefixActions(actions: Action[]): this;
384
+ suffixActions(actions: Action[]): this;
385
+ /**
386
+ * Options from a relationship of the form's model (Objection `relationMappings`): a belongs-to relationship (the field is the
387
+ * foreign key, `Select.make('author_id').relationship('author', 'name')`), or a many-to-many one with `multiple()` (the field is
388
+ * named after the relationship, and saves the links once the record is saved). `modifyQueryUsing(query, search)` scopes the
389
+ * options; picked keys outside that scope are rejected. With `searchable()`, the options are searched on the server.
390
+ */
391
+ relationship(name: string, titleAttribute?: string | null, modifyQueryUsing?: ((query: any, search?: string) => unknown) | null, options?: {
392
+ model?: any;
393
+ }): this;
394
+ private relationInfo;
395
+ private relationshipOptions;
396
+ private isManyRelationship;
397
+ savesRelationships(): Promise<boolean>;
398
+ loadStateFromRelationships(record: any): Promise<{
399
+ state: unknown;
400
+ } | null>;
401
+ /** Many-to-many: links the picked records and unlinks the others (only keys allowed by the relationship's query). */
402
+ saveRelationships(record: any): Promise<void>;
201
403
  /** Disables an option depending on the context (e.g. already picked in another row). */
202
404
  disableOptionWhen(condition: (value: string, ctx: ComponentContext) => boolean): this;
203
405
  /** Sends the state to the server as soon as it changes. */
@@ -216,19 +418,20 @@ export declare class Select extends Field {
216
418
  /** Sanitized selected values: unique strings. */
217
419
  private selectedValues;
218
420
  private labelsOf;
219
- /** No-op: options are always loaded with the form. */
220
- preload(): this;
421
+ private _preload;
422
+ /** Relationship with `searchable()`: loads the options with the form, instead of on search only. Other options are always loaded. */
423
+ preload(condition?: boolean): this;
221
424
  hydrateState(value: unknown): unknown;
222
425
  dehydrateState(state: unknown): unknown;
426
+ /** Options with their groups (labels sanitized when `allowHtml()`). */
427
+ resolveGroupedOptions(): Promise<(OptionGroup | [string, string])[]>;
223
428
  resolveOptions(): Promise<[string, string][]>;
429
+ private itemCountRules;
224
430
  getValidationRules(): Promise<Rule[]>;
225
431
  getExposedMethods(): string[];
226
432
  private static forJs;
227
- getOptionsForJs(): Promise<{
228
- label: string;
229
- value: string;
230
- isDisabled: boolean;
231
- }[]>;
433
+ private groupsForJs;
434
+ getOptionsForJs(): Promise<unknown[]>;
232
435
  private withDisabled;
233
436
  getSearchResultsForJs(search?: unknown): Promise<{
234
437
  label: string;
@@ -274,7 +477,10 @@ export declare class Select extends Field {
274
477
  private getEditOptionAction;
275
478
  /** Actions mounted on the field: hint, create option and edit option. */
276
479
  getActions(): Action[];
277
- /** "+" and "edit" buttons on the right of the field. */
480
+ private affixIcon;
481
+ private affixActionsHtml;
482
+ private prefixHtml;
483
+ /** Suffix: label, icon, actions, then the "+" and "edit" buttons. */
278
484
  private suffixHtml;
279
485
  toHtml(): Promise<string>;
280
486
  /** JavaScript select rendering. */
@@ -295,59 +501,221 @@ type ItemLink = {
295
501
  url: string;
296
502
  };
297
503
  /**
298
- * List of repeated items sharing the same fields, displayed as a table.
504
+ * List of repeated items sharing the same fields: stacked cards (default), a table (`table()`), or one field per item (`simple()`).
299
505
  * The state is a map `item key → fields`; key order is display order.
300
- * Add, delete and move are component actions, mounted with `schemaComponent`.
506
+ * Add, delete, clone and move are component actions, mounted with `schemaComponent`.
301
507
  */
302
508
  export declare class Repeater extends Field {
303
509
  private _schema;
304
510
  private _columns;
305
511
  private _defaultItems;
306
512
  private _maxItems;
513
+ private _minItems;
307
514
  private _itemLinks;
515
+ private _simple;
516
+ private _grid;
517
+ private _collapsible;
518
+ private _collapsed;
519
+ private _persistCollapsed;
520
+ private _cloneable;
521
+ private _addable;
522
+ private _deletable;
523
+ private _reorderableWithButtons;
524
+ private _itemLabel;
525
+ private _itemNumbers;
526
+ private _truncateItemLabel;
527
+ private _headingTag;
528
+ private _addActionLabel;
529
+ private _addActionAlignment;
530
+ private _labelBetweenItems;
531
+ private _itemHeaders;
532
+ private _relationship;
308
533
  static make(name: string): Repeater;
309
- /** Item fields: a function called for each item. */
310
- schema(factory: () => Field[]): this;
534
+ /** Item components: a function called for each item. */
535
+ schema(factory: (() => Component[]) | Component[]): this;
311
536
  /** Table display, one column per field. */
312
537
  table(columns: TableColumn[]): this;
538
+ isTable(): boolean;
539
+ /**
540
+ * One field per item, without a card: the state is saved as a list of values (`['a@example.com', 'b@example.com']`).
541
+ * The field is cloned for each item.
542
+ */
543
+ simple(field: Field | null): this;
313
544
  defaultItems(count: number): this;
314
545
  maxItems(count: number): this;
546
+ /** Minimum number of items, checked on the server. */
547
+ minItems(count: number | null): this;
315
548
  private _reorderable;
316
549
  /** `false`: items keep their order (e.g. a relationship without an order column). */
317
550
  reorderable(condition?: boolean): this;
551
+ private _dragAndDrop;
552
+ /** Move up / move down buttons in each item header, besides drag and drop. */
553
+ reorderableWithButtons(condition?: boolean): this;
554
+ /** `false`: no drag and drop (buttons may still reorder, with `reorderableWithButtons()`). */
555
+ reorderableWithDragAndDrop(condition?: boolean): this;
556
+ /** Reordering is off with `reorderable(false)`, and for a relationship without `orderColumn()` (the order could not be saved). */
557
+ private isReorderable;
558
+ private canDrag;
559
+ private canMove;
318
560
  /** Link shown on each row (an item action pointing to a URL). */
319
561
  extraItemLink(resolver: (ctx: {
320
562
  item: Record<string, unknown>;
321
563
  key: string;
322
564
  }) => ItemLink | null): this;
565
+ /** Items laid out in a grid (`2`, or `{ md: 2, xl: 3 }`). */
566
+ grid(columns?: Record<string, number> | number | null): this;
567
+ /** Items can be collapsed from their header; `collapsed()` collapses them on load. */
568
+ collapsible(condition?: boolean): this;
569
+ collapsed(condition?: boolean): this;
570
+ /** Keeps each item's collapsed state in the browser (local storage). */
571
+ persistCollapsed(condition?: boolean): this;
572
+ cloneable(condition?: boolean): this;
573
+ /** `false`: no "Add" button (and adding is refused on the server). */
574
+ addable(condition?: boolean): this;
575
+ /** `false`: no delete button (and deleting is refused on the server). */
576
+ deletable(condition?: boolean): this;
577
+ /** Item header label: a string, or a function of the item's state (`({ state }) => state.name ?? null`). Escaped. */
578
+ itemLabel(label: ((ctx: {
579
+ state: Record<string, unknown>;
580
+ key: string;
581
+ index: number;
582
+ }) => string | null) | string | null): this;
583
+ /** Item number after the label. */
584
+ itemNumbers(condition?: boolean): this;
585
+ truncateItemLabel(condition?: boolean): this;
586
+ /** `false`: items have no header (and so no buttons): only their fields. */
587
+ itemHeaders(condition?: boolean): this;
588
+ addActionLabel(label: string | null): this;
589
+ addActionAlignment(alignment: 'start' | 'center' | 'end' | null): this;
590
+ labelBetweenItems(label: string | null): this;
591
+ /**
592
+ * Items are the records of a has-many relationship of the form's model (Objection `relationMappings`): loaded when the form is
593
+ * filled, then created, updated and deleted once the record is saved. `orderColumn()` saves the items' order.
594
+ */
595
+ relationship(name?: string | null, modifyQueryUsing?: ((query: any) => unknown) | null, options?: {
596
+ model?: any;
597
+ }): this;
598
+ orderColumn(column?: string | null): this;
599
+ private _orderColumnPending;
600
+ private itemComponents;
601
+ private itemFields;
323
602
  /** Well-formed items only: keys come from the browser, so only simple keys are kept. */
324
603
  getItems(): Record<string, Record<string, unknown>>;
325
604
  private setItems;
326
605
  private itemDefaults;
327
606
  getDefaultState(): unknown;
328
- /** Relationship records → `record-<id>` items, limited to the declared fields. */
607
+ /** A list (records → `record-<id>` items; simple values → one-field items) or a keyed map, limited to the declared fields. */
329
608
  hydrateState(value: unknown): unknown;
330
609
  dehydrateState(): unknown;
610
+ /** Simple repeater: saved as a list of values. */
611
+ finalizeDehydratedState(state: unknown): unknown;
331
612
  getChildSchemas(): Schema[];
613
+ private _childColumns;
614
+ /** Grid columns inside each item. */
615
+ columns(columns: Record<string, number> | number | null): this;
332
616
  getValidationRules(): Promise<Rule[]>;
617
+ private relationInfo;
618
+ savesRelationships(): Promise<boolean>;
619
+ private relatedQuery;
620
+ loadStateFromRelationships(record: any): Promise<{
621
+ state: unknown;
622
+ } | null>;
623
+ /** Creates, updates and deletes the related records; only records of this owner (and query) are touched. */
624
+ saveRelationships(record: any): Promise<void>;
333
625
  getActions(): Action[];
626
+ private itemLabelOf;
334
627
  toHtml(): Promise<string>;
628
+ private toTableHtml;
335
629
  }
336
- /** Native time input: the state is `HH:MM` with `seconds(false)`, `HH:MM:SS` otherwise. */
337
- export declare class TimePicker extends Field {
338
- private _seconds;
339
- static make(name: string): TimePicker;
630
+ /** PHP date format (`M j, Y H:i`) to day.js (`MMM D, YYYY HH:mm`), as Filament's `convert_date_format()`. Escaped characters (`\at`) become literals. */
631
+ export declare function phpDateFormatToDayjs(format: string): string;
632
+ type DateLimit = Lazy<string | null, ComponentContext>;
633
+ /**
634
+ * Base of the date, time and date-time pickers: native browser input by default, or Filament's JavaScript picker with
635
+ * `native(false)` (display format, disabled dates, first day of the week, locale). Limits and disabled dates are checked on the server.
636
+ */
637
+ declare abstract class TemporalField extends Field {
638
+ protected abstract hasDate(): boolean;
639
+ protected abstract hasTime(): boolean;
640
+ protected _seconds: boolean;
641
+ protected _native: boolean;
642
+ protected _maxDate: DateLimit;
643
+ protected _minDate: DateLimit;
644
+ protected _displayFormat: string | null;
645
+ protected _format: string | null;
646
+ protected _firstDayOfWeek: number;
647
+ protected _disabledDates: Lazy<string[], ComponentContext>;
648
+ protected _closeOnDateSelection: boolean;
649
+ protected _locale: string | null;
650
+ protected _timezone: string | null;
651
+ protected _hoursStep: number;
652
+ protected _minutesStep: number;
653
+ protected _secondsStep: number;
654
+ protected _defaultFocusedDate: string | null;
655
+ protected _temporalPlaceholder: string | null;
340
656
  seconds(condition?: boolean): this;
657
+ hasSeconds(): boolean;
658
+ /** `false`: Filament's JavaScript picker instead of the browser's native input. */
659
+ native(condition?: boolean): this;
660
+ isNative(): boolean;
661
+ /** Display format of the JavaScript picker, in PHP's notation (`'d/m/Y'`, `'M j, Y H:i'`). */
662
+ displayFormat(format: string | null): this;
663
+ /** Format of the saved value, in PHP's notation (`'Y-m-d'` by default for dates, `'Y-m-d H:i:s'` with a time). */
664
+ format(format: string | null): this;
665
+ /** First day of the week in the JavaScript picker: 0 (Sunday) to 6. */
666
+ firstDayOfWeek(day: number): this;
667
+ weekStartsOnMonday(): this;
668
+ weekStartsOnSunday(): this;
669
+ /** Dates that cannot be picked (`YYYY-MM-DD`), also rejected on the server. */
670
+ disabledDates(dates: Lazy<string[], ComponentContext>): this;
671
+ closeOnDateSelection(condition?: boolean): this;
672
+ /** Locale of the JavaScript picker's month and day names (`'fr'`). */
673
+ locale(locale: string | null): this;
674
+ /** Shows and edits the value in this time zone, while it is saved in UTC (date-time pickers). */
675
+ timezone(timezone: string | null): this;
676
+ hoursStep(step: number): this;
677
+ minutesStep(step: number): this;
678
+ secondsStep(step: number): this;
679
+ /** Date focused when the JavaScript picker opens on an empty field. */
680
+ defaultFocusedDate(date: string | null): this;
681
+ placeholder(text: string | null): this;
682
+ private static resolveLimit;
683
+ protected limit(date: DateLimit): Promise<string | null>;
684
+ /** Latest accepted date: `'today'`, a date, or a context function (`({ get }) => get('ends_at')`). */
685
+ maxDate(date: DateLimit): this;
686
+ minDate(date: DateLimit): this;
687
+ /** Comparable string of a value: the date only for date pickers, the time only for time pickers. */
688
+ private comparable;
689
+ getValidationRules(): Promise<Rule[]>;
690
+ /** Record value to form state, shown in the field's time zone. */
341
691
  hydrateState(value: unknown): unknown;
692
+ /** Form state as the input expects it. */
693
+ protected stateOf(local: string): string;
694
+ /** Wall-clock value of the field's time zone to UTC. */
695
+ private toUtc;
696
+ /** Saved value: in UTC, in `format()` (or the default format). */
697
+ dehydrateState(state: unknown): unknown;
698
+ protected defaultDisplayFormat(): string;
699
+ protected nativeType(): string;
342
700
  toHtml(): Promise<string>;
343
701
  }
344
- /** Native `datetime-local` input: the saved state is `YYYY-MM-DD HH:MM:SS`. */
345
- export declare class DateTimePicker extends Field {
346
- private _seconds;
702
+ /** Date picker: native `<input type="date">` by default; the saved state is `YYYY-MM-DD`. */
703
+ export declare class DatePicker extends TemporalField {
704
+ static make(name: string): DatePicker;
705
+ protected hasDate(): boolean;
706
+ protected hasTime(): boolean;
707
+ }
708
+ /** Time picker: native `<input type="time">` by default; the state is `HH:MM:SS` (`HH:MM` with `seconds(false)`). */
709
+ export declare class TimePicker extends TemporalField {
710
+ static make(name: string): TimePicker;
711
+ protected hasDate(): boolean;
712
+ protected hasTime(): boolean;
713
+ }
714
+ /** Date-time picker: native `datetime-local` input by default; the saved state is `YYYY-MM-DD HH:MM:SS`. */
715
+ export declare class DateTimePicker extends TemporalField {
347
716
  static make(name: string): DateTimePicker;
348
- seconds(condition?: boolean): this;
349
- dehydrateState(state: unknown): unknown;
350
- toHtml(): Promise<string>;
717
+ protected hasDate(): boolean;
718
+ protected hasTime(): boolean;
351
719
  }
352
720
  /** Single choice as radio buttons. */
353
721
  export declare class Radio extends Field {
@@ -362,6 +730,12 @@ export declare class Radio extends Field {
362
730
  /** Options side by side. */
363
731
  inline(condition?: boolean): this;
364
732
  live(condition?: boolean): this;
733
+ private _disableOptionWhen;
734
+ /** Options that cannot be picked; they are refused on the server too. */
735
+ disableOptionWhen(condition: ((value: string, label: string) => boolean) | null): this;
736
+ isOptionDisabled(value: string, label: string): boolean;
737
+ /** "Yes" (`1`) and "No" (`0`) options. */
738
+ boolean(trueLabel?: string | null, falseLabel?: string | null): this;
365
739
  getValidationRules(): Promise<Rule[]>;
366
740
  toHtml(): Promise<string>;
367
741
  }
@@ -371,6 +745,27 @@ export declare class Textarea extends Field {
371
745
  static make(name: string): Textarea;
372
746
  rows(rows: number): this;
373
747
  maxLength(length: number): this;
748
+ private _minLength;
749
+ private _placeholder;
750
+ private _readOnly;
751
+ private _autosize;
752
+ private _grammarly;
753
+ private _trim;
754
+ minLength(length: number): this;
755
+ /** Exact length. */
756
+ length(length: number): this;
757
+ placeholder(text: string | null): this;
758
+ readOnly(condition?: boolean): this;
759
+ /** The textarea grows with its content. */
760
+ autosize(condition?: boolean): this;
761
+ /** Stops the Grammarly extension from analyzing the content. */
762
+ disableGrammarly(condition?: boolean): this;
763
+ /** Removes the spaces around the value before it is validated and saved. */
764
+ trim(condition?: boolean): this;
765
+ hydrateState(value: unknown): unknown;
766
+ /** `trim()`: the submitted value is trimmed before it is validated and saved. */
767
+ protected stateForValidation(state: unknown): unknown;
768
+ dehydrateState(state: unknown): unknown;
374
769
  toHtml(): Promise<string>;
375
770
  }
376
771
  /** Tag list input: the state is an array of strings. */
@@ -384,10 +779,28 @@ export declare class TagsInput extends Field {
384
779
  suggestions(suggestions: string[]): this;
385
780
  /** Keys that confirm the current tag, besides Enter (`['Tab', ',']`). */
386
781
  splitKeys(keys: string[]): this;
782
+ private _separator;
783
+ private _tagPrefix;
784
+ private _tagSuffix;
785
+ private _reorderable;
786
+ private _tagColor;
787
+ private _tagRules;
788
+ /** Saves the tags as one string joined with this separator (`'a,b,c'`) instead of a list. */
789
+ separator(separator?: string | null): this;
790
+ /** Text shown before / after each tag (not saved). */
791
+ tagPrefix(prefix: string | null): this;
792
+ tagSuffix(suffix: string | null): this;
793
+ /** Tags can be reordered by dragging. */
794
+ reorderable(condition?: boolean): this;
795
+ /** Badge color of the tags (`primary` by default). */
796
+ color(color: string | null): this;
797
+ /** Rules applied to each tag (e.g. a maximum length); the message names the tag. */
798
+ nestedRecursiveRules(rules: (Rule | string)[]): this;
387
799
  getDefaultState(): unknown;
388
800
  hydrateState(value: unknown): unknown;
389
- /** Only non-empty strings are saved, whatever shape the browser sends. */
801
+ /** Only non-empty strings are saved, whatever shape the browser sends (joined with the separator, if any). */
390
802
  dehydrateState(state: unknown): unknown;
803
+ getValidationRules(): Promise<Rule[]>;
391
804
  toHtml(): Promise<string>;
392
805
  }
393
806
  /** On/off switch. */
@@ -395,6 +808,19 @@ export declare class Toggle extends Field {
395
808
  static make(name: string): Toggle;
396
809
  getDefaultState(): unknown;
397
810
  hydrateState(value: unknown): unknown;
811
+ private _onIcon;
812
+ private _offIcon;
813
+ private _onColor;
814
+ private _offColor;
815
+ private _inline;
816
+ /** Icons inside the switch's knob. */
817
+ onIcon(icon: string | null): this;
818
+ offIcon(icon: string | null): this;
819
+ /** Colors when on (`primary` by default) and off (`gray`). */
820
+ onColor(color: string | null): this;
821
+ offColor(color: string | null): this;
822
+ /** `false`: the label goes above the switch instead of beside it. */
823
+ inline(condition?: boolean): this;
398
824
  toHtml(): Promise<string>;
399
825
  }
400
826
  /** Single choice shown as buttons. */
@@ -417,6 +843,34 @@ export declare class ToggleButtons extends Field {
417
843
  inline(condition?: boolean): this;
418
844
  /** Buttons joined into a single group. */
419
845
  grouped(condition?: boolean): this;
846
+ private _multiple;
847
+ private _tooltips;
848
+ private _size;
849
+ private _labelsHidden;
850
+ private _fullWidth;
851
+ private _columns;
852
+ private _disableOptionWhen;
853
+ /** Several buttons can be selected: the state is a list of values. */
854
+ multiple(condition?: boolean): this;
855
+ /** Tooltip per value. */
856
+ tooltips(tooltips: Record<string, string>): this;
857
+ /** Button size: `xs`, `sm`, `md` (default), `lg` or `xl`. */
858
+ size(size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | null): this;
859
+ /** Icons only: the labels stay for screen readers. */
860
+ hiddenButtonLabels(condition?: boolean): this;
861
+ /** Buttons stretch to the width of the field. */
862
+ fullWidth(condition?: boolean): this;
863
+ /** Number of columns the buttons are spread over (from `lg`), when not inline. */
864
+ columns(columns: number | null): this;
865
+ /** Options that cannot be selected; they are refused on the server too. */
866
+ disableOptionWhen(condition: ((value: string, label: string) => boolean) | null): this;
867
+ isOptionDisabled(value: string, label: string): boolean;
868
+ /** "Yes" (`1`) and "No" (`0`) buttons, green and red, with check and cross icons. */
869
+ boolean(trueLabel?: string | null, falseLabel?: string | null): this;
870
+ getDefaultState(): unknown;
871
+ hydrateState(value: unknown): unknown;
872
+ /** Multiple: only offered values, in option order. */
873
+ dehydrateState(state: unknown): unknown;
420
874
  getValidationRules(): Promise<Rule[]>;
421
875
  toHtml(): Promise<string>;
422
876
  }
@@ -430,38 +884,153 @@ export declare function richHtmlToDocument(html: string | null | undefined): Ric
430
884
  * TipTap document → HTML to save, or null if empty or invalid. The document comes from the browser,
431
885
  * so it is rendered then re-parsed to keep only what the schema accepts.
432
886
  */
887
+ /** HTML of a document already sanitized (`RichContentRenderer`): no re-parsing. */
888
+ export declare function renderRichDocument(document: unknown): string;
433
889
  export declare function richDocumentToHtml(document: unknown): string | null;
890
+ /** Tools of the rich editor's toolbar, in their default groups. */
891
+ export declare const RICH_EDITOR_TOOLS: string[][];
892
+ /** Custom blocks of an editor: a list, or groups of blocks by label. */
893
+ export type CustomBlocks = CustomBlockClass[] | Record<string, CustomBlockClass[]>;
434
894
  /**
435
- * Rich text editor: the browser works on a TipTap document, the database receives HTML.
436
- * Not supported yet: attachments, custom blocks, mentions.
895
+ * Rich text editor (TipTap): the browser works on a document, the database receives HTML (or JSON with `json()`).
896
+ * Besides the starter kit: text colors, merge tags, mentions, custom blocks, columns, resizable images, and your own
897
+ * tools and TipTap extensions.
437
898
  */
438
899
  export declare class RichEditor extends Field {
900
+ private _toolbar;
901
+ private _disabledTools;
902
+ private _json;
903
+ private _richPlaceholder;
904
+ private _attachmentsStorage;
905
+ private _attachmentsDirectory;
906
+ private _attachmentTypes;
907
+ private _attachmentMaxSize;
908
+ private _customTools;
909
+ private _textColors;
910
+ private _customTextColors;
911
+ private _mergeTags;
912
+ private _noMergeTagResults;
913
+ private _mentions;
914
+ private _customBlocks;
915
+ private _minimalCustomBlockControls;
916
+ private _customBlocksGrid;
917
+ private _searchableCustomBlocks;
918
+ private _activePanel;
919
+ private _stickyToolbar;
920
+ private _resizableImages;
921
+ private _jsExtensions;
922
+ private _editorActions;
439
923
  static make(name: string): RichEditor;
924
+ /** Toolbar tools, by group (`[['bold', 'italic', 'link'], ['h2', 'h3'], ['undo', 'redo']]`); see `RICH_EDITOR_TOOLS` and `RICH_EDITOR_TOOL_DEFINITIONS`. */
925
+ toolbarButtons(groups: string[][] | string[]): this;
926
+ private assertKnownTools;
927
+ /** Removes buttons; with no argument, removes them all. */
928
+ disableToolbarButtons(tools?: string[]): this;
929
+ disableAllToolbarButtons(condition?: boolean): this;
930
+ /** Your own toolbar buttons (or replacements of built-in ones, by name); place them with `toolbarButtons()`. */
931
+ tools(tools: RichEditorTool[]): this;
932
+ /** Actions mounted by your tools (`RichEditorTool.make('x').action()`), run with the editor selection in `arguments.editorSelection`. */
933
+ editorActions(actions: Action[]): this;
934
+ /** Browser TipTap extensions: URLs of ES modules exporting an extension (or an array of them). Register their server side with `registerRichContentExtensions()`. */
935
+ extensions(urls: string[]): this;
936
+ /** Saves the content as the editor's JSON document instead of HTML. */
937
+ json(condition?: boolean): this;
938
+ placeholder(text: string | null): this;
939
+ /** Where files attached in the editor are stored (`FileUpload.storage` by default); without storage, attaching is off. */
940
+ fileAttachmentsStorage(storage: FileStorage | null): this;
941
+ fileAttachmentsDirectory(directory: string | null): this;
942
+ /** Accepted types of attached files, checked on the file's content (images only: they are inserted as images). */
943
+ fileAttachmentsAcceptedFileTypes(types: string[]): this;
944
+ /** Maximum size of attached files, in kilobytes. */
945
+ fileAttachmentsMaxSize(kilobytes: number): this;
946
+ /** Text colors offered by the `textColor` tool (the Tailwind palette by default): `{ brand: TextColor.make('Brand', '#0ea5e9', '#38bdf8') }`. */
947
+ textColors(colors: Record<string, TextColor> | null): this;
948
+ getTextColors(): Record<string, TextColor>;
949
+ /** Lets users pick any color, besides the listed ones. */
950
+ customTextColors(condition?: boolean): this;
951
+ /** Merge tags users insert (`mergeTags` tool and `{{` in the text), replaced by values when the content is rendered: `['name', 'today']` or `{ name: 'Name' }`. */
952
+ mergeTags(tags: string[] | Record<string, string>): this;
953
+ noMergeTagSearchResultsMessage(message: string | null): this;
954
+ /** Mentions: `[MentionProvider.make('@').items({ 1: 'Ada' })]`. */
955
+ mentions(providers: MentionProvider[]): this;
956
+ /** Custom blocks (`customBlocks` tool), as a list or grouped by label. */
957
+ customBlocks(blocks: CustomBlocks): this;
958
+ minimalCustomBlockControls(condition?: boolean): this;
959
+ /** Shows the blocks of the panel in a grid. */
960
+ customBlocksGrid(condition?: boolean): this;
961
+ /** Adds a search field to the blocks panel. */
962
+ searchableCustomBlocks(condition?: boolean): this;
963
+ /** Panel open when the editor loads (`customBlocks`, `mergeTags`). */
964
+ activePanel(panel: string | null): this;
965
+ /** Keeps the toolbar visible while scrolling the content. */
966
+ stickyToolbar(condition?: boolean): this;
967
+ /** Lets users resize images with handles; the size is saved on the image. */
968
+ resizableImages(condition?: boolean): this;
969
+ private attachmentsStorage;
970
+ private findTool;
971
+ private toolGroups;
972
+ private blockGroups;
973
+ getCustomBlock(id: unknown): CustomBlockClass | null;
440
974
  hydrateState(value: unknown): unknown;
441
975
  getDefaultState(): unknown;
976
+ /** HTML (sanitized by the editor's schema), or the JSON document with `json()` (normalized through the same schema). */
442
977
  dehydrateState(state: unknown): unknown;
443
978
  getValidationRules(): Promise<Rule[]>;
444
979
  getExposedMethods(): string[];
445
- getMentionSearchResultsForJs(): unknown[];
446
- getMentionLabelsForJs(): unknown[];
447
- getCustomBlockPreviewsForJs(): unknown[];
448
- getUploadedFileAttachmentTemporaryUrl(): null;
449
- saveUploadedFileAttachmentAndGetUrl(): null;
450
- /** Toolbar "link" action: a modal (URL, new tab), then commands sent back to the browser editor. */
980
+ private mentionProvider;
981
+ /** Mention search, for the editor's JavaScript: `[{ id, label }]`. */
982
+ getMentionSearchResultsForJs(search?: unknown, triggerCharacter?: unknown): Promise<{
983
+ id: string;
984
+ label: string;
985
+ }[]>;
986
+ /** Current labels of mentioned ids, by trigger character. */
987
+ getMentionLabelsForJs(mentions?: unknown): Promise<Record<string, Record<string, string>>>;
988
+ /** Previews of inserted blocks, for the editor's JavaScript. */
989
+ getCustomBlockPreviewsForJs(customBlocks?: unknown): {
990
+ key: number;
991
+ label: string;
992
+ preview: string;
993
+ shouldApplyProseStylingToPreview: boolean;
994
+ }[];
995
+ /**
996
+ * URL of a file the editor just uploaded: the file is checked (type from its content, size), stored, and its public URL returned.
997
+ * It stays stored even if the form is not saved.
998
+ */
999
+ getUploadedFileAttachmentTemporaryUrl(args?: unknown): Promise<string | null>;
1000
+ saveUploadedFileAttachmentAndGetUrl(args?: unknown): Promise<string | null>;
1001
+ /** Sends commands to the editor in the browser (after an action), on the given selection. */
1002
+ runCommands(livewire: any, commands: {
1003
+ name: string;
1004
+ arguments?: unknown[];
1005
+ }[], editorSelection?: unknown): void;
1006
+ /** Toolbar actions: link, text color, custom block, and the ones of `editorActions()`. */
451
1007
  getActions(): Action[];
1008
+ private textColorAction;
1009
+ /** Modal of a custom block, built from the block class named in the arguments (`id`): insert or edit. */
1010
+ private customBlockAction;
1011
+ private customBlocksPanelHtml;
1012
+ private mergeTagsPanelHtml;
452
1013
  toHtml(): Promise<string>;
453
1014
  }
454
1015
  /** Color picker (hex format). */
455
1016
  export declare class ColorPicker extends Field {
456
1017
  static make(name: string): ColorPicker;
457
- /** Hex format: the only one supported, kept for API compatibility. */
1018
+ private _format;
1019
+ /** Color format of the state: `#ff0000` (default), `rgb(255, 0, 0)`, `rgba(255, 0, 0, 1)` or `hsl(0, 100%, 50%)`. */
1020
+ format(format: 'hex' | 'rgb' | 'rgba' | 'hsl'): this;
458
1021
  hex(): this;
459
- constructor(name: string);
1022
+ rgb(): this;
1023
+ rgba(): this;
1024
+ hsl(): this;
1025
+ getValidationRules(): Promise<Rule[]>;
460
1026
  toHtml(): Promise<string>;
461
1027
  }
462
1028
  export declare class Checkbox extends Field {
1029
+ private _inline;
463
1030
  static make(name: string): Checkbox;
464
1031
  getDefaultState(): unknown;
1032
+ /** `false`: the label goes above the checkbox instead of beside it. */
1033
+ inline(condition?: boolean): this;
465
1034
  toHtml(): Promise<string>;
466
1035
  }
467
1036
  /** Checkboxes for a list of values (no search or "select all" yet). */
@@ -472,6 +1041,23 @@ export declare class CheckboxList extends Field {
472
1041
  options(options: Options): this;
473
1042
  /** Number of columns the options are spread over (from `lg`). */
474
1043
  columns(columns: number | null): this;
1044
+ private _descriptions;
1045
+ private _searchable;
1046
+ private _bulkToggleable;
1047
+ private _disableOptionWhen;
1048
+ /** Helper text per value, under the option label. */
1049
+ descriptions(descriptions: Record<string, string>): this;
1050
+ /** A search field filters the options in the browser. */
1051
+ searchable(condition?: boolean): this;
1052
+ private _searchPrompt;
1053
+ private _noSearchResultsMessage;
1054
+ searchPrompt(prompt: string): this;
1055
+ noSearchResultsMessage(message: string): this;
1056
+ /** "Select all" / "Deselect all" links. */
1057
+ bulkToggleable(condition?: boolean): this;
1058
+ /** Options that cannot be checked; they are refused on the server too. */
1059
+ disableOptionWhen(condition: ((value: string, label: string) => boolean) | null): this;
1060
+ isOptionDisabled(value: string, label: string): boolean;
475
1061
  getDefaultState(): unknown;
476
1062
  hydrateState(value: unknown): unknown;
477
1063
  /** Only offered values are kept, without duplicates, in option order. */
@@ -488,8 +1074,29 @@ export declare class KeyValue extends Field {
488
1074
  keyLabel(label: string): this;
489
1075
  valueLabel(label: string): this;
490
1076
  reorderable(condition?: boolean): this;
1077
+ private _addable;
1078
+ private _deletable;
1079
+ private _editableKeys;
1080
+ private _editableValues;
1081
+ private _keyPlaceholder;
1082
+ private _valuePlaceholder;
1083
+ private _addLabel;
1084
+ private _deleteLabel;
1085
+ /** `false`: no "Add row" button. */
1086
+ addable(condition?: boolean): this;
1087
+ /** `false`: rows cannot be deleted. */
1088
+ deletable(condition?: boolean): this;
1089
+ /** `false`: keys cannot be changed. The server keeps the saved keys. */
1090
+ editableKeys(condition?: boolean): this;
1091
+ /** `false`: values cannot be changed. */
1092
+ editableValues(condition?: boolean): this;
1093
+ keyPlaceholder(text: string | null): this;
1094
+ valuePlaceholder(text: string | null): this;
1095
+ addActionLabel(label: string | null): this;
1096
+ deleteActionLabel(label: string | null): this;
491
1097
  getDefaultState(): unknown;
492
1098
  private static toPairs;
1099
+ private _originalValue;
493
1100
  hydrateState(value: unknown): unknown;
494
1101
  /** Flat object of strings: empty keys, nested objects and dangerous keys are dropped; 200 pairs at most. */
495
1102
  dehydrateState(state: unknown): unknown;
@@ -504,7 +1111,7 @@ export declare class RawJs {
504
1111
  }
505
1112
  /**
506
1113
  * Markdown editor (EasyMDE). The state is raw Markdown text, to sanitize before any HTML display.
507
- * Attachments are not supported (no `attachFiles` button by default); when read-only (`disabled()`),
1114
+ * Files can be attached (images, stored on upload) when a storage is set (`fileAttachmentsStorage()` or `FileUpload.storage`); when read-only (`disabled()`),
508
1115
  * the text is shown escaped, without Markdown conversion.
509
1116
  */
510
1117
  export declare class MarkdownEditor extends Field {
@@ -536,9 +1143,23 @@ export declare class MarkdownEditor extends Field {
536
1143
  minLength(length: number): this;
537
1144
  hydrateState(value: unknown): unknown;
538
1145
  getValidationRules(): Promise<Rule[]>;
1146
+ private _attachmentsStorage;
1147
+ private _attachmentsDirectory;
1148
+ private _attachmentTypes;
1149
+ private _attachmentMaxSize;
1150
+ /** Where attached files are stored (`FileUpload.storage` by default); without storage, the "Attach files" button is hidden. */
1151
+ fileAttachmentsStorage(storage: FileStorage | null): this;
1152
+ fileAttachmentsDirectory(directory: string | null): this;
1153
+ /** Accepted types of attached files (images), checked on the file's content. */
1154
+ fileAttachmentsAcceptedFileTypes(types: string[]): this;
1155
+ /** Maximum size of attached files, in kilobytes. */
1156
+ fileAttachmentsMaxSize(kilobytes: number): this;
1157
+ private canAttachFiles;
1158
+ private visibleToolbar;
539
1159
  getExposedMethods(): string[];
540
1160
  getUploadedFileAttachmentTemporaryUrl(): null;
541
- saveUploadedFileAttachmentAndGetUrl(): null;
1161
+ /** Stores the file just uploaded to `componentFileAttachments.<state path>` and returns its URL. */
1162
+ saveUploadedFileAttachmentAndGetUrl(): Promise<string | null>;
542
1163
  toHtml(): Promise<string>;
543
1164
  }
544
1165
  /** Code editor (CodeMirror): highlighting from `language()`, line wrapping with `wrap()`. */
@@ -681,6 +1302,10 @@ export declare class Placeholder extends Component {
681
1302
  export declare function fillSchema(schema: Schema, target: Record<string, unknown>): void;
682
1303
  /** Fills the form state from a record, field by field. */
683
1304
  export declare function fillSchemaFromRecord(schema: Schema, target: Record<string, unknown>, record: Record<string, unknown>): void;
1305
+ /** Fills the fields that load their state from the record's relationships (`loadStateFromRelationshipsUsing()`, `Select::relationship()`…). */
1306
+ export declare function loadSchemaRelationships(schema: Schema, target: Record<string, unknown>, record: unknown): Promise<void>;
1307
+ /** Saves the visible fields that write the record's relationships, once the record is created or updated. */
1308
+ export declare function saveSchemaRelationships(schema: Schema, record: unknown): Promise<void>;
684
1309
  /** Validates every visible field of a schema; returns errors by state path. */
685
1310
  export declare function validateSchema(schema: Schema): Promise<Record<string, string[]>>;
686
1311
  /** State to save: only declared, visible, dehydrated fields; an empty string becomes `null`. */
@@ -795,17 +1420,39 @@ export type FileUploadEntry = {
795
1420
  };
796
1421
  /** Where files go once the form is saved. */
797
1422
  export interface FileStorage {
798
- /** Stores a temporary file and returns its key (relative path). */
799
- store(file: TemporaryUpload, directory: string | null): Promise<string>;
1423
+ /** Stores a temporary file and returns its key (relative path); `name` is the file name to use instead of a random one. */
1424
+ store(file: TemporaryUpload, directory: string | null, name?: string | null): Promise<string>;
800
1425
  info(key: string): Promise<UploadedFileInfo | null>;
801
1426
  }
802
1427
  /** Local disk storage, served under a public URL. File names are random; the extension comes from the detected type. */
1428
+ /**
1429
+ * A file name chosen by the application or the user, made safe for storage: no directory part, only letters, digits, `_` and
1430
+ * `-`, and always the extension detected from the file's content (an uploaded `shell.php` declared as an image is stored
1431
+ * as `shell.png`).
1432
+ */
1433
+ export declare function safeStorageName(name: string, extension: string): string;
1434
+ /** Width and height of a PNG, GIF, JPEG or WebP image, read from its header; `null` for other files. */
1435
+ export declare function readImageSize(path: string): {
1436
+ width: number;
1437
+ height: number;
1438
+ } | null;
1439
+ export type ImageDimensions = {
1440
+ minWidth?: number;
1441
+ maxWidth?: number;
1442
+ minHeight?: number;
1443
+ maxHeight?: number;
1444
+ width?: number;
1445
+ height?: number;
1446
+ minRatio?: number;
1447
+ maxRatio?: number;
1448
+ ratio?: number;
1449
+ };
803
1450
  export declare class LocalFileStorage implements FileStorage {
804
1451
  private root;
805
1452
  private baseUrl;
806
1453
  constructor(root: string, baseUrl: string);
807
1454
  private resolve;
808
- store(file: TemporaryUpload, directory: string | null): Promise<string>;
1455
+ store(file: TemporaryUpload, directory: string | null, name?: string | null): Promise<string>;
809
1456
  info(key: string): Promise<UploadedFileInfo | null>;
810
1457
  }
811
1458
  /**
@@ -825,7 +1472,98 @@ export declare class FileUpload extends Field {
825
1472
  private _storeFiles;
826
1473
  private _storage;
827
1474
  private _getUploadedFileUsing;
1475
+ private _minSize;
1476
+ private _ui;
1477
+ private _avatar;
1478
+ private _imageEditor;
1479
+ private _circleCropper;
1480
+ private _editorAspectRatios;
1481
+ private _cropAspectRatio;
1482
+ private _imageAspectRatios;
1483
+ private _dimensions;
1484
+ private _deletable;
1485
+ private _preserveFilenames;
1486
+ private _fileNameUsing;
1487
+ private _fileNamesField;
1488
+ private _storedNames;
1489
+ private _alignment;
1490
+ private _placeholder;
828
1491
  static make(name: string): FileUpload;
1492
+ /** Minimum file size, in kilobytes. */
1493
+ minSize(kilobytes: number | null): this;
1494
+ /** Download button on each saved file. */
1495
+ downloadable(condition?: boolean): this;
1496
+ /** Button opening each saved file in a new tab. */
1497
+ openable(condition?: boolean): this;
1498
+ /** `false`: no preview of images. */
1499
+ previewable(condition?: boolean): this;
1500
+ /** `false`: files cannot be pasted from the clipboard. */
1501
+ pasteable(condition?: boolean): this;
1502
+ /** New files are added after the existing ones (instead of before). */
1503
+ appendFiles(condition?: boolean): this;
1504
+ /** `false`: saved files cannot be removed (the remove button is hidden, and removing is refused on the server). */
1505
+ deletable(condition?: boolean): this;
1506
+ /** `'grid'` (with `multiple()`), `'compact'`, `'integrated'` or `'circle'`. */
1507
+ panelLayout(layout: 'grid' | 'compact' | 'integrated' | 'circle' | string | null): this;
1508
+ panelAspectRatio(ratio: string | null): this;
1509
+ itemPanelAspectRatio(ratio: string | null): this;
1510
+ imagePreviewHeight(height: string | null): this;
1511
+ loadingIndicatorPosition(position: string): this;
1512
+ removeUploadedFileButtonPosition(position: string): this;
1513
+ uploadButtonPosition(position: string): this;
1514
+ uploadProgressIndicatorPosition(position: string): this;
1515
+ /** Message shown while a file uploads. */
1516
+ uploadingMessage(message: string): this;
1517
+ /** Number of files uploaded at the same time. */
1518
+ maxParallelUploads(count: number | null): this;
1519
+ /** Extension → MIME type map for files the browser cannot type (`{ zip: 'application/zip' }`). */
1520
+ mimeTypeMap(map: Record<string, string>): this;
1521
+ /** `false`: images keep their EXIF orientation. */
1522
+ orientImagesFromExif(condition?: boolean): this;
1523
+ alignment(alignment: 'start' | 'center' | 'end' | null): this;
1524
+ /** Text of the drop area. */
1525
+ placeholder(text: string | null): this;
1526
+ /** Round avatar: one image, cropped to a square and shown as a circle. */
1527
+ avatar(): this;
1528
+ /** Image editor (crop, rotate, flip, zoom) opened from each image, powered by Cropper.js in the browser. */
1529
+ imageEditor(condition?: boolean): this;
1530
+ /** Aspect ratios offered by the image editor (`['16:9', '4:3', '1:1']`; `null` for a free crop). */
1531
+ imageEditorAspectRatioOptions(ratios: (string | null)[]): this;
1532
+ imageEditorAspectRatios(ratios: (string | null)[]): this;
1533
+ /** Cropper.js view mode (1, 2 or 3). */
1534
+ imageEditorMode(mode: 1 | 2 | 3): this;
1535
+ imageEditorEmptyFillColor(color: string | null): this;
1536
+ imageEditorViewportWidth(width: string | number | null): this;
1537
+ imageEditorViewportHeight(height: string | number | null): this;
1538
+ private _autoOpenEditor;
1539
+ /** With `imageEditor()` and a single aspect ratio, opens the editor when an uploaded image has another ratio (on by default). */
1540
+ automaticallyOpenImageEditorForAspectRatio(condition?: boolean): this;
1541
+ private autoOpenEditorRatio;
1542
+ /** Circle crop in the image editor (avatars). */
1543
+ circleCropper(condition?: boolean): this;
1544
+ /** Crops images to this ratio in the browser before upload (`'16:9'`), without opening the editor. */
1545
+ automaticallyCropImagesToAspectRatio(ratio: string | null): this;
1546
+ imageCropAspectRatio(ratio: string | null): this;
1547
+ /** Resizes images in the browser before upload (`'cover'`, `'contain'` or `'force'`). */
1548
+ automaticallyResizeImagesMode(mode: 'cover' | 'contain' | 'force' | null): this;
1549
+ automaticallyResizeImagesToWidth(width: string | number | null): this;
1550
+ automaticallyResizeImagesToHeight(height: string | number | null): this;
1551
+ automaticallyUpscaleImagesWhenResizing(condition?: boolean): this;
1552
+ imageResizeMode(mode: 'cover' | 'contain' | 'force' | null): this;
1553
+ imageResizeTargetWidth(width: string | number | null): this;
1554
+ imageResizeTargetHeight(height: string | number | null): this;
1555
+ /** Accepted aspect ratio(s) of uploaded images (`'16:9'` or a list), checked on the server. */
1556
+ imageAspectRatio(ratio: string | string[] | null): this;
1557
+ /** Width, height and ratio bounds of uploaded images (`{ minWidth: 800, maxHeight: 1080 }`), checked on the server. */
1558
+ imageDimensions(dimensions: ImageDimensions | null): this;
1559
+ /** Stores files under their original (sanitized) names. See the security notes in the docs. */
1560
+ preserveFilenames(condition?: boolean): this;
1561
+ /** Name of the stored file, from the temporary file; sanitized, and its extension is the detected one. */
1562
+ getUploadedFileNameForStorageUsing(callback: ((file: TemporaryUpload, ctx: ComponentContext) => string | Promise<string>) | null): this;
1563
+ /** Saves the original file names (keyed by stored file) in another attribute of the record. */
1564
+ storeFileNamesIn(field: string | null): this;
1565
+ /** Extra saved values, next to the field (the original file names of `storeFileNamesIn()`). */
1566
+ getExtraDehydratedState(): Record<string, unknown>;
829
1567
  multiple(condition?: boolean): this;
830
1568
  maxFiles(count: number | null): this;
831
1569
  /** Maximum file size, in kilobytes. */
@@ -849,6 +1587,8 @@ export declare class FileUpload extends Field {
849
1587
  /** Stored keys the current record already owns: the only ones accepted back from the browser. */
850
1588
  private originalKeys;
851
1589
  private isTemporary;
1590
+ /** Original names saved by `storeFileNamesIn()`, by stored key. */
1591
+ private savedFileNames;
852
1592
  getExposedMethods(): string[];
853
1593
  getUploadedFiles(): Promise<Record<string, UploadedFileInfo | null>>;
854
1594
  private writeState;
@@ -861,6 +1601,7 @@ export declare class FileUpload extends Field {
861
1601
  validate(): Promise<string[]>;
862
1602
  /** On save: temporary files are stored, unrecognized existing keys are dropped. */
863
1603
  dehydrateState(): Promise<unknown>;
1604
+ /** Filament's image editor dialog (Cropper.js), rendered once per field. */
1605
+ private imageEditorHtml;
864
1606
  toHtml(): Promise<string>;
865
1607
  }
866
- export {};