@fieldia/viewer 0.3.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldia/viewer",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Mount a Fieldia page in any web page: plain DOM, no framework.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://fieldia.dev",
@@ -24,8 +24,8 @@
24
24
  "./package.json": "./package.json"
25
25
  },
26
26
  "dependencies": {
27
- "@fieldia/core": "0.3.0",
28
- "@fieldia/widgets": "0.3.0",
27
+ "@fieldia/core": "0.5.0",
28
+ "@fieldia/widgets": "0.5.0",
29
29
  "tslib": "^2.8.1"
30
30
  },
31
31
  "sideEffects": false,
package/src/index.d.mts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './lib/viewer.mjs';
2
+ export * from './lib/dialog.mjs';
package/src/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './lib/viewer';
2
+ export * from './lib/dialog';
package/src/index.js CHANGED
@@ -2,4 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  tslib_1.__exportStar(require("./lib/viewer"), exports);
5
+ tslib_1.__exportStar(require("./lib/dialog"), exports);
5
6
  //# sourceMappingURL=index.js.map
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/viewer/src/index.ts"],"names":[],"mappings":";;;AAAA,uDAA6B"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/viewer/src/index.ts"],"names":[],"mappings":";;;AAAA,uDAA6B;AAC7B,uDAA6B"}
package/src/index.mjs CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './lib/viewer.mjs';
2
+ export * from './lib/dialog.mjs';
2
3
  //# sourceMappingURL=index.mjs.map
