@fieldia/viewer 0.4.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/bundle/fieldia.js +73 -33
- package/bundle/fieldia.js.map +4 -4
- package/package.json +3 -3
- package/src/index.d.mts +1 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/index.js.map +1 -1
- package/src/index.mjs +1 -0
- package/src/index.mjs.map +1 -1
- package/src/lib/dialog.d.mts +48 -0
- package/src/lib/dialog.d.ts +48 -0
- package/src/lib/dialog.js +236 -0
- package/src/lib/dialog.js.map +1 -0
- package/src/lib/dialog.mjs +232 -0
- package/src/lib/dialog.mjs.map +1 -0
- package/src/lib/viewer.d.mts +14 -0
- package/src/lib/viewer.d.ts +14 -0
- package/src/lib/viewer.js +110 -4
- package/src/lib/viewer.js.map +1 -1
- package/src/lib/viewer.mjs +110 -4
- package/src/lib/viewer.mjs.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fieldia/viewer",
|
|
3
|
-
"version": "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.
|
|
28
|
-
"@fieldia/widgets": "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
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
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
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":"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"}
|
|
@@ -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
|