@fieldia/viewer 0.10.0 → 0.12.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.
Files changed (110) hide show
  1. package/README.md +126 -1
  2. package/bundle/fieldia.ar.js +2 -2
  3. package/bundle/fieldia.ar.js.map +2 -2
  4. package/bundle/fieldia.de.js +2 -2
  5. package/bundle/fieldia.de.js.map +2 -2
  6. package/bundle/fieldia.fr.js +2 -2
  7. package/bundle/fieldia.fr.js.map +2 -2
  8. package/bundle/fieldia.js +6 -4
  9. package/bundle/fieldia.js.map +4 -4
  10. package/package.json +3 -3
  11. package/src/lib/attachment-preview.d.mts +27 -0
  12. package/src/lib/attachment-preview.d.ts +27 -0
  13. package/src/lib/attachment-preview.js +89 -0
  14. package/src/lib/attachment-preview.js.map +1 -0
  15. package/src/lib/attachment-preview.mjs +86 -0
  16. package/src/lib/attachment-preview.mjs.map +1 -0
  17. package/src/lib/dialog.d.mts +31 -2
  18. package/src/lib/dialog.d.ts +31 -2
  19. package/src/lib/dialog.js +158 -30
  20. package/src/lib/dialog.js.map +1 -1
  21. package/src/lib/dialog.mjs +155 -30
  22. package/src/lib/dialog.mjs.map +1 -1
  23. package/src/lib/hotkeys.d.mts +11 -0
  24. package/src/lib/hotkeys.d.ts +11 -0
  25. package/src/lib/hotkeys.js +34 -0
  26. package/src/lib/hotkeys.js.map +1 -0
  27. package/src/lib/hotkeys.mjs +30 -0
  28. package/src/lib/hotkeys.mjs.map +1 -0
  29. package/src/lib/labels.d.mts +39 -0
  30. package/src/lib/labels.d.ts +39 -0
  31. package/src/lib/labels.js +23 -0
  32. package/src/lib/labels.js.map +1 -1
  33. package/src/lib/labels.mjs +23 -0
  34. package/src/lib/labels.mjs.map +1 -1
  35. package/src/lib/list.d.mts +5 -2
  36. package/src/lib/list.d.ts +5 -2
  37. package/src/lib/list.js +6 -6
  38. package/src/lib/list.js.map +1 -1
  39. package/src/lib/list.mjs +6 -6
  40. package/src/lib/list.mjs.map +1 -1
  41. package/src/lib/locales/ar.js +23 -0
  42. package/src/lib/locales/ar.js.map +1 -1
  43. package/src/lib/locales/ar.mjs +23 -0
  44. package/src/lib/locales/ar.mjs.map +1 -1
  45. package/src/lib/locales/de.js +23 -0
  46. package/src/lib/locales/de.js.map +1 -1
  47. package/src/lib/locales/de.mjs +23 -0
  48. package/src/lib/locales/de.mjs.map +1 -1
  49. package/src/lib/locales/fr.js +23 -0
  50. package/src/lib/locales/fr.js.map +1 -1
  51. package/src/lib/locales/fr.mjs +23 -0
  52. package/src/lib/locales/fr.mjs.map +1 -1
  53. package/src/lib/look.d.mts +7 -3
  54. package/src/lib/look.d.ts +7 -3
  55. package/src/lib/look.js +72 -15
  56. package/src/lib/look.js.map +1 -1
  57. package/src/lib/look.mjs +72 -15
  58. package/src/lib/look.mjs.map +1 -1
  59. package/src/lib/open.d.mts +30 -0
  60. package/src/lib/open.d.ts +30 -0
  61. package/src/lib/open.js +92 -0
  62. package/src/lib/open.js.map +1 -0
  63. package/src/lib/open.mjs +89 -0
  64. package/src/lib/open.mjs.map +1 -0
  65. package/src/lib/place.d.mts +2 -0
  66. package/src/lib/place.d.ts +2 -0
  67. package/src/lib/place.js.map +1 -1
  68. package/src/lib/place.mjs.map +1 -1
  69. package/src/lib/record-bar.d.mts +56 -0
  70. package/src/lib/record-bar.d.ts +56 -0
  71. package/src/lib/record-bar.js +237 -0
  72. package/src/lib/record-bar.js.map +1 -0
  73. package/src/lib/record-bar.mjs +234 -0
  74. package/src/lib/record-bar.mjs.map +1 -0
  75. package/src/lib/record-styles.d.mts +9 -0
  76. package/src/lib/record-styles.d.ts +9 -0
  77. package/src/lib/record-styles.js +83 -0
  78. package/src/lib/record-styles.js.map +1 -0
  79. package/src/lib/record-styles.mjs +79 -0
  80. package/src/lib/record-styles.mjs.map +1 -0
  81. package/src/lib/related.d.mts +26 -3
  82. package/src/lib/related.d.ts +26 -3
  83. package/src/lib/related.js +37 -10
  84. package/src/lib/related.js.map +1 -1
  85. package/src/lib/related.mjs +36 -10
  86. package/src/lib/related.mjs.map +1 -1
  87. package/src/lib/say.d.mts +11 -0
  88. package/src/lib/say.d.ts +11 -0
  89. package/src/lib/say.js +61 -0
  90. package/src/lib/say.js.map +1 -0
  91. package/src/lib/say.mjs +58 -0
  92. package/src/lib/say.mjs.map +1 -0
  93. package/src/lib/tab-strip.d.mts +17 -0
  94. package/src/lib/tab-strip.d.ts +17 -0
  95. package/src/lib/tab-strip.js +51 -0
  96. package/src/lib/tab-strip.js.map +1 -0
  97. package/src/lib/tab-strip.mjs +48 -0
  98. package/src/lib/tab-strip.mjs.map +1 -0
  99. package/src/lib/value-words.d.mts +10 -0
  100. package/src/lib/value-words.d.ts +10 -0
  101. package/src/lib/value-words.js +38 -0
  102. package/src/lib/value-words.js.map +1 -0
  103. package/src/lib/value-words.mjs +35 -0
  104. package/src/lib/value-words.mjs.map +1 -0
  105. package/src/lib/viewer.d.mts +54 -5
  106. package/src/lib/viewer.d.ts +54 -5
  107. package/src/lib/viewer.js +924 -297
  108. package/src/lib/viewer.js.map +1 -1
  109. package/src/lib/viewer.mjs +926 -299
  110. package/src/lib/viewer.mjs.map +1 -1
