declarative-forms 2.0.0-alpha.0
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/CHANGELOG.md +115 -0
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/index.cjs +2508 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1235 -0
- package/dist/index.d.ts +1235 -0
- package/dist/index.js +2473 -0
- package/dist/index.js.map +1 -0
- package/package.json +89 -0
- package/styles/declarative-forms.css +740 -0
- package/styles/theme.css +1293 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in HTML for label/message/option content.
|
|
3
|
+
*
|
|
4
|
+
* Everything the library renders is treated as plain text by default. When you
|
|
5
|
+
* genuinely need markup — a link inside a checkbox caption, a tag badge inside a
|
|
6
|
+
* select option — wrap it in {@link html} to say so explicitly.
|
|
7
|
+
*/
|
|
8
|
+
declare const SAFE_HTML: unique symbol;
|
|
9
|
+
/** A string the caller has explicitly marked as trusted HTML. */
|
|
10
|
+
declare class SafeHtml {
|
|
11
|
+
readonly value: string;
|
|
12
|
+
/** Brand used by {@link isSafeHtml} so instances survive duplicated module copies. */
|
|
13
|
+
readonly [SAFE_HTML] = true;
|
|
14
|
+
constructor(value: string);
|
|
15
|
+
toString(): string;
|
|
16
|
+
}
|
|
17
|
+
declare function isSafeHtml(value: unknown): value is SafeHtml;
|
|
18
|
+
/** Escape a string for interpolation into an HTML context. */
|
|
19
|
+
declare function escapeHtml(value: unknown): string;
|
|
20
|
+
/**
|
|
21
|
+
* Mark content as trusted HTML.
|
|
22
|
+
*
|
|
23
|
+
* As a tagged template, interpolated values are escaped and only the literal
|
|
24
|
+
* parts are trusted — this is the form you want for anything data-driven:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* html`<b>${userSuppliedName}</b>`
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Called with a plain string, the whole string is trusted. Use it only for
|
|
31
|
+
* markup you control:
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* html('<span class="dl-option-tag">Offline</span>')
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare function html(raw: string): SafeHtml;
|
|
38
|
+
declare function html(strings: TemplateStringsArray, ...values: unknown[]): SafeHtml;
|
|
39
|
+
/** Content that is rendered as text unless explicitly wrapped in {@link html}. */
|
|
40
|
+
type TextOrHtml = string | SafeHtml;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set an element's content from a {@link TextOrHtml}.
|
|
44
|
+
*
|
|
45
|
+
* Plain strings go through `textContent`; only values explicitly wrapped in
|
|
46
|
+
* `html()` reach `innerHTML`.
|
|
47
|
+
*/
|
|
48
|
+
declare function setContent(el: Element, content: TextOrHtml | null | undefined): void;
|
|
49
|
+
|
|
50
|
+
/** A snapshot of a form's values. Always carries the active tab under `activeTab`. */
|
|
51
|
+
type FormValues = Record<string, unknown>;
|
|
52
|
+
/**
|
|
53
|
+
* Everything a descriptor callback is given.
|
|
54
|
+
*
|
|
55
|
+
* v1 passed these as a different positional argument list per callback
|
|
56
|
+
* (`isActive(formData, stack, field, parentData)` vs `tab(field)` vs
|
|
57
|
+
* `defaultValue(formData)`). One context object makes them uniform, typed, and
|
|
58
|
+
* extensible without another breaking change.
|
|
59
|
+
*/
|
|
60
|
+
interface FieldContext {
|
|
61
|
+
/** Current values of this form. */
|
|
62
|
+
readonly data: FormValues;
|
|
63
|
+
/** The form the field belongs to. */
|
|
64
|
+
readonly form: DeclarativeForm;
|
|
65
|
+
/** The descriptor being evaluated. */
|
|
66
|
+
readonly field: FieldDescriptor;
|
|
67
|
+
/** Values of the enclosing form, when this form is nested (array entry or sub-dialog). */
|
|
68
|
+
readonly parentData: FormValues | undefined;
|
|
69
|
+
/** Values of every form currently on the modal stack, outermost first. */
|
|
70
|
+
readonly stackData: readonly FormValues[];
|
|
71
|
+
/** True while the field is rendered inside an "edit existing array entry" dialog. */
|
|
72
|
+
readonly isEditingArrayEntry: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** Context for `onFormChange`, which also knows what triggered the update. */
|
|
75
|
+
interface FormChangeContext extends FieldContext {
|
|
76
|
+
/** The field whose input triggered this update, if any. */
|
|
77
|
+
readonly trigger: FieldHandle | undefined;
|
|
78
|
+
}
|
|
79
|
+
/** Context handed to a `custom` field's `render`. */
|
|
80
|
+
interface RenderContext extends FieldContext {
|
|
81
|
+
/** The element to render into. Owned by the field; safe to replace its children. */
|
|
82
|
+
readonly element: HTMLElement;
|
|
83
|
+
/** Re-run the form update cycle, e.g. after mutating a value from custom UI. */
|
|
84
|
+
requestUpdate(force?: boolean): void;
|
|
85
|
+
/** Set this field's value and trigger an update. */
|
|
86
|
+
setValue(value: unknown): void;
|
|
87
|
+
}
|
|
88
|
+
/** Context for an options loader that rejected. */
|
|
89
|
+
interface OptionsErrorContext extends FieldContext {
|
|
90
|
+
readonly error: unknown;
|
|
91
|
+
}
|
|
92
|
+
/** A message to surface on a field's tooltip. */
|
|
93
|
+
interface FieldMessage {
|
|
94
|
+
level: 'info' | 'warning' | 'error';
|
|
95
|
+
text: string;
|
|
96
|
+
}
|
|
97
|
+
/** A value that may be provided directly, or computed from the form context. */
|
|
98
|
+
type Reactive<T> = T | ((ctx: FieldContext) => T);
|
|
99
|
+
/** A tab name, or several — a field may appear in more than one tab. */
|
|
100
|
+
type TabSpec = string | readonly string[];
|
|
101
|
+
interface TooltipDescriptor {
|
|
102
|
+
text: TextOrHtml;
|
|
103
|
+
/** Render the tooltip marker inside the input rather than next to the label. */
|
|
104
|
+
inInput?: boolean;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A select option.
|
|
108
|
+
*
|
|
109
|
+
* - `'JavaScript'` — value and label are the same
|
|
110
|
+
* - `{ value: 'js', label: 'JavaScript' }`
|
|
111
|
+
* - `{ value: 'js', label: 'JavaScript', displayWhenSelected: 'Lang: JS' }`
|
|
112
|
+
*/
|
|
113
|
+
type SelectOption = string | {
|
|
114
|
+
value: string;
|
|
115
|
+
label: TextOrHtml;
|
|
116
|
+
/** Shown in the closed input / multi-select tag instead of `label`. */
|
|
117
|
+
displayWhenSelected?: string;
|
|
118
|
+
};
|
|
119
|
+
/** Properties shared by every field kind. */
|
|
120
|
+
interface BaseFieldDescriptor {
|
|
121
|
+
/** Key this field's value appears under in `getValues()`. Must be unique per form. */
|
|
122
|
+
name: string;
|
|
123
|
+
/** Label text. Omit to render the field without a label (adds `.withoutLabel`). */
|
|
124
|
+
displayName?: TextOrHtml;
|
|
125
|
+
/** Extra class names for the field wrapper, space separated. */
|
|
126
|
+
className?: string;
|
|
127
|
+
/** Tab(s) this field belongs to. */
|
|
128
|
+
tab?: Reactive<TabSpec | undefined>;
|
|
129
|
+
/** Help marker rendered next to the label, or inside the input. */
|
|
130
|
+
tooltip?: TextOrHtml | TooltipDescriptor;
|
|
131
|
+
/** Return false to hide the field and drop it from `getValues()`. */
|
|
132
|
+
isActive?: (ctx: FieldContext) => boolean;
|
|
133
|
+
/** Called on every form update, after values settle. */
|
|
134
|
+
onFormChange?: (ctx: FormChangeContext) => void;
|
|
135
|
+
/** Names of fields whose change should re-run this field's async work. */
|
|
136
|
+
reloadOnChangeOf?: readonly string[];
|
|
137
|
+
/** Initial value, or a function of the form context. */
|
|
138
|
+
defaultValue?: Reactive<unknown>;
|
|
139
|
+
}
|
|
140
|
+
interface TextFieldDescriptor extends BaseFieldDescriptor {
|
|
141
|
+
kind?: 'text';
|
|
142
|
+
/** Maps to the `type` attribute — `'text'`, `'password'`, `'number'`, `'email'`, … */
|
|
143
|
+
type?: string;
|
|
144
|
+
placeholder?: Reactive<string | undefined>;
|
|
145
|
+
/** Maps to the `autocomplete` attribute. Defaults to off at the form level. */
|
|
146
|
+
autocomplete?: string;
|
|
147
|
+
}
|
|
148
|
+
interface TextAreaFieldDescriptor extends BaseFieldDescriptor {
|
|
149
|
+
kind: 'textarea';
|
|
150
|
+
placeholder?: Reactive<string | undefined>;
|
|
151
|
+
/** When false (the default), Enter confirms the dialog instead of inserting a newline. */
|
|
152
|
+
allowNewlines?: boolean;
|
|
153
|
+
}
|
|
154
|
+
interface SelectFieldDescriptor extends BaseFieldDescriptor {
|
|
155
|
+
kind: 'select';
|
|
156
|
+
/** Options, or a sync/async function of the form context. */
|
|
157
|
+
options: Reactive<readonly SelectOption[] | Promise<readonly SelectOption[]> | undefined>;
|
|
158
|
+
/** Allow selecting several options. The value becomes a `string[]`. */
|
|
159
|
+
multiple?: boolean;
|
|
160
|
+
placeholder?: Reactive<string | undefined>;
|
|
161
|
+
/** Turn a rejected `options` promise into a tooltip message. */
|
|
162
|
+
onOptionsError?: (ctx: OptionsErrorContext) => FieldMessage | undefined;
|
|
163
|
+
}
|
|
164
|
+
interface CheckboxFieldDescriptor extends BaseFieldDescriptor {
|
|
165
|
+
kind: 'checkbox';
|
|
166
|
+
/** Caption rendered next to the box. Wrap in `html()` for links. */
|
|
167
|
+
label: TextOrHtml;
|
|
168
|
+
}
|
|
169
|
+
interface MessageFieldDescriptor extends BaseFieldDescriptor {
|
|
170
|
+
kind: 'message';
|
|
171
|
+
/** Static text, or a function of the form context. Wrap in `html()` for markup. */
|
|
172
|
+
message: Reactive<TextOrHtml>;
|
|
173
|
+
}
|
|
174
|
+
interface FileFieldDescriptor extends BaseFieldDescriptor {
|
|
175
|
+
kind: 'file';
|
|
176
|
+
/** Maps to the `accept` attribute, e.g. `'image/png, image/jpeg'`. */
|
|
177
|
+
accept?: string;
|
|
178
|
+
}
|
|
179
|
+
interface ComputedFieldDescriptor extends BaseFieldDescriptor {
|
|
180
|
+
kind: 'computed';
|
|
181
|
+
/** Derives the value. May be async; runs before the form is confirmed. */
|
|
182
|
+
compute: (ctx: FieldContext) => unknown;
|
|
183
|
+
}
|
|
184
|
+
interface CardsFieldDescriptor extends BaseFieldDescriptor {
|
|
185
|
+
kind: 'cards';
|
|
186
|
+
cards: readonly {
|
|
187
|
+
value: string;
|
|
188
|
+
content: TextOrHtml;
|
|
189
|
+
}[];
|
|
190
|
+
}
|
|
191
|
+
interface CustomFieldDescriptor extends BaseFieldDescriptor {
|
|
192
|
+
kind: 'custom';
|
|
193
|
+
/** Render into `ctx.element`. Called on mount and on every form update. */
|
|
194
|
+
render: (ctx: RenderContext) => void;
|
|
195
|
+
}
|
|
196
|
+
interface ArrayFieldDescriptor extends BaseFieldDescriptor {
|
|
197
|
+
kind: 'array';
|
|
198
|
+
/** Field descriptors for one entry. */
|
|
199
|
+
of: readonly FieldDescriptor[];
|
|
200
|
+
/** Label for the add button. Defaults to `'Add'`. */
|
|
201
|
+
newButtonLabel?: string;
|
|
202
|
+
/** One-line summary of an entry. Defaults to joining its non-empty string values. */
|
|
203
|
+
renderEntry?: (entry: FormValues) => TextOrHtml;
|
|
204
|
+
/** Gate the entry dialog's confirm button. */
|
|
205
|
+
isValidRecord?: (entry: FormValues, ctx: FieldContext) => boolean | Promise<boolean>;
|
|
206
|
+
/** Adjust the entry fields when editing an existing entry rather than adding one. */
|
|
207
|
+
mapFieldsOnEdit?: (fields: readonly FieldDescriptor[], entry: FormValues) => readonly FieldDescriptor[];
|
|
208
|
+
/** Entries offered as opt-in checkboxes above the list. */
|
|
209
|
+
suggested?: Reactive<readonly FormValues[] | undefined>;
|
|
210
|
+
/** Called after an entry is added, edited or removed. */
|
|
211
|
+
onChange?: (values: FormValues) => void;
|
|
212
|
+
}
|
|
213
|
+
/** Any built-in field. */
|
|
214
|
+
type FieldDescriptor = TextFieldDescriptor | TextAreaFieldDescriptor | SelectFieldDescriptor | CheckboxFieldDescriptor | MessageFieldDescriptor | FileFieldDescriptor | ComputedFieldDescriptor | CardsFieldDescriptor | CustomFieldDescriptor | ArrayFieldDescriptor;
|
|
215
|
+
/** The `kind` discriminant of a built-in field. */
|
|
216
|
+
type FieldKind = NonNullable<FieldDescriptor['kind']>;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Pluggable tooltip backend.
|
|
220
|
+
*
|
|
221
|
+
* v1 hard-depended on tippy.js (and transitively @popperjs/core). The default
|
|
222
|
+
* implementation here is native and dependency-free; pass your own provider to
|
|
223
|
+
* `new DeclarativeForm({ tooltipProvider })` to restore tippy or use any other
|
|
224
|
+
* library.
|
|
225
|
+
*/
|
|
226
|
+
interface TooltipProvider {
|
|
227
|
+
/**
|
|
228
|
+
* Attach a tooltip to `element` showing `content` (an HTML string).
|
|
229
|
+
* Called again with new content when the tooltip text changes.
|
|
230
|
+
*/
|
|
231
|
+
attach(element: HTMLElement, content: string): void;
|
|
232
|
+
/** Detach the tooltip from `element` and release any resources. */
|
|
233
|
+
detach(element: HTMLElement): void;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Predicate context for button state, mirroring {@link FieldContext}. */
|
|
237
|
+
interface ButtonContext {
|
|
238
|
+
readonly data: FormValues;
|
|
239
|
+
readonly parentData: FormValues | undefined;
|
|
240
|
+
readonly stackData: readonly FormValues[];
|
|
241
|
+
}
|
|
242
|
+
interface ButtonDescriptor {
|
|
243
|
+
/** Invoked with the form values when the button is pressed. May be async. */
|
|
244
|
+
action?: (values: FormValues) => unknown;
|
|
245
|
+
/**
|
|
246
|
+
* DOM id for the button element.
|
|
247
|
+
*
|
|
248
|
+
* Required for `isActive` / `isVisible` to be wired up — a button without an
|
|
249
|
+
* id is rendered but never state-managed.
|
|
250
|
+
*/
|
|
251
|
+
id?: string;
|
|
252
|
+
/** Extra class names, space separated. `'secondary'` is styled by the default CSS. */
|
|
253
|
+
class?: string;
|
|
254
|
+
/** Enable/disable the button. Async predicates are supported. */
|
|
255
|
+
isActive?: (ctx: ButtonContext) => boolean | Promise<boolean>;
|
|
256
|
+
/** Show/hide the button. */
|
|
257
|
+
isVisible?: (ctx: ButtonContext) => boolean | Promise<boolean>;
|
|
258
|
+
/** Run `action` without closing the dialog — for wizard "Next"/"Back" buttons. */
|
|
259
|
+
doNotCloseModal?: boolean;
|
|
260
|
+
}
|
|
261
|
+
/** Buttons keyed by their visible label. */
|
|
262
|
+
type ButtonMap = Record<string, ButtonDescriptor>;
|
|
263
|
+
interface DeclarativeFormOptions {
|
|
264
|
+
fields: readonly FieldDescriptor[];
|
|
265
|
+
/**
|
|
266
|
+
* Buttons for the dialog's lower bar, keyed by label.
|
|
267
|
+
* Defaults to a single `OK` button running {@link onConfirm}.
|
|
268
|
+
*/
|
|
269
|
+
buttons?: ButtonMap;
|
|
270
|
+
/** Label for the default confirm button. Ignored when `buttons` is given. */
|
|
271
|
+
confirmLabel?: string;
|
|
272
|
+
/** Run when the dialog is confirmed. May be async. */
|
|
273
|
+
onConfirm?: (values: FormValues) => unknown;
|
|
274
|
+
/**
|
|
275
|
+
* Run when the dialog is dismissed via Esc or the close button.
|
|
276
|
+
* Omit to make the dialog non-dismissable — no close button is rendered.
|
|
277
|
+
*/
|
|
278
|
+
onCancel?: () => void;
|
|
279
|
+
/** Extra class names added to the inner `<form>` element. */
|
|
280
|
+
classNames?: readonly string[];
|
|
281
|
+
/** Persist a picked file and resolve to the URL to store as the field's value. */
|
|
282
|
+
persistFile?: (file: File) => Promise<URL | string>;
|
|
283
|
+
/** Swap the tooltip implementation. Defaults to a native, zero-dependency one. */
|
|
284
|
+
tooltipProvider?: TooltipProvider;
|
|
285
|
+
}
|
|
286
|
+
interface OpenInModalOptions {
|
|
287
|
+
/** Class names added to the inner `.modal` element. */
|
|
288
|
+
classNames?: readonly string[];
|
|
289
|
+
/** Class names added to the outer `.dl-modal` wrapper. */
|
|
290
|
+
wrapperClassNames?: readonly string[];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Per-form unique DOM ids.
|
|
295
|
+
*
|
|
296
|
+
* v1 gave checkboxes ids like `field-0`, which collided between any two forms on
|
|
297
|
+
* the page — and always collided between a parent form and its array sub-form.
|
|
298
|
+
* Scoping the counter per form makes `label[for]` reliable, which is what the
|
|
299
|
+
* accessibility work builds on.
|
|
300
|
+
*/
|
|
301
|
+
declare class IdGenerator {
|
|
302
|
+
private readonly prefix;
|
|
303
|
+
private counter;
|
|
304
|
+
constructor(prefix?: string);
|
|
305
|
+
/** A unique id, e.g. `dlf3-field-2`. */
|
|
306
|
+
next(kind?: string): string;
|
|
307
|
+
/** A stable id derived from a name, e.g. `dlf3-control-email`. */
|
|
308
|
+
for(kind: string, name: string): string;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Owns the tooltip markers of one form.
|
|
313
|
+
*
|
|
314
|
+
* v1 looked markers up with `document.querySelector('#dl-form-field-wrapper-for-…')`,
|
|
315
|
+
* so two forms with a same-named field fought over each other's tooltips. This
|
|
316
|
+
* keeps its own registry, keyed by field name.
|
|
317
|
+
*/
|
|
318
|
+
declare class TooltipController {
|
|
319
|
+
private readonly markers;
|
|
320
|
+
private readonly provider;
|
|
321
|
+
constructor(provider?: TooltipProvider);
|
|
322
|
+
/**
|
|
323
|
+
* Create the `<span class="dl-tooltip">?</span>` marker for a field.
|
|
324
|
+
*
|
|
325
|
+
* Registered by field name so `setTooltip(name, …)` can find it later. The
|
|
326
|
+
* `data-tippy-content` attribute is retained from v1 as the content store.
|
|
327
|
+
*/
|
|
328
|
+
createMarker(content: TextOrHtml): HTMLElement;
|
|
329
|
+
/** Associate a marker with a field name. Called by the form after mounting. */
|
|
330
|
+
register(fieldName: string, marker: HTMLElement | undefined): void;
|
|
331
|
+
/**
|
|
332
|
+
* Replace a field's tooltip text, icon and state class.
|
|
333
|
+
*
|
|
334
|
+
* A field without a tooltip is a silent no-op, matching v1.
|
|
335
|
+
*/
|
|
336
|
+
set(fieldName: string, text: string, icon?: string, className?: string): void;
|
|
337
|
+
setSuccess(fieldName: string, text: string): void;
|
|
338
|
+
setWarning(fieldName: string, text: string): void;
|
|
339
|
+
setError(fieldName: string, text: string): void;
|
|
340
|
+
setLoading(fieldName: string, text: string): void;
|
|
341
|
+
/** Apply a `{ level, text }` message from an options loader. */
|
|
342
|
+
setMessage(fieldName: string, level: 'info' | 'warning' | 'error', text: string): void;
|
|
343
|
+
/** Restore a field's original tooltip text and `?` icon. */
|
|
344
|
+
reset(fieldName: string): void;
|
|
345
|
+
resetAll(fieldNames: Iterable<string>): void;
|
|
346
|
+
destroy(): void;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Tracks the form's in-flight async work.
|
|
351
|
+
*
|
|
352
|
+
* v1 pushed every promise onto a `this.allPromises` array that was never
|
|
353
|
+
* cleared, so each `Promise.allSettled(allPromises)` re-awaited the entire
|
|
354
|
+
* history of the form and the array grew without bound. This tracks only what
|
|
355
|
+
* is actually pending, and hands out generation tokens so a slow async result
|
|
356
|
+
* from an earlier keystroke cannot overwrite a newer one.
|
|
357
|
+
*/
|
|
358
|
+
declare class UpdateScheduler {
|
|
359
|
+
private pending;
|
|
360
|
+
private generations;
|
|
361
|
+
/** Register async work and drop it from the set once it settles. */
|
|
362
|
+
track<T>(work: Promise<T>): Promise<T>;
|
|
363
|
+
/** Number of currently in-flight operations. Test/diagnostics only. */
|
|
364
|
+
get pendingCount(): number;
|
|
365
|
+
/**
|
|
366
|
+
* Resolve once nothing is in flight.
|
|
367
|
+
*
|
|
368
|
+
* Loops because settling one batch may schedule another (an options load
|
|
369
|
+
* whose completion triggers a dependent field's reload).
|
|
370
|
+
*/
|
|
371
|
+
whenSettled(): Promise<void>;
|
|
372
|
+
/**
|
|
373
|
+
* Claim the next generation for `key` and return a predicate that reports
|
|
374
|
+
* whether that claim is still the newest.
|
|
375
|
+
*
|
|
376
|
+
* ```ts
|
|
377
|
+
* const isCurrent = scheduler.claim(`options:${field.name}`);
|
|
378
|
+
* const values = await load();
|
|
379
|
+
* if (!isCurrent()) return; // a newer load superseded this one
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
382
|
+
claim(key: string): () => boolean;
|
|
383
|
+
/** Invalidate every outstanding claim and forget all pending work. */
|
|
384
|
+
reset(): void;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The slice of the owning form a field is allowed to see.
|
|
389
|
+
*
|
|
390
|
+
* Fields depend on this narrow interface rather than on `DeclarativeForm`
|
|
391
|
+
* itself, which keeps the dependency one-directional and makes fields testable
|
|
392
|
+
* against a stub.
|
|
393
|
+
*/
|
|
394
|
+
interface FormContext {
|
|
395
|
+
readonly ids: IdGenerator;
|
|
396
|
+
readonly tooltips: TooltipController;
|
|
397
|
+
readonly scheduler: UpdateScheduler;
|
|
398
|
+
/** Values of the enclosing form, when this form is nested. */
|
|
399
|
+
readonly parentData: FormValues | undefined;
|
|
400
|
+
/** Values of every form on the modal stack, outermost first. */
|
|
401
|
+
readonly stackData: readonly FormValues[];
|
|
402
|
+
/** True while this form is an "edit existing array entry" dialog. */
|
|
403
|
+
readonly isEditingArrayEntry: boolean;
|
|
404
|
+
/** Build the context object passed to a descriptor callback. */
|
|
405
|
+
contextFor(descriptor: FieldDescriptor): FieldContext;
|
|
406
|
+
/** Re-run the update cycle because `source` changed. */
|
|
407
|
+
requestUpdate(source: FieldHandle | undefined, force?: boolean): void;
|
|
408
|
+
/** Persist a picked file, returning the URL to store as the value. */
|
|
409
|
+
persistFile(file: File): Promise<string>;
|
|
410
|
+
/**
|
|
411
|
+
* Build a nested form that knows this form as its parent.
|
|
412
|
+
*
|
|
413
|
+
* Lets `ArrayField` open entry dialogs without importing `DeclarativeForm`,
|
|
414
|
+
* keeping the field layer free of a cycle back into the core.
|
|
415
|
+
*/
|
|
416
|
+
createSubForm(options: SubFormOptions): SubForm;
|
|
417
|
+
}
|
|
418
|
+
interface SubFormOptions {
|
|
419
|
+
fields: readonly FieldDescriptor[];
|
|
420
|
+
classNames?: readonly string[];
|
|
421
|
+
buttons: ButtonMap;
|
|
422
|
+
/** Mark the dialog as editing an existing array entry. */
|
|
423
|
+
isEditingArrayEntry?: boolean;
|
|
424
|
+
}
|
|
425
|
+
/** The slice of a nested form its opener needs. */
|
|
426
|
+
interface SubForm {
|
|
427
|
+
openInModal(): HTMLElement;
|
|
428
|
+
getValues(): FormValues;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The imperative handle consumers get from `form.field(name)`.
|
|
433
|
+
*
|
|
434
|
+
* Replaces v1's `descriptor.domElement`, which the library wrote back onto the
|
|
435
|
+
* caller's own object.
|
|
436
|
+
*/
|
|
437
|
+
interface FieldHandle {
|
|
438
|
+
readonly name: string;
|
|
439
|
+
/** The control element — an `<input>`, `<textarea>`, `<dl-select>`, … */
|
|
440
|
+
readonly element: HTMLElement;
|
|
441
|
+
/** The `.dl-form-field-wrapper` containing label, tooltip and control. */
|
|
442
|
+
readonly wrapper: HTMLElement;
|
|
443
|
+
getValue(): unknown;
|
|
444
|
+
setValue(value: unknown): void;
|
|
445
|
+
focus(): void;
|
|
446
|
+
/** Show a loading state, where the field kind supports one. */
|
|
447
|
+
setLoading(loading: boolean): void;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Base class for every field kind.
|
|
451
|
+
*
|
|
452
|
+
* Subclasses implement {@link createControl} plus the value accessors; the base
|
|
453
|
+
* class owns the wrapper, label, tooltip marker and active state — i.e. all the
|
|
454
|
+
* parts of the frozen DOM contract that are the same for every kind.
|
|
455
|
+
*/
|
|
456
|
+
declare abstract class Field<D extends BaseFieldDescriptor = FieldDescriptor> implements FieldHandle {
|
|
457
|
+
readonly descriptor: D;
|
|
458
|
+
protected readonly context: FormContext;
|
|
459
|
+
readonly wrapper: HTMLElement;
|
|
460
|
+
protected label: HTMLLabelElement | undefined;
|
|
461
|
+
/** The `.dl-tooltip` marker, when this field declares a tooltip. */
|
|
462
|
+
tooltipMarker: HTMLElement | undefined;
|
|
463
|
+
private control;
|
|
464
|
+
private active;
|
|
465
|
+
constructor(descriptor: D, context: FormContext);
|
|
466
|
+
/**
|
|
467
|
+
* Build the control and the surrounding label/tooltip.
|
|
468
|
+
*
|
|
469
|
+
* Deliberately separate from the constructor: subclass field initializers run
|
|
470
|
+
* *after* `super()` under ES2022 class-field semantics, so anything
|
|
471
|
+
* `createControl` assigned would be reset to `undefined` if it ran from the
|
|
472
|
+
* base constructor. {@link FieldRegistry} calls this immediately after
|
|
473
|
+
* construction.
|
|
474
|
+
*/
|
|
475
|
+
build(): this;
|
|
476
|
+
/** The control element — an `<input>`, `<textarea>`, `<dl-select>`, … */
|
|
477
|
+
get element(): HTMLElement;
|
|
478
|
+
get name(): string;
|
|
479
|
+
/** Extra class the kind contributes to its wrapper, e.g. `dl-select-wrapper`. */
|
|
480
|
+
protected get wrapperExtraClass(): string | undefined;
|
|
481
|
+
/**
|
|
482
|
+
* Whether this kind holds a value at all.
|
|
483
|
+
*
|
|
484
|
+
* `message` fields are presentational and stay out of `getValues()`.
|
|
485
|
+
*/
|
|
486
|
+
get contributesValue(): boolean;
|
|
487
|
+
/** Build the control element. Called once, from {@link build}. */
|
|
488
|
+
protected abstract createControl(): HTMLElement;
|
|
489
|
+
abstract getValue(): unknown;
|
|
490
|
+
abstract setValue(value: unknown): void;
|
|
491
|
+
focus(): void;
|
|
492
|
+
setLoading(_loading: boolean): void;
|
|
493
|
+
/** Ask the owning form to recompute values and re-run the update cycle. */
|
|
494
|
+
protected requestUpdate(force?: boolean): void;
|
|
495
|
+
/** Resolve a value that may have been given as a function of the form context. */
|
|
496
|
+
protected resolve<T>(value: Reactive<T> | undefined): T | undefined;
|
|
497
|
+
isActive(): boolean;
|
|
498
|
+
setActive(active: boolean): void;
|
|
499
|
+
/** Show or hide the field because of the currently selected tab. */
|
|
500
|
+
setInTab(inTab: boolean): void;
|
|
501
|
+
/** Tabs this field currently belongs to. */
|
|
502
|
+
tabs(): string[];
|
|
503
|
+
/**
|
|
504
|
+
* Apply the default value. Awaited during form initialisation.
|
|
505
|
+
*
|
|
506
|
+
* A `defaultValue` function may return a promise, so the result is always
|
|
507
|
+
* resolved before being applied.
|
|
508
|
+
*/
|
|
509
|
+
applyDefaultValue(): Promise<void>;
|
|
510
|
+
/**
|
|
511
|
+
* Hook for setup that cannot happen in {@link build} — loading select
|
|
512
|
+
* options, or a first render that needs the form context. May be synchronous.
|
|
513
|
+
*/
|
|
514
|
+
initialize(): void | Promise<void>;
|
|
515
|
+
/**
|
|
516
|
+
* Called on every form update, after values have settled.
|
|
517
|
+
*
|
|
518
|
+
* @param triggerName name of the field whose input started this update
|
|
519
|
+
*/
|
|
520
|
+
onFormUpdate(_ctx: FormChangeContext, _triggerName: string | undefined): void;
|
|
521
|
+
destroy(): void;
|
|
522
|
+
private applyTabClasses;
|
|
523
|
+
/**
|
|
524
|
+
* Tab classes are stamped on at construction time, before the form has any
|
|
525
|
+
* values, so only a statically declared `tab` can contribute one.
|
|
526
|
+
*/
|
|
527
|
+
private tabsFromStaticDescriptor;
|
|
528
|
+
private createLabel;
|
|
529
|
+
/**
|
|
530
|
+
* The id `label[for]` should target.
|
|
531
|
+
*
|
|
532
|
+
* Defaults to giving the control element an id; kinds whose control is a
|
|
533
|
+
* wrapper (checkbox, cards, file) override this to point at the real input.
|
|
534
|
+
*/
|
|
535
|
+
protected controlIdForLabel(): string;
|
|
536
|
+
private createTooltipMarker;
|
|
537
|
+
/** Convenience for subclasses that render simple text content. */
|
|
538
|
+
protected setContent: typeof setContent;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Constructs a field from its descriptor. */
|
|
542
|
+
type FieldConstructor = new (descriptor: never, context: FormContext) => Field<never>;
|
|
543
|
+
/**
|
|
544
|
+
* Maps a descriptor's `kind` to the class that implements it.
|
|
545
|
+
*
|
|
546
|
+
* This replaces v1's ten-branch `if/else`, and makes the set of field kinds
|
|
547
|
+
* extensible: register your own class and it becomes usable in any descriptor.
|
|
548
|
+
*/
|
|
549
|
+
declare class FieldRegistry {
|
|
550
|
+
private readonly kinds;
|
|
551
|
+
constructor(entries?: Iterable<[string, FieldConstructor]>);
|
|
552
|
+
register(kind: string, ctor: FieldConstructor): this;
|
|
553
|
+
has(kind: string): boolean;
|
|
554
|
+
/** A copy, so callers can add kinds without mutating the shared default. */
|
|
555
|
+
clone(): FieldRegistry;
|
|
556
|
+
create(descriptor: FieldDescriptor, context: FormContext): Field;
|
|
557
|
+
}
|
|
558
|
+
/** The built-in field kinds. */
|
|
559
|
+
declare const defaultFieldRegistry: FieldRegistry;
|
|
560
|
+
|
|
561
|
+
type InputSubscriber = (values: FormValues) => void;
|
|
562
|
+
/**
|
|
563
|
+
* Change detection and input subscribers for one form.
|
|
564
|
+
*
|
|
565
|
+
* Tracks two baselines — one including the active tab and one excluding it — so
|
|
566
|
+
* that switching tabs can be treated as a change or not depending on the caller.
|
|
567
|
+
*/
|
|
568
|
+
declare class FormModel {
|
|
569
|
+
private last;
|
|
570
|
+
private lastWithoutTab;
|
|
571
|
+
private readonly subscribers;
|
|
572
|
+
/**
|
|
573
|
+
* Report whether `values` differ from the last seen snapshot, and adopt it as
|
|
574
|
+
* the new baseline.
|
|
575
|
+
*
|
|
576
|
+
* @param includeTab compare `activeTab` as well as the field values
|
|
577
|
+
*/
|
|
578
|
+
hasChanged(values: FormValues, includeTab?: boolean): boolean;
|
|
579
|
+
subscribe(subscriber: InputSubscriber): () => void;
|
|
580
|
+
notify(values: FormValues): void;
|
|
581
|
+
/** Forget the baseline so the next comparison reports a change. */
|
|
582
|
+
reset(): void;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** What the stack needs from a form on it. */
|
|
586
|
+
interface StackableDialog {
|
|
587
|
+
getValues(): FormValues;
|
|
588
|
+
hide(): void;
|
|
589
|
+
show(): void;
|
|
590
|
+
/** Re-apply the active tab after being revealed again. */
|
|
591
|
+
refreshTabs(): void;
|
|
592
|
+
/** Handle Esc. No-op when the dialog is not dismissable. */
|
|
593
|
+
requestCancel(): void;
|
|
594
|
+
/** Handle Enter. Only called when the dialog has exactly one button. */
|
|
595
|
+
requestConfirm(event: KeyboardEvent): void;
|
|
596
|
+
/** Number of buttons in the lower bar. */
|
|
597
|
+
readonly buttonCount: number;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* The stack of open modal dialogs.
|
|
601
|
+
*
|
|
602
|
+
* v1 kept this in a module-level `var` and registered its `keydown` handler as
|
|
603
|
+
* an import side effect, which ran even in Node and could not be torn down.
|
|
604
|
+
* Here the listener is attached when the first dialog opens and removed when
|
|
605
|
+
* the last one closes, so importing the library does nothing observable.
|
|
606
|
+
*/
|
|
607
|
+
declare class ModalStack {
|
|
608
|
+
private readonly dialogs;
|
|
609
|
+
private listening;
|
|
610
|
+
get size(): number;
|
|
611
|
+
/** Values of every dialog on the stack, outermost first. */
|
|
612
|
+
values(): FormValues[];
|
|
613
|
+
/** The dialog currently on top, if any. */
|
|
614
|
+
get top(): StackableDialog | undefined;
|
|
615
|
+
push(dialog: StackableDialog): void;
|
|
616
|
+
/** Remove a dialog and reveal whatever it was covering. */
|
|
617
|
+
remove(dialog: StackableDialog): void;
|
|
618
|
+
contains(dialog: StackableDialog): boolean;
|
|
619
|
+
private readonly onKeyDown;
|
|
620
|
+
private startListening;
|
|
621
|
+
private stopListening;
|
|
622
|
+
}
|
|
623
|
+
/** The stack shared by all forms that do not opt into their own. */
|
|
624
|
+
declare const globalModalStack: ModalStack;
|
|
625
|
+
|
|
626
|
+
/** Options only used when a form is created as a child of another. */
|
|
627
|
+
interface InternalOptions {
|
|
628
|
+
parent?: DeclarativeForm;
|
|
629
|
+
stack?: ModalStack;
|
|
630
|
+
registry?: FieldRegistry;
|
|
631
|
+
isEditingArrayEntry?: boolean;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* A declarative form dialog.
|
|
635
|
+
*
|
|
636
|
+
* ```ts
|
|
637
|
+
* const form = new DeclarativeForm({
|
|
638
|
+
* fields: [
|
|
639
|
+
* { name: 'title', displayName: 'Title' },
|
|
640
|
+
* { name: 'lang', kind: 'select', displayName: 'Language',
|
|
641
|
+
* options: [{ value: 'en', label: 'English' }] },
|
|
642
|
+
* ],
|
|
643
|
+
* onConfirm: (values) => console.log(values),
|
|
644
|
+
* onCancel: () => {},
|
|
645
|
+
* });
|
|
646
|
+
*
|
|
647
|
+
* form.openInModal();
|
|
648
|
+
* ```
|
|
649
|
+
*/
|
|
650
|
+
declare class DeclarativeForm implements SubForm, StackableDialog {
|
|
651
|
+
/** The `.dl-form` wrapper containing the `<form>`. */
|
|
652
|
+
readonly dom: HTMLElement;
|
|
653
|
+
/** The `<form>` element the fields are rendered into. */
|
|
654
|
+
readonly formElement: HTMLFormElement;
|
|
655
|
+
private readonly options;
|
|
656
|
+
private readonly registry;
|
|
657
|
+
private readonly stack;
|
|
658
|
+
private readonly parent;
|
|
659
|
+
private readonly fieldList;
|
|
660
|
+
private readonly fieldsByName;
|
|
661
|
+
private readonly model;
|
|
662
|
+
private readonly scheduler;
|
|
663
|
+
private readonly ids;
|
|
664
|
+
private readonly tooltips;
|
|
665
|
+
private readonly tabBar;
|
|
666
|
+
private readonly buttonBar;
|
|
667
|
+
private view;
|
|
668
|
+
private readonly editingArrayEntry;
|
|
669
|
+
private embedded;
|
|
670
|
+
private ready;
|
|
671
|
+
constructor(options: DeclarativeFormOptions, internal?: InternalOptions);
|
|
672
|
+
private resolveButtons;
|
|
673
|
+
private buildFields;
|
|
674
|
+
private initialize;
|
|
675
|
+
/** Resolves once initial options and default values have been applied. */
|
|
676
|
+
whenReady(): Promise<void>;
|
|
677
|
+
private createFieldContext;
|
|
678
|
+
private contextFor;
|
|
679
|
+
private createSubForm;
|
|
680
|
+
private persistFile;
|
|
681
|
+
/**
|
|
682
|
+
* Current values, keyed by field name.
|
|
683
|
+
*
|
|
684
|
+
* Inactive fields (`isActive` returned false) are omitted. The active tab is
|
|
685
|
+
* always included under `activeTab`.
|
|
686
|
+
*/
|
|
687
|
+
getValues(): FormValues;
|
|
688
|
+
/** Imperative handle for a field, or `undefined` if there is no such field. */
|
|
689
|
+
field(name: string): FieldHandle | undefined;
|
|
690
|
+
/** Every field handle, in declaration order. */
|
|
691
|
+
get fields(): readonly FieldHandle[];
|
|
692
|
+
/** Notified whenever the form's values change. Returns an unsubscribe function. */
|
|
693
|
+
subscribeOnInput(subscriber: InputSubscriber): () => void;
|
|
694
|
+
/**
|
|
695
|
+
* Recompute derived state: field visibility, tabs, reactive content and
|
|
696
|
+
* button predicates.
|
|
697
|
+
*
|
|
698
|
+
* @param source the field whose input triggered this update
|
|
699
|
+
* @param force run even when nothing changed
|
|
700
|
+
* @param includeTab treat a tab switch as a change
|
|
701
|
+
*/
|
|
702
|
+
update(source?: FieldHandle, force?: boolean, includeTab?: boolean): Promise<void>;
|
|
703
|
+
private buttonContext;
|
|
704
|
+
/** Recompute every `computed` field. Runs before a button action fires. */
|
|
705
|
+
updateComputedFields(): Promise<void>;
|
|
706
|
+
/** Tabs in declaration order, keeping only those with at least one active field. */
|
|
707
|
+
private renderTabs;
|
|
708
|
+
/**
|
|
709
|
+
* Switch tabs.
|
|
710
|
+
*
|
|
711
|
+
* Called with no argument, it re-applies the current tab — useful after a
|
|
712
|
+
* nested dialog closes and the fields underneath need re-syncing.
|
|
713
|
+
*/
|
|
714
|
+
setActiveTab(tab?: string): void;
|
|
715
|
+
private applyActiveTab;
|
|
716
|
+
/** Re-apply the active tab. Called by the stack when this dialog is revealed. */
|
|
717
|
+
refreshTabs(): void;
|
|
718
|
+
/**
|
|
719
|
+
* Open the form as a modal dialog.
|
|
720
|
+
*
|
|
721
|
+
* Returns the outer `.dl-modal` element.
|
|
722
|
+
*/
|
|
723
|
+
openInModal(options?: OpenInModalOptions): HTMLElement;
|
|
724
|
+
/** Render the form inline, inside `host`, instead of as a floating dialog. */
|
|
725
|
+
appendInElement(host: HTMLElement, options?: OpenInModalOptions): HTMLElement;
|
|
726
|
+
private ensureView;
|
|
727
|
+
/** The outer `.dl-modal` element, once the form has been displayed. */
|
|
728
|
+
get modalElement(): HTMLElement | undefined;
|
|
729
|
+
hide(): void;
|
|
730
|
+
show(): void;
|
|
731
|
+
/** The form's HTML, for tests and snapshots. */
|
|
732
|
+
getHTML(): string;
|
|
733
|
+
get buttonCount(): number;
|
|
734
|
+
/**
|
|
735
|
+
* Run the confirm action and close.
|
|
736
|
+
*
|
|
737
|
+
* @param action defaults to the form's `onConfirm`
|
|
738
|
+
*/
|
|
739
|
+
close(action?: ButtonDescriptor['action']): Promise<void>;
|
|
740
|
+
/** Dismiss the dialog. A no-op when the form has no `onCancel`. */
|
|
741
|
+
cancel(): void;
|
|
742
|
+
/** Close without running any callback. */
|
|
743
|
+
remove(): void;
|
|
744
|
+
/** Release tooltips and listeners. Call when discarding an embedded form. */
|
|
745
|
+
destroy(): void;
|
|
746
|
+
private teardownView;
|
|
747
|
+
requestCancel(): void;
|
|
748
|
+
/**
|
|
749
|
+
* Enter pressed while this dialog is on top.
|
|
750
|
+
*
|
|
751
|
+
* Inside a textarea that allows newlines, Enter inserts one. Otherwise it
|
|
752
|
+
* confirms — unless the confirm button is disabled.
|
|
753
|
+
*/
|
|
754
|
+
requestConfirm(event: KeyboardEvent): void;
|
|
755
|
+
setTooltip(fieldName: string, text: string, icon?: string, className?: string): void;
|
|
756
|
+
setTooltipSuccess(fieldName: string, text: string): void;
|
|
757
|
+
setTooltipWarning(fieldName: string, text: string): void;
|
|
758
|
+
setTooltipError(fieldName: string, text: string): void;
|
|
759
|
+
setTooltipLoading(fieldName: string, text: string): void;
|
|
760
|
+
resetTooltip(fieldName: string): void;
|
|
761
|
+
resetTooltips(fieldNames: Iterable<string>): void;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* A repeating list of sub-records, each edited in its own nested dialog.
|
|
766
|
+
*
|
|
767
|
+
* v1 implemented this by overwriting the caller's `field.render` with a closure
|
|
768
|
+
* that also owned the suggestion checkboxes and the add/edit/delete buttons.
|
|
769
|
+
* Here the entries, the accepted suggestions and the DOM are all state on the
|
|
770
|
+
* field, and the entry dialogs are built through `context.createSubForm`.
|
|
771
|
+
*/
|
|
772
|
+
declare class ArrayField extends Field<ArrayFieldDescriptor> {
|
|
773
|
+
private entries;
|
|
774
|
+
/** Suggestions the user ticked, kept with their index so they can be un-ticked. */
|
|
775
|
+
private acceptedSuggestions;
|
|
776
|
+
protected createControl(): HTMLElement;
|
|
777
|
+
initialize(): void;
|
|
778
|
+
getValue(): FormValues[];
|
|
779
|
+
setValue(value: unknown): void;
|
|
780
|
+
onFormUpdate(_ctx: FormChangeContext): void;
|
|
781
|
+
private render;
|
|
782
|
+
private renderSuggestions;
|
|
783
|
+
private renderEntry;
|
|
784
|
+
private renderAddButton;
|
|
785
|
+
/** Default summary: every non-empty string value, comma separated. */
|
|
786
|
+
private entrySummary;
|
|
787
|
+
/** Open the add dialog, or the edit dialog when `index` is given. */
|
|
788
|
+
private openEntryDialog;
|
|
789
|
+
/** Seed each field with the entry's current value, then apply `mapFieldsOnEdit`. */
|
|
790
|
+
private editFields;
|
|
791
|
+
private validateEntry;
|
|
792
|
+
private notifyChange;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* A row of clickable option cards — a richer alternative to a select when there
|
|
797
|
+
* are few options and each needs a description or an icon.
|
|
798
|
+
*
|
|
799
|
+
* v1 called this `detailedOptions`.
|
|
800
|
+
*/
|
|
801
|
+
declare class CardsField extends Field<CardsFieldDescriptor> {
|
|
802
|
+
private value;
|
|
803
|
+
protected createControl(): HTMLElement;
|
|
804
|
+
getValue(): string;
|
|
805
|
+
setValue(value: unknown): void;
|
|
806
|
+
private select;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* A checkbox with a caption.
|
|
811
|
+
*
|
|
812
|
+
* The control is a `<span class="check">` wrapping the input and its caption —
|
|
813
|
+
* part of the frozen DOM contract, since `.dl-form form .check input` and
|
|
814
|
+
* `.check label` are styled by the default stylesheet and by consumers.
|
|
815
|
+
*/
|
|
816
|
+
declare class CheckboxField extends Field<CheckboxFieldDescriptor> {
|
|
817
|
+
private input;
|
|
818
|
+
protected createControl(): HTMLElement;
|
|
819
|
+
/** The outer `displayName` label points at the real input, not the wrapper. */
|
|
820
|
+
protected controlIdForLabel(): string;
|
|
821
|
+
getValue(): boolean;
|
|
822
|
+
setValue(value: unknown): void;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* A value derived from the rest of the form.
|
|
827
|
+
*
|
|
828
|
+
* Rendered as a hidden input (which adds `.dl-form-hidden-field` to the wrapper)
|
|
829
|
+
* and recomputed before the dialog is confirmed. The computed value lives on the
|
|
830
|
+
* field rather than in the DOM, so it is not limited to strings.
|
|
831
|
+
*/
|
|
832
|
+
declare class ComputedField extends Field<ComputedFieldDescriptor> {
|
|
833
|
+
private value;
|
|
834
|
+
protected createControl(): HTMLElement;
|
|
835
|
+
getValue(): unknown;
|
|
836
|
+
setValue(value: unknown): void;
|
|
837
|
+
/** Recompute. Awaited by `updateComputedFields()` before a button action runs. */
|
|
838
|
+
recompute(): Promise<void>;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* An escape hatch: you render the content, the form manages the value.
|
|
843
|
+
*
|
|
844
|
+
* `render` is called on mount and again on every form update. v1 injected
|
|
845
|
+
* `setValue` and `onChange` onto the DOM element; both now arrive on the render
|
|
846
|
+
* context instead, so the element stays a plain `HTMLElement`.
|
|
847
|
+
*/
|
|
848
|
+
declare class CustomField extends Field<CustomFieldDescriptor> {
|
|
849
|
+
private value;
|
|
850
|
+
protected createControl(): HTMLElement;
|
|
851
|
+
getValue(): unknown;
|
|
852
|
+
setValue(value: unknown): void;
|
|
853
|
+
initialize(): void;
|
|
854
|
+
onFormUpdate(_ctx: FormChangeContext): void;
|
|
855
|
+
private render;
|
|
856
|
+
private renderContext;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* A file picker with an image preview.
|
|
861
|
+
*
|
|
862
|
+
* The field's value is the URL returned by the form's `persistFile` hook, not
|
|
863
|
+
* the `File` itself — the dialog stores a reference, the app decides where the
|
|
864
|
+
* bytes go.
|
|
865
|
+
*/
|
|
866
|
+
declare class FileField extends Field<FileFieldDescriptor> {
|
|
867
|
+
private input;
|
|
868
|
+
private preview;
|
|
869
|
+
private url;
|
|
870
|
+
protected createControl(): HTMLElement;
|
|
871
|
+
protected controlIdForLabel(): string;
|
|
872
|
+
getValue(): string;
|
|
873
|
+
setValue(value: unknown): void;
|
|
874
|
+
setLoading(loading: boolean): void;
|
|
875
|
+
private handlePick;
|
|
876
|
+
/**
|
|
877
|
+
* Surface an upload failure.
|
|
878
|
+
*
|
|
879
|
+
* Dispatches a cancelable `dl-error` event on the field so the app can show
|
|
880
|
+
* its own message; if nothing calls `preventDefault()`, the error is logged
|
|
881
|
+
* rather than vanishing. Throwing here would only produce an unhandled
|
|
882
|
+
* rejection, since this runs from an event handler.
|
|
883
|
+
*/
|
|
884
|
+
private reportError;
|
|
885
|
+
private clearPreview;
|
|
886
|
+
private renderPreview;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* A presentational paragraph. Holds no value and never appears in `getValues()`.
|
|
891
|
+
*/
|
|
892
|
+
declare class MessageField extends Field<MessageFieldDescriptor> {
|
|
893
|
+
protected createControl(): HTMLElement;
|
|
894
|
+
get contributesValue(): boolean;
|
|
895
|
+
getValue(): undefined;
|
|
896
|
+
setValue(): void;
|
|
897
|
+
onFormUpdate(_ctx: FormChangeContext): void;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** A `<dl-select>` combobox, single or multiple. */
|
|
901
|
+
declare class SelectField extends Field<SelectFieldDescriptor> {
|
|
902
|
+
private select;
|
|
903
|
+
protected get wrapperExtraClass(): string;
|
|
904
|
+
protected createControl(): HTMLElement;
|
|
905
|
+
initialize(): Promise<void>;
|
|
906
|
+
getValue(): string | string[];
|
|
907
|
+
setValue(value: unknown): void;
|
|
908
|
+
setLoading(loading: boolean): void;
|
|
909
|
+
onFormUpdate(_ctx: FormChangeContext, triggerName: string | undefined): void;
|
|
910
|
+
/**
|
|
911
|
+
* Populate the options.
|
|
912
|
+
*
|
|
913
|
+
* Each load claims a generation token; if a newer load starts while this one
|
|
914
|
+
* is still in flight, the stale result is dropped instead of clobbering the
|
|
915
|
+
* fresher options. v1 had no such guard, so a slow response from an earlier
|
|
916
|
+
* keystroke could repopulate the select with outdated values.
|
|
917
|
+
*/
|
|
918
|
+
private loadOptions;
|
|
919
|
+
private createOption;
|
|
920
|
+
private reportOptionsError;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** A multi-line `<textarea>`. */
|
|
924
|
+
declare class TextAreaField extends Field<TextAreaFieldDescriptor> {
|
|
925
|
+
private textarea;
|
|
926
|
+
protected createControl(): HTMLElement;
|
|
927
|
+
/** Whether Enter inserts a newline rather than confirming the dialog. */
|
|
928
|
+
get allowsNewlines(): boolean;
|
|
929
|
+
getValue(): string;
|
|
930
|
+
setValue(value: unknown): void;
|
|
931
|
+
onFormUpdate(_ctx: FormChangeContext): void;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** The fallback kind: a single-line `<input>`. */
|
|
935
|
+
declare class TextField extends Field<TextFieldDescriptor> {
|
|
936
|
+
private input;
|
|
937
|
+
protected createControl(): HTMLElement;
|
|
938
|
+
getValue(): string;
|
|
939
|
+
setValue(value: unknown): void;
|
|
940
|
+
onFormUpdate(_ctx: FormChangeContext): void;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Base class for the custom elements.
|
|
945
|
+
*
|
|
946
|
+
* `class X extends HTMLElement` is evaluated as soon as the module loads, which
|
|
947
|
+
* throws outright in Node — breaking any server-rendered or prerendered app
|
|
948
|
+
* that so much as imports the package. Falling back to an empty class keeps the
|
|
949
|
+
* module importable; the stand-in is never instantiated, because custom
|
|
950
|
+
* elements are only registered when `customElements` exists.
|
|
951
|
+
*/
|
|
952
|
+
declare const CustomElementBase: typeof HTMLElement;
|
|
953
|
+
/**
|
|
954
|
+
* An option inside a {@link DlSelect}.
|
|
955
|
+
*
|
|
956
|
+
* v1 never registered this element, leaving it an `HTMLUnknownElement`.
|
|
957
|
+
* Registering it gives it a stable prototype and somewhere to hang the option
|
|
958
|
+
* semantics the accessibility work needs.
|
|
959
|
+
*/
|
|
960
|
+
declare class DlOption extends CustomElementBase {
|
|
961
|
+
/** The value this option contributes, falling back to its text. */
|
|
962
|
+
get optionValue(): string;
|
|
963
|
+
/** Text shown once selected, which may differ from the option's own label. */
|
|
964
|
+
get displayText(): string;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* A filterable combobox that supports async options and multi-select.
|
|
968
|
+
*
|
|
969
|
+
* Deliberately light-DOM: consumers style its internals (`.input-wrapper`,
|
|
970
|
+
* `.options-wrapper`, `dl-option`) and the options popup is reparented to
|
|
971
|
+
* `document.body` while open so it can escape a scrolling container.
|
|
972
|
+
*/
|
|
973
|
+
declare class DlSelect extends CustomElementBase {
|
|
974
|
+
readonly optionsWrapper: HTMLDivElement;
|
|
975
|
+
readonly inputWrapper: HTMLSpanElement;
|
|
976
|
+
readonly noMatchesHint: HTMLSpanElement;
|
|
977
|
+
readonly selectedContainer: HTMLSpanElement;
|
|
978
|
+
readonly inputField: HTMLInputElement;
|
|
979
|
+
readonly arrow: SVGSVGElement;
|
|
980
|
+
multiple: boolean;
|
|
981
|
+
isLoading: boolean;
|
|
982
|
+
private initialized;
|
|
983
|
+
private loadingTimeouts;
|
|
984
|
+
private loadingStartedAt;
|
|
985
|
+
private pendingValue;
|
|
986
|
+
private selectedOption;
|
|
987
|
+
private focusedOption;
|
|
988
|
+
private repositionListener;
|
|
989
|
+
private scrollTargets;
|
|
990
|
+
constructor();
|
|
991
|
+
connectedCallback(): void;
|
|
992
|
+
disconnectedCallback(): void;
|
|
993
|
+
/** Move any author-provided `<dl-option>` children into the popup. */
|
|
994
|
+
private mount;
|
|
995
|
+
private adoptOption;
|
|
996
|
+
addOption(option: DlOption | HTMLElement): void;
|
|
997
|
+
removeAllOptions(): void;
|
|
998
|
+
/** All options currently in the popup. */
|
|
999
|
+
get options(): DlOption[];
|
|
1000
|
+
/**
|
|
1001
|
+
* Enter the loading state.
|
|
1002
|
+
*
|
|
1003
|
+
* The skeleton only appears after a short delay, so options that resolve
|
|
1004
|
+
* quickly never flash one. Repeated calls are safe.
|
|
1005
|
+
*/
|
|
1006
|
+
setLoadingStatus(): void;
|
|
1007
|
+
unsetLoadingStatus(): void;
|
|
1008
|
+
getValue(): string | string[] | undefined;
|
|
1009
|
+
setValue(value: unknown): void;
|
|
1010
|
+
/**
|
|
1011
|
+
* Find an option by its `value` attribute, falling back to its text.
|
|
1012
|
+
*
|
|
1013
|
+
* v1 concatenated the value straight into an attribute selector, which threw
|
|
1014
|
+
* on any value containing a quote or a backslash.
|
|
1015
|
+
*/
|
|
1016
|
+
private findOption;
|
|
1017
|
+
private selectOption;
|
|
1018
|
+
private deselectOption;
|
|
1019
|
+
/** Mark every option carrying this value, since duplicates are allowed. */
|
|
1020
|
+
private markSelected;
|
|
1021
|
+
private createTag;
|
|
1022
|
+
private tagFor;
|
|
1023
|
+
private emitChange;
|
|
1024
|
+
filterOptions(query: string): void;
|
|
1025
|
+
private visibleOptions;
|
|
1026
|
+
private clearFocusedOption;
|
|
1027
|
+
private focusOption;
|
|
1028
|
+
private step;
|
|
1029
|
+
private readonly onKeyDown;
|
|
1030
|
+
private get placeholderText();
|
|
1031
|
+
private updatePlaceholder;
|
|
1032
|
+
/** Open the popup. Named `open` because `focus()` is `HTMLElement`'s. */
|
|
1033
|
+
open(): void;
|
|
1034
|
+
close(): void;
|
|
1035
|
+
private reposition;
|
|
1036
|
+
private attachRepositionListeners;
|
|
1037
|
+
private detachRepositionListeners;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Register `<dl-select>` and `<dl-option>`.
|
|
1041
|
+
*
|
|
1042
|
+
* Idempotent, and a no-op outside a browser. Called automatically whenever a
|
|
1043
|
+
* form builds a select field.
|
|
1044
|
+
*/
|
|
1045
|
+
declare function defineDlSelect(): void;
|
|
1046
|
+
declare global {
|
|
1047
|
+
interface HTMLElementTagNameMap {
|
|
1048
|
+
'dl-select': DlSelect;
|
|
1049
|
+
'dl-option': DlOption;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Structural styles for `<dl-select>`.
|
|
1055
|
+
*
|
|
1056
|
+
* These live in JS rather than in the shipped stylesheet because the component
|
|
1057
|
+
* does not work without them — the options popup is absolutely positioned and
|
|
1058
|
+
* toggled through these rules. Everything here is themable through the `--dl-*`
|
|
1059
|
+
* custom properties, and the form/modal chrome ships separately as
|
|
1060
|
+
* `declarative-forms/styles.css`.
|
|
1061
|
+
*
|
|
1062
|
+
* v1 appended this on `window.load` as an import side effect, which broke SSR
|
|
1063
|
+
* and leaked between tests. It is now injected lazily, the first time a
|
|
1064
|
+
* `<dl-select>` is actually connected to a document.
|
|
1065
|
+
*/
|
|
1066
|
+
declare const DL_SELECT_STYLES = "\n@keyframes dl-placeholder-shimmer {\n 0% { background-position: -468px 0 }\n 100% { background-position: 468px 0 }\n}\n\ndl-select {\n display: block;\n position: relative;\n font-weight: 300;\n font-family: var(--dl-font-family, 'Rubik');\n --dl-select-input-width: 370px;\n}\n\ndl-select .input-wrapper {\n display: inline-block;\n border-width: 2px;\n border-style: solid;\n border-color: var(--dl-line-color, #ddd);\n background-color: var(--dl-options-background, #fff);\n color: var(--dl-options-text-color, #545454);\n border-radius: 4px;\n}\n\ndl-select.dl-focused .input-wrapper {\n border-color: var(--dl-focused-line-color, #bbb);\n}\n\ndl-select.dl-focused .input-wrapper svg {\n border-left: 1px solid var(--dl-focused-line-color, #bbb);\n fill: var(--dl-focused-line-color, #bbb);\n}\n\ndl-select .input-wrapper input {\n width: var(--dl-select-input-width);\n color: var(--dl-options-text-color, #545454);\n outline-width: 0;\n margin-top: 2px;\n padding: 6px;\n font-size: var(--dl-font-size, 0.9em);\n font-family: var(--dl-font-family, 'Rubik');\n font-weight: 300;\n border: 0;\n border-radius: 4px;\n float: left;\n cursor: pointer;\n box-sizing: content-box;\n background: transparent;\n}\n\ndl-select .input-wrapper input::placeholder {\n color: var(--dl-options-inactive-text-color, #545454);\n}\n\ndl-select .input-wrapper svg {\n width: 20px;\n height: 20px;\n margin-top: var(--dl-drop-down-icon-margin-top, 5px);\n margin-right: 4px;\n padding-left: 4px;\n fill: var(--dl-line-color, #ddd);\n float: right;\n border-left: 1px solid var(--dl-line-color, #ddd);\n cursor: pointer;\n}\n\n.options-wrapper {\n position: absolute;\n left: 0;\n top: 20px;\n font-size: var(--dl-font-size, 0.9em);\n width: 100%;\n max-height: 230px;\n overflow-y: auto;\n overflow-x: hidden;\n box-shadow: 0 0 10px -2px rgba(0, 0, 0, 0.4);\n z-index: 100;\n background-color: var(--dl-options-background, #fff);\n font-family: var(--dl-font-family, 'Rubik');\n border-top: 1px solid var(--dl-focused-line-color, #bbb);\n border-bottom: 1px solid var(--dl-focused-line-color, #bbb);\n border-radius: 4px;\n}\n\n.options-wrapper dl-option {\n display: block;\n border-left: 1px solid var(--dl-focused-line-color, #bbb);\n border-right: 1px solid var(--dl-focused-line-color, #bbb);\n cursor: pointer;\n padding-top: var(--dl-options-padding-top, 5px);\n padding-right: var(--dl-options-padding-right, 5px);\n padding-bottom: var(--dl-options-padding-bottom, 5px);\n padding-left: var(--dl-options-padding-left, 5px);\n color: var(--dl-options-text-color, #545454);\n background-color: var(--dl-options-background, #fff);\n}\n\n.options-wrapper dl-option[selected=\"true\"] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.options-wrapper dl-option:hover,\n.options-wrapper dl-option.dl-focused {\n background-color: var(--dl-options-option-hover-color, rgba(224, 240, 227, 0.4));\n}\n\n.options-wrapper .noMatchesHint {\n border-left: 1px solid var(--dl-focused-line-color, #bbb);\n border-right: 1px solid var(--dl-focused-line-color, #bbb);\n padding: 5px;\n font-style: italic;\n color: var(--dl-options-text-color, #545454);\n}\n\ndl-select .dl-option-tag {\n float: right;\n border: 1px solid #888;\n font-size: 0.8em;\n padding: 2px;\n font-family: 'Source Code Pro', monospace;\n border-radius: 2px;\n margin-top: -1px;\n}\n\ndl-select .dl-option-tag:last-child {\n margin-right: 10px;\n}\n\n.dl-select-no-options-available svg path { display: none; }\n.dl-select-no-options-available svg { border-left: none !important; }\n\n.dl-select-loading svg,\n.dl-select-no-options-available svg,\n.dl-select-loading input,\n.dl-select-no-options-available input {\n pointer-events: none;\n}\n\n.dl-select-loading .input-wrapper {\n animation: dl-placeholder-shimmer 1.25s linear infinite forwards;\n background: linear-gradient(\n to right,\n var(--dl-select-loading-col1, #eee) 10%,\n var(--dl-select-loading-col2, #ddd) 18%,\n var(--dl-select-loading-col1, #eee) 33%\n );\n background-size: 800px 104px;\n position: relative;\n width: 412px;\n}\n\n.dl-select-loading input { opacity: 0; }\n\n.dl-field-one-third input,\n.dl-field-two-third input {\n --dl-select-input-width: 127px;\n}\n\nbody.dark-theme dl-select {\n --dl-select-loading-col1: #282727;\n --dl-select-loading-col2: #505656;\n}\n";
|
|
1067
|
+
/**
|
|
1068
|
+
* Inject the `<dl-select>` styles once per document.
|
|
1069
|
+
*
|
|
1070
|
+
* Safe to call repeatedly and safe to call before `document.body` exists.
|
|
1071
|
+
*/
|
|
1072
|
+
declare function injectStyles(): void;
|
|
1073
|
+
|
|
1074
|
+
interface ButtonBarCallbacks {
|
|
1075
|
+
/** Resolve once all pending form work has settled. */
|
|
1076
|
+
whenSettled(): Promise<void>;
|
|
1077
|
+
/** Recompute `computed` fields before an action runs. */
|
|
1078
|
+
updateComputedFields(): Promise<void>;
|
|
1079
|
+
getValues(): Record<string, unknown>;
|
|
1080
|
+
/** Run the action and close the dialog. */
|
|
1081
|
+
closeWith(action: ButtonDescriptor['action']): Promise<void>;
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* The dialog's lower bar.
|
|
1085
|
+
*
|
|
1086
|
+
* Buttons are real `<button type="button">` elements rather than v1's
|
|
1087
|
+
* `<div class="btn">`. They keep the `.btn` class, so every existing stylesheet
|
|
1088
|
+
* still matches, and the shipped CSS normalises the font so the change is not
|
|
1089
|
+
* visible — but they are now focusable and operable from the keyboard.
|
|
1090
|
+
*/
|
|
1091
|
+
declare class ButtonBar {
|
|
1092
|
+
private readonly config;
|
|
1093
|
+
private readonly callbacks;
|
|
1094
|
+
readonly element: HTMLElement;
|
|
1095
|
+
private readonly buttons;
|
|
1096
|
+
constructor(config: ButtonMap, callbacks: ButtonBarCallbacks);
|
|
1097
|
+
get count(): number;
|
|
1098
|
+
/** The first button — the one Enter activates. */
|
|
1099
|
+
get primary(): HTMLElement | undefined;
|
|
1100
|
+
/** Whether the primary button is currently disabled. */
|
|
1101
|
+
get primaryDisabled(): boolean;
|
|
1102
|
+
private createButton;
|
|
1103
|
+
private activate;
|
|
1104
|
+
/**
|
|
1105
|
+
* Re-evaluate every `isActive` / `isVisible` predicate.
|
|
1106
|
+
*
|
|
1107
|
+
* Buttons without an `id` are skipped, matching v1 — an id is what opts a
|
|
1108
|
+
* button into state management.
|
|
1109
|
+
*/
|
|
1110
|
+
refresh(ctx: ButtonContext): Promise<void>;
|
|
1111
|
+
/**
|
|
1112
|
+
* Apply a predicate result.
|
|
1113
|
+
*
|
|
1114
|
+
* A synchronous predicate is applied immediately. v1 always set the button
|
|
1115
|
+
* disabled first and cleared it in a `.then()`, which made every button flash
|
|
1116
|
+
* disabled on every keystroke and let a well-timed click be dropped.
|
|
1117
|
+
*/
|
|
1118
|
+
private applyPredicate;
|
|
1119
|
+
/** The configured buttons, for callers that want to invoke an action directly. */
|
|
1120
|
+
get descriptors(): ButtonMap;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* The dialog's tab strip.
|
|
1125
|
+
*
|
|
1126
|
+
* v1 resolved tabs with `document.querySelector('.dl-tab-btn.active')`, so two
|
|
1127
|
+
* open forms fought over each other's active tab. This owns its buttons and
|
|
1128
|
+
* never queries the document.
|
|
1129
|
+
*
|
|
1130
|
+
* The rendered shape is part of the frozen contract: `.dl-tab-btn` elements are
|
|
1131
|
+
* direct, clickable siblings inside `.tabWrapper`, each carrying the tab label
|
|
1132
|
+
* with whitespace stripped as a class (`'Find Citation'` → `.FindCitation`), and
|
|
1133
|
+
* a `seen` class once visited.
|
|
1134
|
+
*/
|
|
1135
|
+
declare class TabBar {
|
|
1136
|
+
private readonly onSelect;
|
|
1137
|
+
readonly element: HTMLElement;
|
|
1138
|
+
private buttons;
|
|
1139
|
+
private seen;
|
|
1140
|
+
private active;
|
|
1141
|
+
constructor(onSelect: (tab: string) => void);
|
|
1142
|
+
get activeTab(): string | undefined;
|
|
1143
|
+
/** Rebuild the strip from the tabs that currently have at least one visible field. */
|
|
1144
|
+
render(tabs: readonly string[]): void;
|
|
1145
|
+
/**
|
|
1146
|
+
* Mark a tab active.
|
|
1147
|
+
*
|
|
1148
|
+
* Selecting a tab that is not currently rendered is a no-op rather than an
|
|
1149
|
+
* error — a tab can disappear when the fields in it all become inactive.
|
|
1150
|
+
*/
|
|
1151
|
+
select(tab: string): boolean;
|
|
1152
|
+
get tabs(): string[];
|
|
1153
|
+
get isEmpty(): boolean;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
interface ModalViewOptions {
|
|
1157
|
+
tabBar: TabBar;
|
|
1158
|
+
buttonBar: ButtonBar;
|
|
1159
|
+
/** Render a close button. Omit for a dialog that cannot be dismissed. */
|
|
1160
|
+
onCancel?: () => void;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* The modal chrome.
|
|
1164
|
+
*
|
|
1165
|
+
* ```html
|
|
1166
|
+
* <div class="dl-modal">
|
|
1167
|
+
* <div class="modal">
|
|
1168
|
+
* <div class="up-bar"><button class="cancelBtn secondary"></button></div>
|
|
1169
|
+
* <div class="tabWrapper">…</div>
|
|
1170
|
+
* <div class="modal-content">…the form…</div>
|
|
1171
|
+
* <div class="low-bar">…buttons…</div>
|
|
1172
|
+
* </div>
|
|
1173
|
+
* </div>
|
|
1174
|
+
* ```
|
|
1175
|
+
*
|
|
1176
|
+
* Every one of these class names is overridden by at least one consumer, so the
|
|
1177
|
+
* structure is fixed. The upper bar is always present but only carries the
|
|
1178
|
+
* `up-bar` class when the dialog is dismissable — a v1 quirk that consumer CSS
|
|
1179
|
+
* relies on to reserve (or not reserve) the header strip.
|
|
1180
|
+
*/
|
|
1181
|
+
declare class ModalView {
|
|
1182
|
+
readonly wrapper: HTMLElement;
|
|
1183
|
+
readonly modal: HTMLElement;
|
|
1184
|
+
readonly content: HTMLElement;
|
|
1185
|
+
readonly upBar: HTMLElement;
|
|
1186
|
+
constructor(options: ModalViewOptions);
|
|
1187
|
+
/** Attach to the document body. Used by `openInModal`. */
|
|
1188
|
+
mount(): void;
|
|
1189
|
+
setVisible(visible: boolean): void;
|
|
1190
|
+
hide(): void;
|
|
1191
|
+
show(): void;
|
|
1192
|
+
/**
|
|
1193
|
+
* Record how many dialogs this one was opened on top of.
|
|
1194
|
+
*
|
|
1195
|
+
* A stacked dialog inherits a backdrop that is already on screen: the dialog
|
|
1196
|
+
* below is hidden in the same frame this one appears. Fading this backdrop in
|
|
1197
|
+
* would leave the page uncovered for the length of the animation, so themes
|
|
1198
|
+
* suppress it on the `dl-modal-stacked` class. The depth also goes on the
|
|
1199
|
+
* wrapper as `--dl-stack-depth`, so a theme can show how deep the stack is.
|
|
1200
|
+
*/
|
|
1201
|
+
setStackDepth(depth: number): void;
|
|
1202
|
+
/** Mark the dialog as embedded rather than floating. */
|
|
1203
|
+
setEmbedded(): void;
|
|
1204
|
+
addModalClasses(names: readonly string[] | undefined): void;
|
|
1205
|
+
addWrapperClasses(names: readonly string[] | undefined): void;
|
|
1206
|
+
remove(): void;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Zero-dependency tooltip.
|
|
1211
|
+
*
|
|
1212
|
+
* Renders a positioned bubble on hover and on focus, placed to the right of the
|
|
1213
|
+
* trigger and flipped to the left when it would overflow the viewport. Content
|
|
1214
|
+
* is an HTML string because tooltip text has always supported markup; callers
|
|
1215
|
+
* mark it with `html()` at the descriptor level.
|
|
1216
|
+
*/
|
|
1217
|
+
declare class NativeTooltipProvider implements TooltipProvider {
|
|
1218
|
+
private readonly attachments;
|
|
1219
|
+
attach(element: HTMLElement, content: string): void;
|
|
1220
|
+
detach(element: HTMLElement): void;
|
|
1221
|
+
private show;
|
|
1222
|
+
private hide;
|
|
1223
|
+
private position;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Structural equality for form data snapshots.
|
|
1228
|
+
*
|
|
1229
|
+
* Replaces the `@react-hookz/deep-equal` dependency. Scoped to what form values
|
|
1230
|
+
* can actually be: primitives, plain objects, arrays, `Date`, and `File`/`Blob`
|
|
1231
|
+
* (compared by identity, since their contents are not cheaply comparable).
|
|
1232
|
+
*/
|
|
1233
|
+
declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
1234
|
+
|
|
1235
|
+
export { ArrayField, type ArrayFieldDescriptor, type BaseFieldDescriptor, ButtonBar, type ButtonContext, type ButtonDescriptor, type ButtonMap, CardsField, type CardsFieldDescriptor, CheckboxField, type CheckboxFieldDescriptor, ComputedField, type ComputedFieldDescriptor, CustomField, type CustomFieldDescriptor, DL_SELECT_STYLES, DeclarativeForm, type DeclarativeFormOptions, DlOption, DlSelect, Field, type FieldConstructor, type FieldContext, type FieldDescriptor, type FieldHandle, type FieldKind, type FieldMessage, FieldRegistry, FileField, type FileFieldDescriptor, type FormChangeContext, type FormContext, FormModel, type FormValues, IdGenerator, MessageField, type MessageFieldDescriptor, ModalStack, ModalView, NativeTooltipProvider, type OpenInModalOptions, type OptionsErrorContext, type Reactive, type RenderContext, SafeHtml, SelectField, type SelectFieldDescriptor, type SelectOption, type StackableDialog, type SubForm, type SubFormOptions, TabBar, type TabSpec, TextAreaField, type TextAreaFieldDescriptor, TextField, type TextFieldDescriptor, type TextOrHtml, TooltipController, type TooltipDescriptor, type TooltipProvider, UpdateScheduler, deepEqual, defaultFieldRegistry, defineDlSelect, escapeHtml, globalModalStack, html, injectStyles, isSafeHtml };
|