package/src/index.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../../../../libs/viewer/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../../../../libs/viewer/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,48 @@
1
+ import type { Locale, RecordId, RelatedRecord, Values } from '@fieldia/core';
2
+ import { type Skin, type ViewerOptions } from './viewer.mjs';
3
+ /**
4
+ * A page in a dialog: a new related record, a line to edit, a quick task. The
5
+ * dialog saves (Save & Close) or closes without saving (Discard, ×, Escape);
6
+ * the page's own Save and Discard stay hidden. The focus moves into the dialog,
7
+ * Tab stays inside it, and the focus goes back where it was when it closes.
8
+ */
9
+ export interface FormDialogOptions extends ViewerOptions {
10
+ title: string;
11
+ size?: 'small' | 'medium' | 'large' | 'full';
12
+ /**
13
+ * "save" (the default) saves the record through the data source. "values"
14
+ * only checks the form and hands its values back, for something saved with
15
+ * a bigger record, such as a line.
16
+ */
17
+ mode?: 'save' | 'values';
18
+ /**
19
+ * Recalculates the values as they change, for a dialog with no data source of
20
+ * its own: a line's subtotal following its quantity. What comes back is shown.
21
+ */
22
+ recompute?: (values: Values) => Promise<Values>;
23
+ /** Where the dialog goes: the document's body by default. */
24
+ container?: HTMLElement;
25
+ }
26
+ export interface FormDialogResult {
27
+ /** True when Save & Close was pressed and the form accepted. */
28
+ saved: boolean;
29
+ recordId: RecordId | null;
30
+ values: Values;
31
+ }
32
+ export declare function openFormDialog(options: FormDialogOptions): Promise<FormDialogResult>;
33
+ export interface SearchDialogOptions {
34
+ title: string;
35
+ /** Finds records for what is typed: the field's own search, filter and all. */
36
+ search(query: string, limit: number): Promise<RelatedRecord[]>;
37
+ locale?: Locale;
38
+ skin?: Skin;
39
+ dir?: 'ltr' | 'rtl';
40
+ /** Where the dialog goes: the document's body by default. */
41
+ container?: HTMLElement;
42
+ }
43
+ /**
44
+ * A searchable list in a dialog, for "Search more…": every record the field may
45
+ * point to, narrowed as the person types, picked with a click or the keyboard.
46
+ * Resolves with the record picked, or null when closed.
47
+ */
48
+ export declare function openSearchDialog(options: SearchDialogOptions): Promise<RelatedRecord | null>;
@@ -0,0 +1,48 @@
1
+ import type { Locale, RecordId, RelatedRecord, Values } from '@fieldia/core';
2
+ import { type Skin, type ViewerOptions } from './viewer';
3
+ /**
4
+ * A page in a dialog: a new related record, a line to edit, a quick task. The
5
+ * dialog saves (Save & Close) or closes without saving (Discard, ×, Escape);
6
+ * the page's own Save and Discard stay hidden. The focus moves into the dialog,
7
+ * Tab stays inside it, and the focus goes back where it was when it closes.
8
+ */
9
+ export interface FormDialogOptions extends ViewerOptions {
10
+ title: string;
11
+ size?: 'small' | 'medium' | 'large' | 'full';
12
+ /**
13
+ * "save" (the default) saves the record through the data source. "values"
14
+ * only checks the form and hands its values back, for something saved with
15
+ * a bigger record, such as a line.
16
+ */
17
+ mode?: 'save' | 'values';
18
+ /**
19
+ * Recalculates the values as they change, for a dialog with no data source of
20
+ * its own: a line's subtotal following its quantity. What comes back is shown.
21
+ */
22
+ recompute?: (values: Values) => Promise<Values>;
23
+ /** Where the dialog goes: the document's body by default. */
24
+ container?: HTMLElement;
25
+ }
26
+ export interface FormDialogResult {
27
+ /** True when Save & Close was pressed and the form accepted. */
28
+ saved: boolean;
29
+ recordId: RecordId | null;
30
+ values: Values;
31
+ }
32
+ export declare function openFormDialog(options: FormDialogOptions): Promise<FormDialogResult>;
33
+ export interface SearchDialogOptions {
34
+ title: string;
35
+ /** Finds records for what is typed: the field's own search, filter and all. */
36
+ search(query: string, limit: number): Promise<RelatedRecord[]>;
37
+ locale?: Locale;
38
+ skin?: Skin;
39
+ dir?: 'ltr' | 'rtl';
40
+ /** Where the dialog goes: the document's body by default. */
41
+ container?: HTMLElement;
42
+ }
43
+ /**
44
+ * A searchable list in a dialog, for "Search more…": every record the field may
45
+ * point to, narrowed as the person types, picked with a click or the keyboard.
46
+ * Resolves with the record picked, or null when closed.
47
+ */
48
+ export declare function openSearchDialog(options: SearchDialogOptions): Promise<RelatedRecord | null>;
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openFormDialog = openFormDialog;
4
+ exports.openSearchDialog = openSearchDialog;
5
+ const widgets_1 = require("@fieldia/widgets");
6
+ const viewer_1 = require("./viewer");
7
+ let dialogs = 0;
8
+ const FOCUSABLE = 'button, [href], input, select, textarea, [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
9
+ function openFormDialog(options) {
10
+ const doc = options.container?.ownerDocument ?? document;
11
+ const container = options.container ?? doc.body;
12
+ const opener = doc.activeElement instanceof doc.defaultView.HTMLElement ? doc.activeElement : null;
13
+ const labels = { ...viewer_1.VIEWER_LABELS[options.locale ?? 'en'], ...options.labels };
14
+ const id = `fd-dialog-${++dialogs}`;
15
+ const make = (tag, attrs = {}, text) => {
16
+ const element = doc.createElement(tag);
17
+ for (const [name, value] of Object.entries(attrs))
18
+ element.setAttribute(name, value);
19
+ if (text !== undefined)
20
+ element.textContent = text;
21
+ return element;
22
+ };
23
+ const title = make('h2', { id: `${id}-title`, class: 'fd-form-dialog-title' }, options.title);
24
+ const closeButton = make('button', { type: 'button', class: 'fd-dialog-close', 'aria-label': labels.close }, '×');
25
+ const body = make('div', { class: 'fd-form-dialog-body' });
26
+ const discard = make('button', { type: 'button', class: 'fd-button' }, labels.discard);
27
+ const saveClose = make('button', { type: 'button', class: 'fd-button fd-button-primary' }, labels.saveClose);
28
+ const box = make('div', {
29
+ // Its own tokens and skin: the dialog sits outside the page that opened it.
30
+ class: `fd-theme fd-form-dialog fd-size-${options.size ?? 'medium'}`,
31
+ 'data-fd-skin': options.skin ?? 'underline',
32
+ role: 'dialog',
33
+ 'aria-modal': 'true',
34
+ 'aria-labelledby': title.id,
35
+ tabindex: '-1',
36
+ });
37
+ if (options.dir)
38
+ box.setAttribute('dir', options.dir);
39
+ box.append(make('div', { class: 'fd-form-dialog-head' }), body, make('div', { class: 'fd-actions fd-actions-end fd-form-dialog-foot' }));
40
+ box.firstElementChild?.append(title, closeButton);
41
+ box.lastElementChild?.append(discard, saveClose);
42
+ const backdrop = make('div', { class: 'fd-dialog-backdrop fd-form-dialog-backdrop' });
43
+ backdrop.append(box);
44
+ container.append(backdrop);
45
+ const handle = (0, viewer_1.mountViewer)(body, { ...options, showActions: false });
46
+ let done = false;
47
+ if (options.recompute)
48
+ recalculate(handle.form, options.page.fields, options.recompute, () => done);
49
+ const focusables = () => [...box.querySelectorAll(FOCUSABLE)].filter((element) => !element.closest('[hidden], [inert]') && !element.disabled);
50
+ // Into the dialog: its first field, once the record is in, or the dialog itself meanwhile.
51
+ const firstField = () => body.querySelector('input:not([type="hidden"]), select, textarea, [contenteditable="true"]');
52
+ (firstField() ?? box).focus();
53
+ void handle.form.settled().then(() => {
54
+ if (doc.activeElement === box || !box.contains(doc.activeElement))
55
+ firstField()?.focus();
56
+ });
57
+ return new Promise((resolve) => {
58
+ const close = (saved) => {
59
+ if (done)
60
+ return;
61
+ done = true;
62
+ const state = handle.form.getState();
63
+ // The form never changes its values in place, so these are safe to hand back.
64
+ const result = { saved, recordId: state.recordId ?? null, values: state.values };
65
+ handle.destroy();
66
+ backdrop.remove();
67
+ opener?.focus();
68
+ resolve(result);
69
+ };
70
+ saveClose.addEventListener('click', async () => {
71
+ saveClose.disabled = true;
72
+ const accepted = options.mode === 'values' ? handle.check() : await handle.save();
73
+ saveClose.disabled = false;
74
+ if (accepted)
75
+ close(true);
76
+ });
77
+ discard.addEventListener('click', () => close(false));
78
+ closeButton.addEventListener('click', () => close(false));
79
+ box.addEventListener('keydown', (event) => {
80
+ // A key a field used itself (Escape closing a list) is not the dialog's.
81
+ if (event.defaultPrevented)
82
+ return;
83
+ if (event.key === 'Escape') {
84
+ event.preventDefault();
85
+ close(false);
86
+ }
87
+ else if (event.key === 'Tab') {
88
+ const all = focusables();
89
+ if (!all.length)
90
+ return;
91
+ const first = all[0];
92
+ const last = all[all.length - 1];
93
+ if (event.shiftKey && doc.activeElement === first) {
94
+ event.preventDefault();
95
+ last.focus();
96
+ }
97
+ else if (!event.shiftKey && doc.activeElement === last) {
98
+ event.preventDefault();
99
+ first.focus();
100
+ }
101
+ }
102
+ });
103
+ });
104
+ }
105
+ /** Each change of the values asks for them recalculated, and shows the answer unless they changed again meanwhile. */
106
+ function recalculate(form, fields, recompute, closed) {
107
+ let seen = JSON.stringify(form.getState().values);
108
+ let writing = false;
109
+ form.subscribe(() => {
110
+ const values = form.getState().values;
111
+ const now = JSON.stringify(values);
112
+ if (writing || now === seen)
113
+ return;
114
+ seen = now;
115
+ recompute(values).then((next) => {
116
+ if (closed() || JSON.stringify(form.getState().values) !== now)
117
+ return; // a newer answer is on its way
118
+ writing = true;
119
+ for (const [name, value] of Object.entries(next)) {
120
+ if (name in fields && JSON.stringify(values[name] ?? null) !== JSON.stringify(value ?? null))
121
+ form.setValue(name, value);
122
+ }
123
+ writing = false;
124
+ seen = JSON.stringify(form.getState().values);
125
+ }, () => undefined);
126
+ });
127
+ }
128
+ /**
129
+ * A searchable list in a dialog, for "Search more…": every record the field may
130
+ * point to, narrowed as the person types, picked with a click or the keyboard.
131
+ * Resolves with the record picked, or null when closed.
132
+ */
133
+ function openSearchDialog(options) {
134
+ const doc = options.container?.ownerDocument ?? document;
135
+ const container = options.container ?? doc.body;
136
+ const opener = doc.activeElement instanceof doc.defaultView.HTMLElement ? doc.activeElement : null;
137
+ const labels = viewer_1.VIEWER_LABELS[options.locale ?? 'en'];
138
+ const words = widgets_1.WIDGET_LABELS[options.locale ?? 'en'];
139
+ const id = `fd-dialog-${++dialogs}`;
140
+ const make = (tag, attrs = {}, text) => {
141
+ const element = doc.createElement(tag);
142
+ for (const [name, value] of Object.entries(attrs))
143
+ element.setAttribute(name, value);
144
+ if (text !== undefined)
145
+ element.textContent = text;
146
+ return element;
147
+ };
148
+ const title = make('h2', { id: `${id}-title`, class: 'fd-form-dialog-title' }, options.title);
149
+ const closeButton = make('button', { type: 'button', class: 'fd-dialog-close', 'aria-label': labels.close }, '×');
150
+ const query = make('input', { type: 'search', class: 'fd-input', 'aria-label': words.search, placeholder: words.search, 'aria-controls': `${id}-list`, autocomplete: 'off' });
151
+ const list = make('ul', { id: `${id}-list`, class: 'fd-search-list', role: 'listbox', 'aria-labelledby': title.id });
152
+ const cancel = make('button', { type: 'button', class: 'fd-button' }, labels.cancel);
153
+ const box = make('div', {
154
+ class: `fd-theme fd-form-dialog fd-search-dialog fd-size-small`,
155
+ 'data-fd-skin': options.skin ?? 'underline',
156
+ role: 'dialog',
157
+ 'aria-modal': 'true',
158
+ 'aria-labelledby': title.id,
159
+ });
160
+ if (options.dir)
161
+ box.setAttribute('dir', options.dir);
162
+ const head = make('div', { class: 'fd-form-dialog-head' });
163
+ head.append(title, closeButton);
164
+ const body = make('div', { class: 'fd-form-dialog-body fd-search-body' });
165
+ body.append(query, list);
166
+ const foot = make('div', { class: 'fd-actions fd-actions-end fd-form-dialog-foot' });
167
+ foot.append(cancel);
168
+ box.append(head, body, foot);
169
+ const backdrop = make('div', { class: 'fd-dialog-backdrop fd-form-dialog-backdrop' });
170
+ backdrop.append(box);
171
+ container.append(backdrop);
172
+ query.focus();
173
+ return new Promise((resolve) => {
174
+ let found = [];
175
+ let active = -1;
176
+ let asked = 0;
177
+ let timer;
178
+ const close = (picked) => {
179
+ clearTimeout(timer);
180
+ asked++;
181
+ backdrop.remove();
182
+ opener?.focus();
183
+ resolve(picked);
184
+ };
185
+ const draw = () => {
186
+ list.replaceChildren(...found.map((record, i) => {
187
+ const option = make('li', { id: `${id}-option-${i}`, role: 'option', class: `fd-option${i === active ? ' fd-active' : ''}`, 'aria-selected': String(i === active) }, record.label);
188
+ option.addEventListener('click', () => close(record));
189
+ return option;
190
+ }));
191
+ if (!found.length)
192
+ list.append(make('li', { class: 'fd-empty' }, words.noResults));
193
+ if (active >= 0)
194
+ query.setAttribute('aria-activedescendant', `${id}-option-${active}`);
195
+ else
196
+ query.removeAttribute('aria-activedescendant');
197
+ };
198
+ const look = () => {
199
+ clearTimeout(timer);
200
+ const mine = ++asked;
201
+ timer = setTimeout(async () => {
202
+ const records = await options.search(query.value, 80).catch(() => []);
203
+ if (mine !== asked)
204
+ return; // a newer search, or the dialog closed
205
+ found = records;
206
+ active = -1;
207
+ draw();
208
+ }, 180);
209
+ };
210
+ query.addEventListener('input', look);
211
+ query.addEventListener('keydown', (event) => {
212
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
213
+ event.preventDefault();
214
+ if (!found.length)
215
+ return;
216
+ active = (active + (event.key === 'ArrowDown' ? 1 : -1) + found.length) % found.length;
217
+ draw();
218
+ list.children[active]?.scrollIntoView?.({ block: 'nearest' });
219
+ }
220
+ else if (event.key === 'Enter' && active >= 0) {
221
+ event.preventDefault();
222
+ close(found[active]);
223
+ }
224
+ });
225
+ cancel.addEventListener('click', () => close(null));
226
+ closeButton.addEventListener('click', () => close(null));
227
+ box.addEventListener('keydown', (event) => {
228
+ if (event.key === 'Escape') {
229
+ event.preventDefault();
230
+ close(null);
231
+ }
232
+ });
233
+ look();
234
+ });
235
+ }
236
+ //# sourceMappingURL=dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog.js","sourceRoot":"","sources":["../../../../../libs/viewer/src/lib/dialog.ts"],"names":[],"mappings":";;AAwCA,wCA2FC;AA0CD,4CAgGC;AA5QD,8CAAiD;AACjD,qCAAwG;AAkCxG,IAAI,OAAO,GAAG,CAAC,CAAC;AAEhB,MAAM,SAAS,GAAG,oGAAoG,CAAC;AAEvH,SAAgB,cAAc,CAAC,OAA0B;IACvD,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,EAAE,aAAa,IAAI,QAAQ,CAAC;IACzD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,GAAG,CAAC,IAAI,CAAC;IAChD,MAAM,MAAM,GAAG,GAAG,CAAC,aAAa,YAAY,GAAG,CAAC,WAAY,CAAC,WAAW,CAAC,CAAC,CAAE,GAAG,CAAC,aAA6B,CAAC,CAAC,CAAC,IAAI,CAAC;IACrH,MAAM,MAAM,GAAG,EAAE,GAAG,sBAAa,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAC/E,MAAM,EAAE,GAAG,aAAa,EAAE,OAAO,EAAE,CAAC;IACpC,MAAM,IAAI,GAAG,CAAwC,GAAM,EAAE,QAAgC,EAAE,EAAE,IAAa,EAAE,EAAE;QAChH,MAAM,OAAO,GAAG,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QACvC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACrF,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;QACnD,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,sBAAsB,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9F,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,CAAC,KAAK,EAAE,EAAE,GAAG,CAAC,CAAC;IAClH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACvF,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,6BAA6B,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IAC7G,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE;QACtB,4EAA4E;QAC5E,KAAK,EAAE,mCAAmC,OAAO,CAAC,IAAI,IAAI,QAAQ,EAAE;QACpE,cAAc,EAAE,OAAO,CAAC,IAAI,IAAI,WAAW;QAC3C,IAAI,EAAE,QAAQ;QACd,YAAY,EAAE,MAAM;QACpB,iBAAiB,EAAE,KAAK,CAAC,EAAE;QAC3B,QAAQ,EAAE,IAAI;KACf,CAAC,CAAC;IACH,IAAI,OAAO,CAAC,GAAG;QAAE,GAAG,CAAC,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACtD,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,+CAA+C,EAAE,CAAC,CAAC,CAAC;IACzI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IAClD,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACjD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,4CAA4C,EAAE,CAAC,CAAC;IACtF,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAE3B,MAAM,MAAM,GAAG,IAAA,oBAAW,EAAC,IAAI,EAAE,EAAE,GAAG,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;IACrE,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,IAAI,OAAO,CAAC,SAAS;QAAE,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAEpG,MAAM,UAAU,GAAG,GAAG,EAAE,CACtB,CAAC,GAAG,GAAG,CAAC,gBAAgB,CAAc,SAAS,CAAC,CAAC,CAAC,MAAM,CACtD,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,IAAI,CAAE,OAA6B,CAAC,QAAQ,CAC/F,CAAC;IACJ,2FAA2F;IAC3F,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,CAAc,wEAAwE,CAAC,CAAC;IACnI,CAAC,UAAU,EAAE,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;QACnC,IAAI,GAAG,CAAC,aAAa,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC;YAAE,UAAU,EAAE,EAAE,KAAK,EAAE,CAAC;IAC3F,CAAC,CAAC,CAAC;IAEH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,CAAC,KAAc,EAAE,EAAE;YAC/B,IAAI,IAAI;gBAAE,OAAO;YACjB,IAAI,GAAG,IAAI,CAAC;YACZ,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACrC,8EAA8E;YAC9E,MAAM,MAAM,GAAqB,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,MAAgB,EAAE,CAAC;YAC7G,MAAM,CAAC,OAAO,EAAE,CAAC;YACjB,QAAQ,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,EAAE,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC,CAAC;QACF,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;YAC7C,SAAS,CAAC,QAAQ,GAAG,IAAI,CAAC;YAC1B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAClF,SAAS,CAAC,QAAQ,GAAG,KAAK,CAAC;YAC3B,IAAI,QAAQ;gBAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACtD,WAAW,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D,GAAG,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;YACxC,yEAAyE;YACzE,IAAI,KAAK,CAAC,gBAAgB;gBAAE,OAAO;YACnC,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;gBAC3B,KAAK,CAAC,cAAc,EAAE,CAAC;gBACvB,KAAK,CAAC,KAAK,CAAC,CAAC;YACf,CAAC;iBAAM,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;gBAC/B,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;gBACzB,IAAI,CAAC,GAAG,CAAC,MAAM;oBAAE,OAAO;gBACxB,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;gBACrB,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBACjC,IAAI,KAAK,CAAC,QAAQ,IAAI,GAAG,CAAC,aAAa,KAAK,KAAK,EAAE,CAAC;oBAClD,KAAK,CAAC,cAAc,EAAE,CAAC;oBACvB,IAAI,CAAC,KAAK,EAAE,CAAC;gBACf,CAAC;qBAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,GAAG,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;oBACzD,KAAK,CAAC,cAAc,EAAE,CAAC;oBACvB,KAAK,CAAC,KAAK,EAAE,CAAC;gBAChB,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,sHAAsH;AACtH,SAAS,WAAW,CAAC,IAA0B,EAAE,MAA+B,EAAE,SAA8C,EAAE,MAAqB;IACrJ,IAAI,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC;IAClD,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,MAAgB,CAAC;QAChD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,OAAO,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO;QACpC,IAAI,GAAG,GAAG,CAAC;QACX,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CACpB,CAAC,IAAI,EAAE,EAAE;YACP,IAAI,MAAM,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,KAAK,GAAG;gBAAE,OAAO,CAAC,+BAA+B;YACvG,OAAO,GAAG,IAAI,CAAC;YACf,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBACjD,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC;oBAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC3H,CAAC;YACD,OAAO,GAAG,KAAK,CAAC;YAChB,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC;QAChD,CAAC,EACD,GAAG,EAAE,CAAC,SAAS,CAChB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAaD;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,OAA4B;IAC3D,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,EAAE,aAAa,IAAI,QAAQ,CAAC;IACzD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,GAAG,CAAC,IAAI,CAAC;IAChD,MAAM,MAAM,GAAG,GAAG,CAAC,aAAa,YAAY,GAAG,CAAC,WAAY,CAAC,WAAW,CAAC,CAAC,CAAE,GAAG,CAAC,aAA6B,CAAC,CAAC,CAAC,IAAI,CAAC;IACrH,MAAM,MAAM,GAAG,sBAAa,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC;IACrD,MAAM,KAAK,GAAG,uBAAa,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC;IACpD,MAAM,EAAE,GAAG,aAAa,EAAE,OAAO,EAAE,CAAC;IACpC,MAAM,IAAI,GAAG,CAAwC,GAAM,EAAE,QAAgC,EAAE,EAAE,IAAa,EAAE,EAAE;QAChH,MAAM,OAAO,GAAG,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QACvC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACrF,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;QACnD,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,sBAAsB,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9F,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,CAAC,KAAK,EAAE,EAAE,GAAG,CAAC,CAAC;IAClH,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9K,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,SAAS,EAAE,iBAAiB,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;IACrH,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACrF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE;QACtB,KAAK,EAAE,wDAAwD;QAC/D,cAAc,EAAE,OAAO,CAAC,IAAI,IAAI,WAAW;QAC3C,IAAI,EAAE,QAAQ;QACd,YAAY,EAAE,MAAM;QACpB,iBAAiB,EAAE,KAAK,CAAC,EAAE;KAC5B,CAAC,CAAC;IACH,IAAI,OAAO,CAAC,GAAG;QAAE,GAAG,CAAC,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAC,CAAC;IAC1E,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACzB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,+CAA+C,EAAE,CAAC,CAAC;IACrF,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACpB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,4CAA4C,EAAE,CAAC,CAAC;IACtF,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC;IAEd,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,KAAK,GAAoB,EAAE,CAAC;QAChC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC;QAChB,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,KAAgD,CAAC;QACrD,MAAM,KAAK,GAAG,CAAC,MAA4B,EAAE,EAAE;YAC7C,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,KAAK,EAAE,CAAC;YACR,QAAQ,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,EAAE,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC,CAAC;QACF,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,IAAI,CAAC,eAAe,CAClB,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;gBACzB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,WAAW,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,eAAe,EAAE,MAAM,CAAC,CAAC,KAAK,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;gBACnL,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;gBACtD,OAAO,MAAM,CAAC;YAChB,CAAC,CAAC,CACH,CAAC;YACF,IAAI,CAAC,KAAK,CAAC,MAAM;gBAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YACnF,IAAI,MAAM,IAAI,CAAC;gBAAE,KAAK,CAAC,YAAY,CAAC,uBAAuB,EAAE,GAAG,EAAE,WAAW,MAAM,EAAE,CAAC,CAAC;;gBAClF,KAAK,CAAC,eAAe,CAAC,uBAAuB,CAAC,CAAC;QACtD,CAAC,CAAC;QACF,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,IAAI,GAAG,EAAE,KAAK,CAAC;YACrB,KAAK,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;gBAC5B,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAqB,CAAC,CAAC;gBACzF,IAAI,IAAI,KAAK,KAAK;oBAAE,OAAO,CAAC,uCAAuC;gBACnE,KAAK,GAAG,OAAO,CAAC;gBAChB,MAAM,GAAG,CAAC,CAAC,CAAC;gBACZ,IAAI,EAAE,CAAC;YACT,CAAC,EAAE,GAAG,CAAC,CAAC;QACV,CAAC,CAAC;QACF,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACtC,KAAK,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;YAC1C,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;gBACzD,KAAK,CAAC,cAAc,EAAE,CAAC;gBACvB,IAAI,CAAC,KAAK,CAAC,MAAM;oBAAE,OAAO;gBAC1B,MAAM,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;gBACvF,IAAI,EAAE,CAAC;gBACP,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YAChE,CAAC;iBAAM,IAAI,KAAK,CAAC,GAAG,KAAK,OAAO,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;gBAChD,KAAK,CAAC,cAAc,EAAE,CAAC;gBACvB,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YACvB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACpD,WAAW,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACzD,GAAG,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;YACxC,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;gBAC3B,KAAK,CAAC,cAAc,EAAE,CAAC;gBACvB,KAAK,CAAC,IAAI,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;QACH,IAAI,EAAE,CAAC;IACT,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,232 @@
1
+ import { WIDGET_LABELS } from '@fieldia/widgets';
2
+ import { mountViewer, VIEWER_LABELS } from './viewer.mjs';
3
+ let dialogs = 0;
4
+ const FOCUSABLE = 'button, [href], input, select, textarea, [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
5
+ export function openFormDialog(options) {
6
+ const doc = options.container?.ownerDocument ?? document;
7
+ const container = options.container ?? doc.body;
8
+ const opener = doc.activeElement instanceof doc.defaultView.HTMLElement ? doc.activeElement : null;
9
+ const labels = { ...VIEWER_LABELS[options.locale ?? 'en'], ...options.labels };
10
+ const id = `fd-dialog-${++dialogs}`;
11
+ const make = (tag, attrs = {}, text) => {
12
+ const element = doc.createElement(tag);
13
+ for (const [name, value] of Object.entries(attrs))
14
+ element.setAttribute(name, value);
15
+ if (text !== undefined)
16
+ element.textContent = text;
17
+ return element;
18
+ };
19
+ const title = make('h2', { id: `${id}-title`, class: 'fd-form-dialog-title' }, options.title);
20
+ const closeButton = make('button', { type: 'button', class: 'fd-dialog-close', 'aria-label': labels.close }, '×');
21
+ const body = make('div', { class: 'fd-form-dialog-body' });
22
+ const discard = make('button', { type: 'button', class: 'fd-button' }, labels.discard);
23
+ const saveClose = make('button', { type: 'button', class: 'fd-button fd-button-primary' }, labels.saveClose);
24
+ const box = make('div', {
25
+ // Its own tokens and skin: the dialog sits outside the page that opened it.
26
+ class: `fd-theme fd-form-dialog fd-size-${options.size ?? 'medium'}`,
27
+ 'data-fd-skin': options.skin ?? 'underline',
28
+ role: 'dialog',
29
+ 'aria-modal': 'true',
30
+ 'aria-labelledby': title.id,
31
+ tabindex: '-1',
32
+ });
33
+ if (options.dir)
34
+ box.setAttribute('dir', options.dir);
35
+ box.append(make('div', { class: 'fd-form-dialog-head' }), body, make('div', { class: 'fd-actions fd-actions-end fd-form-dialog-foot' }));
36
+ box.firstElementChild?.append(title, closeButton);
37
+ box.lastElementChild?.append(discard, saveClose);
38
+ const backdrop = make('div', { class: 'fd-dialog-backdrop fd-form-dialog-backdrop' });
39
+ backdrop.append(box);
40
+ container.append(backdrop);
41
+ const handle = mountViewer(body, { ...options, showActions: false });
42
+ let done = false;
43
+ if (options.recompute)
44
+ recalculate(handle.form, options.page.fields, options.recompute, () => done);
45
+ const focusables = () => [...box.querySelectorAll(FOCUSABLE)].filter((element) => !element.closest('[hidden], [inert]') && !element.disabled);
46
+ // Into the dialog: its first field, once the record is in, or the dialog itself meanwhile.
47
+ const firstField = () => body.querySelector('input:not([type="hidden"]), select, textarea, [contenteditable="true"]');
48
+ (firstField() ?? box).focus();
49
+ void handle.form.settled().then(() => {
50
+ if (doc.activeElement === box || !box.contains(doc.activeElement))
51
+ firstField()?.focus();
52
+ });
53
+ return new Promise((resolve) => {
54
+ const close = (saved) => {
55
+ if (done)
56
+ return;
57
+ done = true;
58
+ const state = handle.form.getState();
59
+ // The form never changes its values in place, so these are safe to hand back.
60
+ const result = { saved, recordId: state.recordId ?? null, values: state.values };
61
+ handle.destroy();
62
+ backdrop.remove();
63
+ opener?.focus();
64
+ resolve(result);
65
+ };
66
+ saveClose.addEventListener('click', async () => {
67
+ saveClose.disabled = true;
68
+ const accepted = options.mode === 'values' ? handle.check() : await handle.save();
69
+ saveClose.disabled = false;
70
+ if (accepted)
71
+ close(true);
72
+ });
73
+ discard.addEventListener('click', () => close(false));
74
+ closeButton.addEventListener('click', () => close(false));
75
+ box.addEventListener('keydown', (event) => {
76
+ // A key a field used itself (Escape closing a list) is not the dialog's.
77
+ if (event.defaultPrevented)
78
+ return;
79
+ if (event.key === 'Escape') {
80
+ event.preventDefault();
81
+ close(false);
82
+ }
83
+ else if (event.key === 'Tab') {
84
+ const all = focusables();
85
+ if (!all.length)
86
+ return;
87
+ const first = all[0];
88
+ const last = all[all.length - 1];
89
+ if (event.shiftKey && doc.activeElement === first) {
90
+ event.preventDefault();
91
+ last.focus();
92
+ }
93
+ else if (!event.shiftKey && doc.activeElement === last) {
94
+ event.preventDefault();
95
+ first.focus();
96
+ }
97
+ }
98
+ });
99
+ });
100
+ }
101
+ /** Each change of the values asks for them recalculated, and shows the answer unless they changed again meanwhile. */
102
+ function recalculate(form, fields, recompute, closed) {
103
+ let seen = JSON.stringify(form.getState().values);
104
+ let writing = false;
105
+ form.subscribe(() => {
106
+ const values = form.getState().values;
107
+ const now = JSON.stringify(values);
108
+ if (writing || now === seen)
109
+ return;
110
+ seen = now;
111
+ recompute(values).then((next) => {
112
+ if (closed() || JSON.stringify(form.getState().values) !== now)
113
+ return; // a newer answer is on its way
114
+ writing = true;
115
+ for (const [name, value] of Object.entries(next)) {
116
+ if (name in fields && JSON.stringify(values[name] ?? null) !== JSON.stringify(value ?? null))
117
+ form.setValue(name, value);
118
+ }
119
+ writing = false;
120
+ seen = JSON.stringify(form.getState().values);
121
+ }, () => undefined);
122
+ });
123
+ }
124
+ /**
125
+ * A searchable list in a dialog, for "Search more…": every record the field may
126
+ * point to, narrowed as the person types, picked with a click or the keyboard.
127
+ * Resolves with the record picked, or null when closed.
128
+ */
129
+ export function openSearchDialog(options) {
130
+ const doc = options.container?.ownerDocument ?? document;
131
+ const container = options.container ?? doc.body;
132
+ const opener = doc.activeElement instanceof doc.defaultView.HTMLElement ? doc.activeElement : null;
133
+ const labels = VIEWER_LABELS[options.locale ?? 'en'];
134
+ const words = WIDGET_LABELS[options.locale ?? 'en'];
135
+ const id = `fd-dialog-${++dialogs}`;
136
+ const make = (tag, attrs = {}, text) => {
137
+ const element = doc.createElement(tag);
138
+ for (const [name, value] of Object.entries(attrs))
139
+ element.setAttribute(name, value);
140
+ if (text !== undefined)
141
+ element.textContent = text;
142
+ return element;
143
+ };
144
+ const title = make('h2', { id: `${id}-title`, class: 'fd-form-dialog-title' }, options.title);
145
+ const closeButton = make('button', { type: 'button', class: 'fd-dialog-close', 'aria-label': labels.close }, '×');
146
+ const query = make('input', { type: 'search', class: 'fd-input', 'aria-label': words.search, placeholder: words.search, 'aria-controls': `${id}-list`, autocomplete: 'off' });
147
+ const list = make('ul', { id: `${id}-list`, class: 'fd-search-list', role: 'listbox', 'aria-labelledby': title.id });
148
+ const cancel = make('button', { type: 'button', class: 'fd-button' }, labels.cancel);
149
+ const box = make('div', {
150
+ class: `fd-theme fd-form-dialog fd-search-dialog fd-size-small`,
151
+ 'data-fd-skin': options.skin ?? 'underline',
152
+ role: 'dialog',
153
+ 'aria-modal': 'true',
154
+ 'aria-labelledby': title.id,
155
+ });
156
+ if (options.dir)
157
+ box.setAttribute('dir', options.dir);
158
+ const head = make('div', { class: 'fd-form-dialog-head' });
159
+ head.append(title, closeButton);
160
+ const body = make('div', { class: 'fd-form-dialog-body fd-search-body' });
161
+ body.append(query, list);
162
+ const foot = make('div', { class: 'fd-actions fd-actions-end fd-form-dialog-foot' });
163
+ foot.append(cancel);
164
+ box.append(head, body, foot);
165
+ const backdrop = make('div', { class: 'fd-dialog-backdrop fd-form-dialog-backdrop' });
166
+ backdrop.append(box);
167
+ container.append(backdrop);
168
+ query.focus();
169
+ return new Promise((resolve) => {
170
+ let found = [];
171
+ let active = -1;
172
+ let asked = 0;
173
+ let timer;
174
+ const close = (picked) => {
175
+ clearTimeout(timer);
176
+ asked++;
177
+ backdrop.remove();
178
+ opener?.focus();
179
+ resolve(picked);
180
+ };
181
+ const draw = () => {
182
+ list.replaceChildren(...found.map((record, i) => {
183
+ const option = make('li', { id: `${id}-option-${i}`, role: 'option', class: `fd-option${i === active ? ' fd-active' : ''}`, 'aria-selected': String(i === active) }, record.label);
184
+ option.addEventListener('click', () => close(record));
185
+ return option;
186
+ }));
187
+ if (!found.length)
188
+ list.append(make('li', { class: 'fd-empty' }, words.noResults));
189
+ if (active >= 0)
190
+ query.setAttribute('aria-activedescendant', `${id}-option-${active}`);
191
+ else
192
+ query.removeAttribute('aria-activedescendant');
193
+ };
194
+ const look = () => {
195
+ clearTimeout(timer);
196
+ const mine = ++asked;
197
+ timer = setTimeout(async () => {
198
+ const records = await options.search(query.value, 80).catch(() => []);
199
+ if (mine !== asked)
200
+ return; // a newer search, or the dialog closed
201
+ found = records;
202
+ active = -1;
203
+ draw();
204
+ }, 180);
205
+ };
206
+ query.addEventListener('input', look);
207
+ query.addEventListener('keydown', (event) => {
208
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
209
+ event.preventDefault();
210
+ if (!found.length)
211
+ return;
212
+ active = (active + (event.key === 'ArrowDown' ? 1 : -1) + found.length) % found.length;
213
+ draw();
214
+ list.children[active]?.scrollIntoView?.({ block: 'nearest' });
215
+ }
216
+ else if (event.key === 'Enter' && active >= 0) {
217
+ event.preventDefault();
218
+ close(found[active]);
219
+ }
220
+ });
221
+ cancel.addEventListener('click', () => close(null));
222
+ closeButton.addEventListener('click', () => close(null));
223
+ box.addEventListener('keydown', (event) => {
224
+ if (event.key === 'Escape') {
225
+ event.preventDefault();
226
+ close(null);
227
+ }
228
+ });
229
+ look();
230
+ });
231
+ }
232
+ //# sourceMappingURL=dialog.mjs.map