@@ -0,0 +1,58 @@
1
+ /** How long words stay before they go by themselves: a warning or a danger stays longer. */
2
+ const STAY_MS = 6000;
3
+ const STAY_LONG_MS = 10000;
4
+ /** The most toasts on screen at once: the oldest give way. */
5
+ const MOST = 4;
6
+ /**
7
+ * Words a step says, as toasts at the foot of the screen: each in its tone,
8
+ * one over another, in a polite live region inside the form — so they wear its
9
+ * skin, scheme and direction. Each goes by itself after a moment, but not
10
+ * while the pointer or the focus is on it, and at once with its ×. They show
11
+ * over a dialog or panel too, and while a modal one is open, a screen reader —
12
+ * which hears only inside it — is told them there as well, unseen.
13
+ */
14
+ export function sayer(root, el, dismiss) {
15
+ // There before anything is said, so a screen reader hears what comes into it.
16
+ const region = el('div', { class: 'fd-says', role: 'status', 'aria-live': 'polite' });
17
+ root.append(region);
18
+ return (message, tone) => {
19
+ const close = el('button', { type: 'button', class: 'fd-alert-close', 'aria-label': dismiss }, '×');
20
+ const toast = el('div', { class: 'fd-say', 'data-tone': tone }, el('span', {}, message), close);
21
+ // Its edge in its tone's colour, from the scheme's own: a danger is the error colour.
22
+ toast.style.setProperty('--fd-tone', `var(--fd-${tone === 'danger' ? 'error' : tone})`);
23
+ let timer;
24
+ const hold = () => clearTimeout(timer);
25
+ const go = () => {
26
+ hold();
27
+ toast.remove();
28
+ };
29
+ const wait = () => {
30
+ hold();
31
+ timer = setTimeout(go, tone === 'warning' || tone === 'danger' ? STAY_LONG_MS : STAY_MS);
32
+ };
33
+ close.addEventListener('click', go);
34
+ toast.addEventListener('mouseenter', hold);
35
+ toast.addEventListener('focusin', hold);
36
+ toast.addEventListener('mouseleave', wait);
37
+ toast.addEventListener('focusout', wait);
38
+ region.append(toast);
39
+ while (region.children.length > MOST)
40
+ region.firstElementChild?.remove();
41
+ echo(region, el, message);
42
+ wait();
43
+ };
44
+ }
45
+ /** The words again, unseen, in a live region of the topmost modal dialog the region is not in. */
46
+ function echo(region, el, message) {
47
+ const modals = [...region.ownerDocument.querySelectorAll('[aria-modal="true"]')].filter((m) => !m.contains(region));
48
+ const top = modals[modals.length - 1];
49
+ if (!top)
50
+ return;
51
+ let heard = top.querySelector(':scope > .fd-says-echo');
52
+ if (!heard) {
53
+ heard = el('div', { class: 'fd-says-echo fd-sr-only', role: 'status', 'aria-live': 'polite' });
54
+ top.append(heard);
55
+ }
56
+ heard.textContent = message;
57
+ }
58
+ //# sourceMappingURL=say.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"say.mjs","sourceRoot":"","sources":["../../../../../libs/viewer/src/lib/say.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { El } from './dom.mjs';
2
+ export interface TabStrip {
3
+ /** The tabs' list with a button at each edge: what goes on the page. */
4
+ element: HTMLElement;
5
+ /** Look again at what is past each edge: after the tabs shown change, or the strip's width. */
6
+ measure(): void;
7
+ /** Scroll the strip, and only the strip, so this tab is in sight. */
8
+ reveal(tab: HTMLElement): void;
9
+ destroy(): void;
10
+ }
11
+ /**
12
+ * A row of tabs too long for its room scrolls sideways: past an edge with more
13
+ * beyond it, the tabs fade out under a button that moves the strip a width
14
+ * along — to the left at the start of a right-to-left page. The buttons are
15
+ * for a pointer: the keyboard moves along the tabs, which come into sight.
16
+ */
17
+ export declare function tabStrip(list: HTMLElement, el: El): TabStrip;
@@ -0,0 +1,17 @@
1
+ import type { El } from './dom';
2
+ export interface TabStrip {
3
+ /** The tabs' list with a button at each edge: what goes on the page. */
4
+ element: HTMLElement;
5
+ /** Look again at what is past each edge: after the tabs shown change, or the strip's width. */
6
+ measure(): void;
7
+ /** Scroll the strip, and only the strip, so this tab is in sight. */
8
+ reveal(tab: HTMLElement): void;
9
+ destroy(): void;
10
+ }
11
+ /**
12
+ * A row of tabs too long for its room scrolls sideways: past an edge with more
13
+ * beyond it, the tabs fade out under a button that moves the strip a width
14
+ * along — to the left at the start of a right-to-left page. The buttons are
15
+ * for a pointer: the keyboard moves along the tabs, which come into sight.
16
+ */
17
+ export declare function tabStrip(list: HTMLElement, el: El): TabStrip;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tabStrip = tabStrip;
4
+ const dom_1 = require("./dom");
5
+ /** Room kept beside a tab brought into sight: the button over that edge and the fade beside it. */
6
+ const EDGE = 64;
7
+ /**
8
+ * A row of tabs too long for its room scrolls sideways: past an edge with more
9
+ * beyond it, the tabs fade out under a button that moves the strip a width
10
+ * along — to the left at the start of a right-to-left page. The buttons are
11
+ * for a pointer: the keyboard moves along the tabs, which come into sight.
12
+ */
13
+ function tabStrip(list, el) {
14
+ const rtl = () => list.closest('[dir]')?.getAttribute('dir') === 'rtl';
15
+ const edge = (side) => el('button', { type: 'button', class: `fd-tabs-scroll fd-tabs-${side}`, tabindex: '-1', 'aria-hidden': 'true', hidden: '' });
16
+ const before = edge('before');
17
+ const after = edge('after');
18
+ const element = el('div', { class: 'fd-tabbar' }, before, list, after);
19
+ // A strip's width along, less a tab's room, toward the button's own edge.
20
+ const move = (toward) => list.scrollBy({ left: toward * (rtl() ? -1 : 1) * Math.max(list.clientWidth - 2 * EDGE, EDGE), behavior: 'smooth' });
21
+ before.addEventListener('click', () => move(-1));
22
+ after.addEventListener('click', () => move(1));
23
+ function measure() {
24
+ // How far along from the start: right to left, scrollLeft runs from 0 down.
25
+ const along = Math.abs(list.scrollLeft);
26
+ const start = along > 1;
27
+ const end = along + list.clientWidth < list.scrollWidth - 1;
28
+ (0, dom_1.setHidden)(before, !start);
29
+ (0, dom_1.setHidden)(after, !end);
30
+ const more = [start && 'start', end && 'end'].filter(Boolean).join(' ');
31
+ (0, dom_1.setAttr)(element, 'data-more', more || null);
32
+ }
33
+ function reveal(tab) {
34
+ if (!list.clientWidth || list.scrollWidth <= list.clientWidth)
35
+ return;
36
+ const room = list.getBoundingClientRect();
37
+ const at = tab.getBoundingClientRect();
38
+ // Either way round, a larger scrollLeft shows what is further right.
39
+ if (at.left < room.left + EDGE)
40
+ list.scrollLeft += at.left - room.left - EDGE;
41
+ else if (at.right > room.right - EDGE)
42
+ list.scrollLeft += at.right - room.right + EDGE;
43
+ measure();
44
+ }
45
+ list.addEventListener('scroll', measure, { passive: true });
46
+ const view = list.ownerDocument.defaultView;
47
+ const watcher = view && 'ResizeObserver' in view ? new view.ResizeObserver(() => measure()) : null;
48
+ watcher?.observe(list);
49
+ return { element, measure, reveal, destroy: () => watcher?.disconnect() };
50
+ }
51
+ //# sourceMappingURL=tab-strip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-strip.js","sourceRoot":"","sources":["../../../../../libs/viewer/src/lib/tab-strip.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { setAttr, setHidden } from './dom.mjs';
2
+ /** Room kept beside a tab brought into sight: the button over that edge and the fade beside it. */
3
+ const EDGE = 64;
4
+ /**
5
+ * A row of tabs too long for its room scrolls sideways: past an edge with more
6
+ * beyond it, the tabs fade out under a button that moves the strip a width
7
+ * along — to the left at the start of a right-to-left page. The buttons are
8
+ * for a pointer: the keyboard moves along the tabs, which come into sight.
9
+ */
10
+ export function tabStrip(list, el) {
11
+ const rtl = () => list.closest('[dir]')?.getAttribute('dir') === 'rtl';
12
+ const edge = (side) => el('button', { type: 'button', class: `fd-tabs-scroll fd-tabs-${side}`, tabindex: '-1', 'aria-hidden': 'true', hidden: '' });
13
+ const before = edge('before');
14
+ const after = edge('after');
15
+ const element = el('div', { class: 'fd-tabbar' }, before, list, after);
16
+ // A strip's width along, less a tab's room, toward the button's own edge.
17
+ const move = (toward) => list.scrollBy({ left: toward * (rtl() ? -1 : 1) * Math.max(list.clientWidth - 2 * EDGE, EDGE), behavior: 'smooth' });
18
+ before.addEventListener('click', () => move(-1));
19
+ after.addEventListener('click', () => move(1));
20
+ function measure() {
21
+ // How far along from the start: right to left, scrollLeft runs from 0 down.
22
+ const along = Math.abs(list.scrollLeft);
23
+ const start = along > 1;
24
+ const end = along + list.clientWidth < list.scrollWidth - 1;
25
+ setHidden(before, !start);
26
+ setHidden(after, !end);
27
+ const more = [start && 'start', end && 'end'].filter(Boolean).join(' ');
28
+ setAttr(element, 'data-more', more || null);
29
+ }
30
+ function reveal(tab) {
31
+ if (!list.clientWidth || list.scrollWidth <= list.clientWidth)
32
+ return;
33
+ const room = list.getBoundingClientRect();
34
+ const at = tab.getBoundingClientRect();
35
+ // Either way round, a larger scrollLeft shows what is further right.
36
+ if (at.left < room.left + EDGE)
37
+ list.scrollLeft += at.left - room.left - EDGE;
38
+ else if (at.right > room.right - EDGE)
39
+ list.scrollLeft += at.right - room.right + EDGE;
40
+ measure();
41
+ }
42
+ list.addEventListener('scroll', measure, { passive: true });
43
+ const view = list.ownerDocument.defaultView;
44
+ const watcher = view && 'ResizeObserver' in view ? new view.ResizeObserver(() => measure()) : null;
45
+ watcher?.observe(list);
46
+ return { element, measure, reveal, destroy: () => watcher?.disconnect() };
47
+ }
48
+ //# sourceMappingURL=tab-strip.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-strip.mjs","sourceRoot":"","sources":["../../../../../libs/viewer/src/lib/tab-strip.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { type Fields, type Locale, type Values } from '@fieldia/core';
2
+ /**
3
+ * Words that hold fields' values — "Entries before {lock_date} cannot be
4
+ * posted." — drawn as text with each value in a span of its own, written as
5
+ * the field shows it, and brought up to date as the record changes.
6
+ */
7
+ export declare function valueWords(doc: Document, fields: Fields, text: string, locale: Locale): {
8
+ nodes: (Node | string)[];
9
+ update(values: Readonly<Values>): void;
10
+ };
@@ -0,0 +1,10 @@
1
+ import { type Fields, type Locale, type Values } from '@fieldia/core';
2
+ /**
3
+ * Words that hold fields' values — "Entries before {lock_date} cannot be
4
+ * posted." — drawn as text with each value in a span of its own, written as
5
+ * the field shows it, and brought up to date as the record changes.
6
+ */
7
+ export declare function valueWords(doc: Document, fields: Fields, text: string, locale: Locale): {
8
+ nodes: (Node | string)[];
9
+ update(values: Readonly<Values>): void;
10
+ };
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.valueWords = valueWords;
4
+ const core_1 = require("@fieldia/core");
5
+ const widgets_1 = require("@fieldia/widgets");
6
+ /**
7
+ * Words that hold fields' values — "Entries before {lock_date} cannot be
8
+ * posted." — drawn as text with each value in a span of its own, written as
9
+ * the field shows it, and brought up to date as the record changes.
10
+ */
11
+ function valueWords(doc, fields, text, locale) {
12
+ const parts = (0, core_1.splitValues)(text);
13
+ if (!parts.some((part) => part.field))
14
+ return { nodes: [text], update: () => undefined };
15
+ const values = [];
16
+ const nodes = parts.map((part) => {
17
+ if (!part.field)
18
+ return part.text;
19
+ // Set apart from the words round it, so a date or an amount keeps its own order on a page read right to left.
20
+ const span = doc.createElement('bdi');
21
+ span.className = 'fd-value';
22
+ span.dataset['field'] = part.field;
23
+ values.push({ field: part.field, span });
24
+ return span;
25
+ });
26
+ return {
27
+ nodes,
28
+ update(record) {
29
+ for (const { field, span } of values) {
30
+ const def = fields[field];
31
+ const words = def ? (0, widgets_1.displayValue)(def, record[field], record, locale) : '';
32
+ if (span.textContent !== words)
33
+ span.textContent = words;
34
+ }
35
+ },
36
+ };
37
+ }
38
+ //# sourceMappingURL=value-words.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value-words.js","sourceRoot":"","sources":["../../../../../libs/viewer/src/lib/value-words.ts"],"names":[],"mappings":";;AAQA,gCAuBC;AA/BD,wCAA+G;AAC/G,8CAAgD;AAEhD;;;;GAIG;AACH,SAAgB,UAAU,CAAC,GAAa,EAAE,MAAc,EAAE,IAAY,EAAE,MAAc;IACpF,MAAM,KAAK,GAAG,IAAA,kBAAW,EAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;IACzF,MAAM,MAAM,GAA2C,EAAE,CAAC;IAC1D,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QAC/B,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC;QAClC,8GAA8G;QAC9G,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC;QAC5B,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IACH,OAAO;QACL,KAAK;QACL,MAAM,CAAC,MAAM;YACX,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,CAAC;gBACrC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,IAAA,sBAAY,EAAC,GAAgB,EAAE,MAAM,CAAC,KAAK,CAAsB,EAAE,MAA+B,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrI,IAAI,IAAI,CAAC,WAAW,KAAK,KAAK;oBAAE,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;YAC3D,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { splitValues } from '@fieldia/core';
2
+ import { displayValue } from '@fieldia/widgets';
3
+ /**
4
+ * Words that hold fields' values — "Entries before {lock_date} cannot be
5
+ * posted." — drawn as text with each value in a span of its own, written as
6
+ * the field shows it, and brought up to date as the record changes.
7
+ */
8
+ export function valueWords(doc, fields, text, locale) {
9
+ const parts = splitValues(text);
10
+ if (!parts.some((part) => part.field))
11
+ return { nodes: [text], update: () => undefined };
12
+ const values = [];
13
+ const nodes = parts.map((part) => {
14
+ if (!part.field)
15
+ return part.text;
16
+ // Set apart from the words round it, so a date or an amount keeps its own order on a page read right to left.
17
+ const span = doc.createElement('bdi');
18
+ span.className = 'fd-value';
19
+ span.dataset['field'] = part.field;
20
+ values.push({ field: part.field, span });
21
+ return span;
22
+ });
23
+ return {
24
+ nodes,
25
+ update(record) {
26
+ for (const { field, span } of values) {
27
+ const def = fields[field];
28
+ const words = def ? displayValue(def, record[field], record, locale) : '';
29
+ if (span.textContent !== words)
30
+ span.textContent = words;
31
+ }
32
+ },
33
+ };
34
+ }
35
+ //# sourceMappingURL=value-words.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value-words.mjs","sourceRoot":"","sources":["../../../../../libs/viewer/src/lib/value-words.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAqE,MAAM,eAAe,CAAC;AAC/G,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,GAAa,EAAE,MAAc,EAAE,IAAY,EAAE,MAAc;IACpF,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;IACzF,MAAM,MAAM,GAA2C,EAAE,CAAC;IAC1D,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QAC/B,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC;QAClC,8GAA8G;QAC9G,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC;QAC5B,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IACH,OAAO;QACL,KAAK;QACL,MAAM,CAAC,MAAM;YACX,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,CAAC;gBACrC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,GAAgB,EAAE,MAAM,CAAC,KAAK,CAAsB,EAAE,MAA+B,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrI,IAAI,IAAI,CAAC,WAAW,KAAK,KAAK;oBAAE,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;YAC3D,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -1,14 +1,18 @@
1
- import { type Form, type FormOptions, type Page, type RecordId, type Locale } from '@fieldia/core';
1
+ import { type ActionHost, type ActionStep, type FormEvents, type Form, type FormOptions, type Page, type RecordId, type ResolvedFilter, type Locale, type OpenRequest, type OpenResult, type RunContext, type RunResult, type Values } from '@fieldia/core';
2
2
  import { type IconSet, type PreferenceStore, type WidgetFactory } from '@fieldia/widgets';
3
+ import { type PageFinder } from './related.mjs';
4
+ import { type Breadcrumb, type RecordPager } from './record-bar.mjs';
5
+ export type { Breadcrumb, RecordPager } from './record-bar.mjs';
3
6
  export type Skin = 'underline' | 'outlined';
4
7
  import { type ViewerLabels } from './labels.mjs';
5
8
  export { VIEWER_LABELS, DEFAULT_LABELS, type ViewerLabels } from './labels.mjs';
9
+ export type { PageFinder, PageRequest } from './related.mjs';
6
10
  /** Fill a slot with the app's own content. Return a function to clean up. */
7
11
  export type SlotRenderer = (element: HTMLElement, context: {
8
12
  form: Form;
9
13
  name: string;
10
14
  }) => void | (() => void);
11
- export interface ViewerOptions extends Omit<FormOptions, 'page'> {
15
+ export interface ViewerOptions extends Omit<FormOptions, 'page' | 'host'> {
12
16
  page: Page;
13
17
  /** A form made elsewhere, to share one record between two views. */
14
18
  form?: Form;
@@ -29,16 +33,24 @@ export interface ViewerOptions extends Omit<FormOptions, 'page'> {
29
33
  /** Labels that win over the language's defaults. */
30
34
  labels?: Partial<ViewerLabels>;
31
35
  dir?: 'ltr' | 'rtl';
32
- /** How to ask before a button with `confirm` runs. Defaults to a small dialog. */
36
+ /** How a question is asked: a button's `confirm`, and an `ask` step. Defaults to the viewer's own small box. */
33
37
  confirm?: (message: string) => Promise<boolean>;
34
38
  /** Where a person's choices about the page's look are kept, such as a table's columns. The browser's storage by default. */
35
39
  preferences?: PreferenceStore;
36
40
  /** False when something around the page saves it, such as a dialog: the page's own Save, Discard and Submit stay hidden. */
37
41
  showActions?: boolean;
38
42
  /**
39
- * Pages for the models links point to, so a link can show its record in a
40
- * dialog: Create and edit…, and the button that opens the linked record.
43
+ * The app's pages, for the parts that show another page. A saved form placed
44
+ * in this one (a `form` part) is drawn from the page asked for by its id —
45
+ * `{ id, version }` when it keeps to a version; and a link shows its record
46
+ * in a dialog (Create and edit…, and the button that opens the linked
47
+ * record) with the page asked for by the model it points to, `{ model }`.
48
+ * Either a record of pages by key — a model, a page's id, or `id@version`
49
+ * for a version kept to — or a function asked for each, which may answer
50
+ * with a promise: a saved form shows a quiet placeholder until it does.
41
51
  */
52
+ pages?: PageFinder;
53
+ /** Pages by the models links point to: what `pages` gives by model, as earlier versions had it. Still read. */
42
54
  relatedPages?: Record<string, Page> | ((model: string) => Page | null | undefined);
43
55
  /**
44
56
  * The app's own icons, or replacements for Fieldia's, by the name a page
@@ -69,6 +81,37 @@ export interface ViewerOptions extends Omit<FormOptions, 'page'> {
69
81
  translate?: (text: string) => string;
70
82
  /** A list page: a row was opened, by a click or by Enter. The app shows the record. */
71
83
  onOpenRecord?: (id: RecordId) => void;
84
+ /** A list page: only the records these conditions let through, whatever is searched — the invoices of one order, opened from it. */
85
+ listFilter?: ResolvedFilter[];
86
+ /**
87
+ * What the page's steps ask of the screen, the viewer does itself: a page
88
+ * opened (in a dialog, a side panel, or this form's place, with Back), words
89
+ * said (a toast), a question asked (its own box, or `confirm`), a tab shown.
90
+ * Give any of these to do it your own way; the rest stay the viewer's.
91
+ */
92
+ host?: Partial<ActionHost>;
93
+ /**
94
+ * A page a step opens: answer with how it ended to open it your own way —
95
+ * your router, your own dialog — or with undefined to let the viewer open it.
96
+ */
97
+ onOpen?: (request: OpenRequest) => Promise<OpenResult> | undefined;
98
+ /**
99
+ * The records round this one, for the pager over a record — "3 / 42",
100
+ * Previous and Next — as the list it was opened from has them: their ids in
101
+ * order, or how many there are, where this one is, and the id at a place.
102
+ * Moving saves what changed first, as Flectra does; the form then shows
103
+ * the other record (its `open` event says which). A copy joins them after
104
+ * its record; a record deleted leaves them, the pager moving to the next.
105
+ * Never in a dialog.
106
+ */
107
+ records?: readonly RecordId[] | RecordPager;
108
+ /**
109
+ * The trail to this record, for the breadcrumbs over it: each page before
110
+ * it with its words and what a press does (back to it); the record itself
111
+ * comes last, by its name. A page a step opens in this one's place adds
112
+ * this one to the trail, as the way back. Never in a dialog.
113
+ */
114
+ breadcrumbs?: readonly Breadcrumb[];
72
115
  }
73
116
  export interface ViewerHandle {
74
117
  readonly form: Form;
@@ -81,6 +124,12 @@ export interface ViewerHandle {
81
124
  /** Lock or unlock the whole form. */
82
125
  setReadonly(readonly: boolean): void;
83
126
  isReadonly(): boolean;
127
+ /** Listen to one of the form's events — change, save, send, action, run, step, open; returns what stops listening. */
128
+ on<E extends keyof FormEvents>(event: E, listener: (event: FormEvents[E]) => void): () => void;
129
+ /** Write several fields at once, as the app: the page's change steps do not run for it. */
130
+ setValues(values: Values): void;
131
+ /** Run steps on the form, as a button would. */
132
+ run(steps: readonly ActionStep[], context?: RunContext): Promise<RunResult>;
84
133
  destroy(): void;
85
134
  }
86
135
  /**
@@ -1,14 +1,18 @@
1
- import { type Form, type FormOptions, type Page, type RecordId, type Locale } from '@fieldia/core';
1
+ import { type ActionHost, type ActionStep, type FormEvents, type Form, type FormOptions, type Page, type RecordId, type ResolvedFilter, type Locale, type OpenRequest, type OpenResult, type RunContext, type RunResult, type Values } from '@fieldia/core';
2
2
  import { type IconSet, type PreferenceStore, type WidgetFactory } from '@fieldia/widgets';
3
+ import { type PageFinder } from './related';
4
+ import { type Breadcrumb, type RecordPager } from './record-bar';
5
+ export type { Breadcrumb, RecordPager } from './record-bar';
3
6
  export type Skin = 'underline' | 'outlined';
4
7
  import { type ViewerLabels } from './labels';
5
8
  export { VIEWER_LABELS, DEFAULT_LABELS, type ViewerLabels } from './labels';
9
+ export type { PageFinder, PageRequest } from './related';
6
10
  /** Fill a slot with the app's own content. Return a function to clean up. */
7
11
  export type SlotRenderer = (element: HTMLElement, context: {
8
12
  form: Form;
9
13
  name: string;
10
14
  }) => void | (() => void);
11
- export interface ViewerOptions extends Omit<FormOptions, 'page'> {
15
+ export interface ViewerOptions extends Omit<FormOptions, 'page' | 'host'> {
12
16
  page: Page;
13
17
  /** A form made elsewhere, to share one record between two views. */
14
18
  form?: Form;
@@ -29,16 +33,24 @@ export interface ViewerOptions extends Omit<FormOptions, 'page'> {
29
33
  /** Labels that win over the language's defaults. */
30
34
  labels?: Partial<ViewerLabels>;
31
35
  dir?: 'ltr' | 'rtl';
32
- /** How to ask before a button with `confirm` runs. Defaults to a small dialog. */
36
+ /** How a question is asked: a button's `confirm`, and an `ask` step. Defaults to the viewer's own small box. */
33
37
  confirm?: (message: string) => Promise<boolean>;
34
38
  /** Where a person's choices about the page's look are kept, such as a table's columns. The browser's storage by default. */
35
39
  preferences?: PreferenceStore;
36
40
  /** False when something around the page saves it, such as a dialog: the page's own Save, Discard and Submit stay hidden. */
37
41
  showActions?: boolean;
38
42
  /**
39
- * Pages for the models links point to, so a link can show its record in a
40
- * dialog: Create and edit…, and the button that opens the linked record.
43
+ * The app's pages, for the parts that show another page. A saved form placed
44
+ * in this one (a `form` part) is drawn from the page asked for by its id —
45
+ * `{ id, version }` when it keeps to a version; and a link shows its record
46
+ * in a dialog (Create and edit…, and the button that opens the linked
47
+ * record) with the page asked for by the model it points to, `{ model }`.
48
+ * Either a record of pages by key — a model, a page's id, or `id@version`
49
+ * for a version kept to — or a function asked for each, which may answer
50
+ * with a promise: a saved form shows a quiet placeholder until it does.
41
51
  */
52
+ pages?: PageFinder;
53
+ /** Pages by the models links point to: what `pages` gives by model, as earlier versions had it. Still read. */
42
54
  relatedPages?: Record<string, Page> | ((model: string) => Page | null | undefined);
43
55
  /**
44
56
  * The app's own icons, or replacements for Fieldia's, by the name a page
@@ -69,6 +81,37 @@ export interface ViewerOptions extends Omit<FormOptions, 'page'> {
69
81
  translate?: (text: string) => string;
70
82
  /** A list page: a row was opened, by a click or by Enter. The app shows the record. */
71
83
  onOpenRecord?: (id: RecordId) => void;
84
+ /** A list page: only the records these conditions let through, whatever is searched — the invoices of one order, opened from it. */
85
+ listFilter?: ResolvedFilter[];
86
+ /**
87
+ * What the page's steps ask of the screen, the viewer does itself: a page
88
+ * opened (in a dialog, a side panel, or this form's place, with Back), words
89
+ * said (a toast), a question asked (its own box, or `confirm`), a tab shown.
90
+ * Give any of these to do it your own way; the rest stay the viewer's.
91
+ */
92
+ host?: Partial<ActionHost>;
93
+ /**
94
+ * A page a step opens: answer with how it ended to open it your own way —
95
+ * your router, your own dialog — or with undefined to let the viewer open it.
96
+ */
97
+ onOpen?: (request: OpenRequest) => Promise<OpenResult> | undefined;
98
+ /**
99
+ * The records round this one, for the pager over a record — "3 / 42",
100
+ * Previous and Next — as the list it was opened from has them: their ids in
101
+ * order, or how many there are, where this one is, and the id at a place.
102
+ * Moving saves what changed first, as Flectra does; the form then shows
103
+ * the other record (its `open` event says which). A copy joins them after
104
+ * its record; a record deleted leaves them, the pager moving to the next.
105
+ * Never in a dialog.
106
+ */
107
+ records?: readonly RecordId[] | RecordPager;
108
+ /**
109
+ * The trail to this record, for the breadcrumbs over it: each page before
110
+ * it with its words and what a press does (back to it); the record itself
111
+ * comes last, by its name. A page a step opens in this one's place adds
112
+ * this one to the trail, as the way back. Never in a dialog.
113
+ */
114
+ breadcrumbs?: readonly Breadcrumb[];
72
115
  }
73
116
  export interface ViewerHandle {
74
117
  readonly form: Form;
@@ -81,6 +124,12 @@ export interface ViewerHandle {
81
124
  /** Lock or unlock the whole form. */
82
125
  setReadonly(readonly: boolean): void;
83
126
  isReadonly(): boolean;
127
+ /** Listen to one of the form's events — change, save, send, action, run, step, open; returns what stops listening. */
128
+ on<E extends keyof FormEvents>(event: E, listener: (event: FormEvents[E]) => void): () => void;
129
+ /** Write several fields at once, as the app: the page's change steps do not run for it. */
130
+ setValues(values: Values): void;
131
+ /** Run steps on the form, as a button would. */
132
+ run(steps: readonly ActionStep[], context?: RunContext): Promise<RunResult>;
84
133
  destroy(): void;
85
134
  }
86
135
  /**