@fieldia/widgets 0.11.0 → 0.13.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 (243) hide show
  1. package/package.json +2 -2
  2. package/src/index.d.mts +2 -0
  3. package/src/index.d.ts +2 -0
  4. package/src/index.js +6 -1
  5. package/src/index.js.map +1 -1
  6. package/src/index.mjs +2 -0
  7. package/src/index.mjs.map +1 -1
  8. package/src/lib/badge.d.mts +8 -0
  9. package/src/lib/badge.d.ts +8 -0
  10. package/src/lib/badge.js +37 -0
  11. package/src/lib/badge.js.map +1 -0
  12. package/src/lib/badge.mjs +33 -0
  13. package/src/lib/badge.mjs.map +1 -0
  14. package/src/lib/calendar.d.mts +3 -1
  15. package/src/lib/calendar.d.ts +3 -1
  16. package/src/lib/calendar.js +17 -1
  17. package/src/lib/calendar.js.map +1 -1
  18. package/src/lib/calendar.mjs +17 -1
  19. package/src/lib/calendar.mjs.map +1 -1
  20. package/src/lib/cards.js +12 -4
  21. package/src/lib/cards.js.map +1 -1
  22. package/src/lib/cards.mjs +13 -5
  23. package/src/lib/cards.mjs.map +1 -1
  24. package/src/lib/choice-images.js +1 -1
  25. package/src/lib/choice-images.js.map +1 -1
  26. package/src/lib/choice-images.mjs +1 -1
  27. package/src/lib/choice-images.mjs.map +1 -1
  28. package/src/lib/choice-tags.js +6 -4
  29. package/src/lib/choice-tags.js.map +1 -1
  30. package/src/lib/choice-tags.mjs +6 -4
  31. package/src/lib/choice-tags.mjs.map +1 -1
  32. package/src/lib/colour.d.mts +11 -0
  33. package/src/lib/colour.d.ts +11 -0
  34. package/src/lib/colour.js +114 -0
  35. package/src/lib/colour.js.map +1 -0
  36. package/src/lib/colour.mjs +110 -0
  37. package/src/lib/colour.mjs.map +1 -0
  38. package/src/lib/copy.d.mts +8 -0
  39. package/src/lib/copy.d.ts +8 -0
  40. package/src/lib/copy.js +58 -0
  41. package/src/lib/copy.js.map +1 -0
  42. package/src/lib/copy.mjs +54 -0
  43. package/src/lib/copy.mjs.map +1 -0
  44. package/src/lib/date-range.d.mts +2 -0
  45. package/src/lib/date-range.d.ts +2 -0
  46. package/src/lib/date-range.js +220 -0
  47. package/src/lib/date-range.js.map +1 -0
  48. package/src/lib/date-range.mjs +216 -0
  49. package/src/lib/date-range.mjs.map +1 -0
  50. package/src/lib/display.d.mts +25 -1
  51. package/src/lib/display.d.ts +25 -1
  52. package/src/lib/display.js +42 -2
  53. package/src/lib/display.js.map +1 -1
  54. package/src/lib/display.mjs +39 -2
  55. package/src/lib/display.mjs.map +1 -1
  56. package/src/lib/distribution.d.mts +13 -0
  57. package/src/lib/distribution.d.ts +13 -0
  58. package/src/lib/distribution.js +159 -0
  59. package/src/lib/distribution.js.map +1 -0
  60. package/src/lib/distribution.mjs +152 -0
  61. package/src/lib/distribution.mjs.map +1 -0
  62. package/src/lib/duration.d.mts +18 -0
  63. package/src/lib/duration.d.ts +18 -0
  64. package/src/lib/duration.js +137 -0
  65. package/src/lib/duration.js.map +1 -0
  66. package/src/lib/duration.mjs +128 -0
  67. package/src/lib/duration.mjs.map +1 -0
  68. package/src/lib/embed.d.mts +3 -0
  69. package/src/lib/embed.d.ts +3 -0
  70. package/src/lib/embed.js +97 -0
  71. package/src/lib/embed.js.map +1 -0
  72. package/src/lib/embed.mjs +92 -0
  73. package/src/lib/embed.mjs.map +1 -0
  74. package/src/lib/extras.js +6 -0
  75. package/src/lib/extras.js.map +1 -1
  76. package/src/lib/extras.mjs +6 -0
  77. package/src/lib/extras.mjs.map +1 -1
  78. package/src/lib/files.d.mts +11 -0
  79. package/src/lib/files.d.ts +11 -0
  80. package/src/lib/files.js +19 -0
  81. package/src/lib/files.js.map +1 -1
  82. package/src/lib/files.mjs +18 -0
  83. package/src/lib/files.mjs.map +1 -1
  84. package/src/lib/float.d.mts +14 -0
  85. package/src/lib/float.d.ts +14 -0
  86. package/src/lib/float.js +91 -0
  87. package/src/lib/float.js.map +1 -0
  88. package/src/lib/float.mjs +88 -0
  89. package/src/lib/float.mjs.map +1 -0
  90. package/src/lib/kind-parts.d.mts +4 -3
  91. package/src/lib/kind-parts.d.ts +4 -3
  92. package/src/lib/kind-parts.js +5 -3
  93. package/src/lib/kind-parts.js.map +1 -1
  94. package/src/lib/kind-parts.mjs +5 -3
  95. package/src/lib/kind-parts.mjs.map +1 -1
  96. package/src/lib/labels.d.mts +39 -0
  97. package/src/lib/labels.d.ts +39 -0
  98. package/src/lib/labels.js +29 -0
  99. package/src/lib/labels.js.map +1 -1
  100. package/src/lib/labels.mjs +29 -0
  101. package/src/lib/labels.mjs.map +1 -1
  102. package/src/lib/layout-styles.d.mts +1 -1
  103. package/src/lib/layout-styles.d.ts +1 -1
  104. package/src/lib/layout-styles.js +12 -5
  105. package/src/lib/layout-styles.js.map +1 -1
  106. package/src/lib/layout-styles.mjs +12 -5
  107. package/src/lib/layout-styles.mjs.map +1 -1
  108. package/src/lib/lines.d.mts +12 -2
  109. package/src/lib/lines.d.ts +12 -2
  110. package/src/lib/lines.js +372 -25
  111. package/src/lib/lines.js.map +1 -1
  112. package/src/lib/lines.mjs +371 -26
  113. package/src/lib/lines.mjs.map +1 -1
  114. package/src/lib/links-table.d.mts +11 -0
  115. package/src/lib/links-table.d.ts +11 -0
  116. package/src/lib/links-table.js +116 -0
  117. package/src/lib/links-table.js.map +1 -0
  118. package/src/lib/links-table.mjs +112 -0
  119. package/src/lib/links-table.mjs.map +1 -0
  120. package/src/lib/locales/ar.js +29 -0
  121. package/src/lib/locales/ar.js.map +1 -1
  122. package/src/lib/locales/ar.mjs +29 -0
  123. package/src/lib/locales/ar.mjs.map +1 -1
  124. package/src/lib/locales/de.js +29 -0
  125. package/src/lib/locales/de.js.map +1 -1
  126. package/src/lib/locales/de.mjs +29 -0
  127. package/src/lib/locales/de.mjs.map +1 -1
  128. package/src/lib/locales/fr.js +29 -0
  129. package/src/lib/locales/fr.js.map +1 -1
  130. package/src/lib/locales/fr.mjs +29 -0
  131. package/src/lib/locales/fr.mjs.map +1 -1
  132. package/src/lib/numbers.d.mts +5 -1
  133. package/src/lib/numbers.d.ts +5 -1
  134. package/src/lib/numbers.js +15 -2
  135. package/src/lib/numbers.js.map +1 -1
  136. package/src/lib/numbers.mjs +13 -2
  137. package/src/lib/numbers.mjs.map +1 -1
  138. package/src/lib/payments.d.mts +2 -0
  139. package/src/lib/payments.d.ts +2 -0
  140. package/src/lib/payments.js +94 -0
  141. package/src/lib/payments.js.map +1 -0
  142. package/src/lib/payments.mjs +90 -0
  143. package/src/lib/payments.mjs.map +1 -0
  144. package/src/lib/popup.d.mts +11 -0
  145. package/src/lib/popup.d.ts +11 -0
  146. package/src/lib/popup.js +48 -0
  147. package/src/lib/popup.js.map +1 -0
  148. package/src/lib/popup.mjs +45 -0
  149. package/src/lib/popup.mjs.map +1 -0
  150. package/src/lib/priority.d.mts +10 -0
  151. package/src/lib/priority.d.ts +10 -0
  152. package/src/lib/priority.js +59 -0
  153. package/src/lib/priority.js.map +1 -0
  154. package/src/lib/priority.mjs +55 -0
  155. package/src/lib/priority.mjs.map +1 -0
  156. package/src/lib/progress.js +3 -0
  157. package/src/lib/progress.js.map +1 -1
  158. package/src/lib/progress.mjs +3 -0
  159. package/src/lib/progress.mjs.map +1 -1
  160. package/src/lib/properties.js +100 -9
  161. package/src/lib/properties.js.map +1 -1
  162. package/src/lib/properties.mjs +100 -9
  163. package/src/lib/properties.mjs.map +1 -1
  164. package/src/lib/read-text.d.mts +25 -0
  165. package/src/lib/read-text.d.ts +25 -0
  166. package/src/lib/read-text.js +102 -0
  167. package/src/lib/read-text.js.map +1 -0
  168. package/src/lib/read-text.mjs +98 -0
  169. package/src/lib/read-text.mjs.map +1 -0
  170. package/src/lib/relations.d.mts +12 -0
  171. package/src/lib/relations.d.ts +12 -0
  172. package/src/lib/relations.js +110 -10
  173. package/src/lib/relations.js.map +1 -1
  174. package/src/lib/relations.mjs +108 -10
  175. package/src/lib/relations.mjs.map +1 -1
  176. package/src/lib/slider.js +1 -1
  177. package/src/lib/slider.js.map +1 -1
  178. package/src/lib/slider.mjs +1 -1
  179. package/src/lib/slider.mjs.map +1 -1
  180. package/src/lib/state-dot.d.mts +2 -0
  181. package/src/lib/state-dot.d.ts +2 -0
  182. package/src/lib/state-dot.js +107 -0
  183. package/src/lib/state-dot.js.map +1 -0
  184. package/src/lib/state-dot.mjs +103 -0
  185. package/src/lib/state-dot.mjs.map +1 -0
  186. package/src/lib/statusbar.js +151 -14
  187. package/src/lib/statusbar.js.map +1 -1
  188. package/src/lib/statusbar.mjs +151 -14
  189. package/src/lib/statusbar.mjs.map +1 -1
  190. package/src/lib/styles-business.d.mts +8 -0
  191. package/src/lib/styles-business.d.ts +8 -0
  192. package/src/lib/styles-business.js +120 -0
  193. package/src/lib/styles-business.js.map +1 -0
  194. package/src/lib/styles-business.mjs +117 -0
  195. package/src/lib/styles-business.mjs.map +1 -0
  196. package/src/lib/styles-inputs.d.mts +1 -1
  197. package/src/lib/styles-inputs.d.ts +1 -1
  198. package/src/lib/styles-inputs.js +2 -0
  199. package/src/lib/styles-inputs.js.map +1 -1
  200. package/src/lib/styles-inputs.mjs +2 -0
  201. package/src/lib/styles-inputs.mjs.map +1 -1
  202. package/src/lib/styles-kinds.d.mts +1 -1
  203. package/src/lib/styles-kinds.d.ts +1 -1
  204. package/src/lib/styles-kinds.js +2 -2
  205. package/src/lib/styles-kinds.mjs +2 -2
  206. package/src/lib/styles-sheet.d.mts +6 -0
  207. package/src/lib/styles-sheet.d.ts +6 -0
  208. package/src/lib/styles-sheet.js +139 -0
  209. package/src/lib/styles-sheet.js.map +1 -0
  210. package/src/lib/styles-sheet.mjs +136 -0
  211. package/src/lib/styles-sheet.mjs.map +1 -0
  212. package/src/lib/styles.js +223 -27
  213. package/src/lib/styles.js.map +1 -1
  214. package/src/lib/styles.mjs +223 -27
  215. package/src/lib/styles.mjs.map +1 -1
  216. package/src/lib/tax-totals.d.mts +2 -0
  217. package/src/lib/tax-totals.d.ts +2 -0
  218. package/src/lib/tax-totals.js +92 -0
  219. package/src/lib/tax-totals.js.map +1 -0
  220. package/src/lib/tax-totals.mjs +88 -0
  221. package/src/lib/tax-totals.mjs.map +1 -0
  222. package/src/lib/themes.d.mts +29 -0
  223. package/src/lib/themes.d.ts +29 -0
  224. package/src/lib/themes.js +180 -0
  225. package/src/lib/themes.js.map +1 -0
  226. package/src/lib/themes.mjs +177 -0
  227. package/src/lib/themes.mjs.map +1 -0
  228. package/src/lib/timer.d.mts +4 -0
  229. package/src/lib/timer.d.ts +4 -0
  230. package/src/lib/timer.js +62 -0
  231. package/src/lib/timer.js.map +1 -0
  232. package/src/lib/timer.mjs +57 -0
  233. package/src/lib/timer.mjs.map +1 -0
  234. package/src/lib/widgets.d.mts +18 -3
  235. package/src/lib/widgets.d.ts +18 -3
  236. package/src/lib/widgets.js +89 -10
  237. package/src/lib/widgets.js.map +1 -1
  238. package/src/lib/widgets.mjs +90 -11
  239. package/src/lib/widgets.mjs.map +1 -1
  240. package/src/lib/yes-no.js +1 -1
  241. package/src/lib/yes-no.js.map +1 -1
  242. package/src/lib/yes-no.mjs +1 -1
  243. package/src/lib/yes-no.mjs.map +1 -1
package/src/lib/lines.mjs CHANGED
@@ -1,9 +1,29 @@
1
1
  import { isEmpty, lineKind } from '@fieldia/core';
2
- import { displayValue } from './display.mjs';
2
+ import { cellText, currencyOf } from './display.mjs';
3
+ import { drawIcon } from './icons.mjs';
3
4
  import { askFirst, fillIn, maker } from './kind-parts.mjs';
4
5
  import { WIDGET_LABELS } from './labels.mjs';
5
6
  import { moveLineTo } from './line-moves.mjs';
6
7
  import { createWidget } from './widgets.mjs';
8
+ /**
9
+ * A one2many as an editable table. Each cell is an ordinary widget; a small
10
+ * adapter routes its edits to `updateLine` and its searches to `searchLine`,
11
+ * so a many2one inside a line searches with that line's own values. Rows are
12
+ * matched by line key, so adding or removing a line never rebuilds the others.
13
+ * When the field declares `lineKinds`, a section or a note is one wide row of
14
+ * its text.
15
+ *
16
+ * A line moves by the grip at its start, or by Alt+↑/↓ from inside it, said
17
+ * aloud. The node's `options`: `min` lines at least — a new table starts with
18
+ * them — and `max` at most; `addLabel`, the Add button's words (and
19
+ * `addSectionLabel`, `addNoteLabel`, its section's and note's); `emptyLabel`,
20
+ * a sentence while there is no line; `confirmDelete`, asking before a line
21
+ * with something in it goes.
22
+ */
23
+ /** The kinds of field whose column is as wide as its values, so the columns of words and links have the rest. */
24
+ /** Columns of numbers, which grow past their kind's width for a long value. */
25
+ const NUMERIC = new Set(['integer', 'float', 'monetary']);
26
+ const FIXED_WIDTH = new Set(['boolean', 'integer', 'float', 'monetary', 'date', 'datetime']);
7
27
  const count = (value) => (typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined);
8
28
  /** The field a section or note is typed into: one line for a heading, a growing box for a note. */
9
29
  export function kindTextField(def, kind) {
@@ -16,11 +36,57 @@ export function lineForm(form, field, key) {
16
36
  ...form,
17
37
  getState: () => ({ ...form.getState(), values: values() }),
18
38
  setValue: (name, value) => form.updateLine(field, key, name, value),
19
- search: (name, query, limit) => form.searchLine(field, key, name, query, limit),
39
+ search: (name, query, limit, options) => form.searchLine(field, key, name, query, limit, options),
20
40
  canCreate: (name) => form.canCreateLine(field, name),
21
- quickCreate: (name, text) => form.quickCreateLine(field, name, text),
41
+ quickCreate: (name, text) => form.quickCreateLine(field, name, text, key),
42
+ createValues: (name) => form.createValues(name, { lines: field, key }),
22
43
  };
23
44
  }
45
+ /** A line's money in its record's currency (`parent.…`), as a field of its own: in that currency, fixed — for a dialog with no record round it. */
46
+ export function ownCurrency(sub, parent) {
47
+ if (sub.type !== 'monetary' || !sub.currencyField?.startsWith('parent.'))
48
+ return sub;
49
+ const { currencyField: _, ...rest } = sub;
50
+ const code = currencyOf(sub, {}, parent);
51
+ return code && /^[A-Z]{3}$/.test(code) ? { ...rest, currency: code } : rest;
52
+ }
53
+ /**
54
+ * A line opened in a dialog: with `lineOpens: "record"`, a saved line's own
55
+ * record by the table's model — what its page saved is written back to the
56
+ * line's fields of the same names — else, or for a line not saved yet, every
57
+ * one of its fields, recalculated as they change and written back on Save &
58
+ * Close. Shared with @fieldia/grid.
59
+ */
60
+ export async function openLineDialog(form, name, node, line, dialogs, readonly) {
61
+ const def = form.page.fields[name];
62
+ const lines = () => form.getState().values[name] ?? [];
63
+ const write = (values) => form.setValue(name, lines().map((l) => (l.key === line.key ? { ...l, values: { ...l.values, ...values } } : l)));
64
+ if (node.lineOpens === 'record' && line.id !== undefined && dialogs.canOpen(def.relation)) {
65
+ const saved = await dialogs.openRecord(def.relation, { recordId: line.id, title: def.label, withValues: true });
66
+ const back = Object.fromEntries(Object.entries(saved?.values ?? {}).filter(([key]) => key in def.fields && key !== def.sequenceField));
67
+ if (Object.keys(back).length)
68
+ write(back);
69
+ return;
70
+ }
71
+ const fields = Object.fromEntries(Object.entries(def.fields)
72
+ .filter(([key]) => key !== def.lineKinds?.field && key !== def.sequenceField)
73
+ .map(([key, sub]) => [key, ownCurrency(sub, form.getState().values)]));
74
+ // The dialog shows what the onchange makes of the line as it is edited; the line itself waits for Save & Close.
75
+ // Laid out by the table's lineForm when it has one, its conditions reading the line, its record as parent.
76
+ const state = form.getState();
77
+ const values = await dialogs.editValues({
78
+ title: def.label,
79
+ fields,
80
+ values: line.values,
81
+ readonly,
82
+ recompute: (edited) => form.previewLine(name, line.key, edited),
83
+ parent: { values: state.values, fields: form.page.fields, id: state.recordId },
84
+ ...(node.lineForm ? { layout: node.lineForm.children } : {}),
85
+ });
86
+ // Written in one go, so the form recalculates once.
87
+ if (values)
88
+ write(values);
89
+ }
24
90
  export const linesWidget = ({ form, name, field, node, id, document, labels = WIDGET_LABELS.en, locale, dialogs }) => {
25
91
  const def = field;
26
92
  const kinds = def.lineKinds;
@@ -31,10 +97,20 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
31
97
  const words = (key) => (typeof options[key] === 'string' && options[key].trim() ? options[key] : null);
32
98
  // The fields that say what a line is and keep the lines' order never show as columns.
33
99
  // Optional columns the page starts hidden stay out of the plain table.
34
- const columns = (node.columns ?? Object.keys(def.fields)).filter((column) => def.fields[column] && column !== kinds?.field && column !== def.sequenceField && node.optionalColumns?.[column] !== 'hide');
100
+ const heads = new Map();
101
+ // The table's own rules, read by the form line by line; a table with none asks nothing.
102
+ const ruled = !!(node.cells || node.rowTones || node.rowBold !== undefined || node.rowButtons || node.lineDelete !== undefined);
103
+ const columns = (node.columns ?? Object.keys(def.fields)).filter(
104
+ // Lines a line holds are edited in its form, never drawn in its row.
105
+ (column) => def.fields[column] && def.fields[column].type !== 'one2many' && column !== kinds?.field && column !== def.sequenceField && node.optionalColumns?.[column] !== 'hide');
35
106
  const element = document.createElement('div');
36
107
  element.className = 'fd-lines';
37
108
  element.id = id;
109
+ // Lines as cards on a narrow form, or always; columns as wide as they hold: the stylesheet draws them so.
110
+ if (node.cards)
111
+ element.dataset['cards'] = node.cards;
112
+ if (node.fit)
113
+ element.dataset['fit'] = node.fit;
38
114
  element.setAttribute('role', 'group');
39
115
  const scroller = document.createElement('div');
40
116
  scroller.className = 'fd-lines-scroll';
@@ -42,11 +118,23 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
42
118
  table.className = 'fd-lines-table';
43
119
  const head = document.createElement('thead');
44
120
  const headRow = document.createElement('tr');
45
- headRow.append(make('th', { class: 'fd-lines-grip' }));
121
+ // Lines chosen for the table's buttons for them: a tick on each line, one in the head for every line, and a bar over the table.
122
+ const choosing = !!node.selectedButtons?.length;
123
+ const chosen = new Set();
124
+ const pickAll = choosing ? make('input', { type: 'checkbox', class: 'fd-checkbox fd-line-pick fd-line-pick-all', 'aria-label': labels.chooseAllLines }) : null;
125
+ headRow.append(make('th', { class: 'fd-lines-grip' }, ...(pickAll ? [pickAll] : [])));
46
126
  for (const column of columns) {
47
127
  const th = document.createElement('th');
48
128
  th.scope = 'col';
129
+ th.dataset['column'] = column;
49
130
  th.textContent = def.fields[column].label;
131
+ const width = node.cells?.[column]?.width;
132
+ if (width)
133
+ th.style.width = `${width}ch`;
134
+ // Its kind, which sizes a column of yes/no, numbers, money or dates to its values, as Flectra's lists do.
135
+ else if (FIXED_WIDTH.has(def.fields[column].type))
136
+ th.dataset['fit'] = def.fields[column].type;
137
+ heads.set(column, th);
50
138
  headRow.append(th);
51
139
  }
52
140
  const lastTh = document.createElement('th');
@@ -58,6 +146,7 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
58
146
  // Number columns the page asks to add up, in a row under the lines.
59
147
  const totals = (node.totals ?? []).filter((column) => columns.includes(column));
60
148
  const sums = new Map();
149
+ const footCells = new Map();
61
150
  if (totals.length) {
62
151
  const foot = document.createElement('tfoot');
63
152
  const row = document.createElement('tr');
@@ -65,6 +154,8 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
65
154
  row.append(document.createElement('td'));
66
155
  columns.forEach((column, i) => {
67
156
  const td = document.createElement('td');
157
+ td.dataset['column'] = column;
158
+ footCells.set(column, td);
68
159
  if (totals.includes(column))
69
160
  sums.set(column, td);
70
161
  else if (i === 0)
@@ -96,19 +187,82 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
96
187
  };
97
188
  addButton(words('addLabel') ?? labels.addLine, {}, 'line');
98
189
  if (kinds) {
99
- addButton(labels.addSection, { [kinds.field]: kinds.section ?? 'section' }, 'section');
100
- addButton(labels.addNote, { [kinds.field]: kinds.note ?? 'note' }, 'note');
190
+ addButton(words('addSectionLabel') ?? labels.addSection, { [kinds.field]: kinds.section ?? 'section' }, 'section');
191
+ addButton(words('addNoteLabel') ?? labels.addNote, { [kinds.field]: kinds.note ?? 'note' }, 'note');
101
192
  }
193
+ // Buttons in the control row, beside the add buttons: the record's, shown by a condition on it (Flectra's <control>).
194
+ const controls = (node.controlButtons ?? []).map((own) => {
195
+ const icon = drawIcon(document, own.icon);
196
+ const button = make('button', { type: 'button', class: 'fd-button fd-button-link fd-lines-add fd-lines-control', 'data-node': own.id }, ...(icon ? [icon] : []), own.label);
197
+ button.addEventListener('click', () => void pressed(button, () => form.runAction(own.id)));
198
+ adds.append(button);
199
+ return { id: own.id, button };
200
+ });
201
+ // The bar of buttons for the lines chosen, over the table while any is.
202
+ const chosenCount = make('span', { class: 'fd-lines-chosen-count', role: 'status' });
203
+ const chosenButtons = (node.selectedButtons ?? []).map((own) => {
204
+ const icon = drawIcon(document, own.icon);
205
+ const button = make('button', { type: 'button', class: `fd-button fd-button-${own.style ?? 'secondary'} fd-lines-chosen-button`, 'data-node': own.id }, ...(icon ? [icon] : []), own.label);
206
+ // In the table's order, whatever order they were ticked in.
207
+ button.addEventListener('click', () => void pressed(button, () => form.runLinesAction(node.id, own.id, current().filter((line) => chosen.has(line.key)).map((line) => line.key))));
208
+ return { id: own.id, button };
209
+ });
210
+ const bar = choosing ? make('div', { class: 'fd-lines-chosen', hidden: '' }, chosenCount, ...chosenButtons.map((b) => b.button)) : null;
102
211
  const emptyWords = words('emptyLabel');
103
212
  const empty = emptyWords ? make('div', { class: 'fd-help fd-lines-empty' }, emptyWords) : null;
104
213
  // Removing a line with something in it asks first, in place: "Remove line 2? Remove · Keep".
105
214
  const confirm = askFirst(make, labels);
106
215
  // Where a line went, said politely; not the page's own announcer, which a page looks for by its class.
107
216
  const voice = make('div', { class: 'fd-sr-only', role: 'status' });
108
- element.append(scroller, ...(empty ? [empty] : []), adds, confirm.element, voice);
217
+ element.append(...(bar ? [bar] : []), scroller, ...(empty ? [empty] : []), adds, confirm.element, voice);
218
+ /** A press that waits for its run, and is not pressed again meanwhile. */
219
+ async function pressed(button, run) {
220
+ if (button.hasAttribute('aria-busy'))
221
+ return;
222
+ button.setAttribute('aria-busy', 'true');
223
+ try {
224
+ await run();
225
+ }
226
+ finally {
227
+ button.removeAttribute('aria-busy');
228
+ }
229
+ }
230
+ /** The bar and the head's tick, as the lines chosen are now. */
231
+ function showChosen() {
232
+ if (!bar || !pickAll)
233
+ return;
234
+ const items = current().filter((line) => !lineKind(def, line.values));
235
+ for (const key of [...chosen])
236
+ if (!items.some((line) => line.key === key))
237
+ chosen.delete(key);
238
+ bar.hidden = readonly || chosen.size === 0;
239
+ chosenCount.textContent = fillIn(labels.linesChosen, { n: chosen.size });
240
+ for (const { id, button } of chosenButtons)
241
+ button.hidden = form.node(id).invisible;
242
+ pickAll.checked = items.length > 0 && chosen.size === items.length;
243
+ pickAll.indeterminate = chosen.size > 0 && chosen.size < items.length;
244
+ pickAll.disabled = readonly || !items.length;
245
+ for (const [key, row] of rows) {
246
+ if (!row.pick)
247
+ continue;
248
+ row.pick.checked = chosen.has(key);
249
+ row.pick.disabled = readonly;
250
+ }
251
+ }
252
+ pickAll?.addEventListener('change', () => {
253
+ const items = current().filter((line) => !lineKind(def, line.values));
254
+ if (pickAll.checked)
255
+ for (const line of items)
256
+ chosen.add(line.key);
257
+ else
258
+ chosen.clear();
259
+ showChosen();
260
+ });
109
261
  const rows = new Map();
110
262
  let focusNew = null;
111
263
  let readonly = false;
264
+ /** The lines the columns were last sized for, by their keys. */
265
+ let columnsSizedFor = null;
112
266
  const current = () => form.getState().values[name] ?? [];
113
267
  // A new table starts with the lines it needs at least.
114
268
  for (let have = current().length; have < min; have++)
@@ -126,6 +280,65 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
126
280
  focused.focus();
127
281
  voice.textContent = fillIn(labels.movedTo, { label: typeof first === 'string' && first.trim() ? first : fillIn(labels.lineN, { n: from + 1 }), n: to + 1, total: current().length });
128
282
  }
283
+ /**
284
+ * A line lifted by its grip: it follows the pointer, the lines it passes
285
+ * slide out of its way, and the table's value changes once, when it is let
286
+ * go — as a grid's row drags. Escape puts it back.
287
+ */
288
+ function lift(tr, key, startY) {
289
+ const all = [...body.children];
290
+ const from = all.indexOf(tr);
291
+ const others = all.filter((row) => row !== tr);
292
+ const box = tr.getBoundingClientRect();
293
+ const middles = others.map((row) => {
294
+ const at = row.getBoundingClientRect();
295
+ return at.top + at.height / 2;
296
+ });
297
+ let to = from;
298
+ let lifted = false;
299
+ const escape = (event) => {
300
+ if (event.key !== 'Escape')
301
+ return;
302
+ event.preventDefault();
303
+ settle();
304
+ };
305
+ function settle() {
306
+ for (const row of all)
307
+ row.style.transform = '';
308
+ tr.classList.remove('fd-line-lifted');
309
+ body.classList.remove('fd-lines-dragging');
310
+ document.removeEventListener('keydown', escape, true);
311
+ lifted = false;
312
+ }
313
+ return {
314
+ follow(y) {
315
+ const by = y - startY;
316
+ if (!lifted) {
317
+ // A press that hardly moves is not a drag yet.
318
+ if (Math.abs(by) < 4 || readonly)
319
+ return;
320
+ lifted = true;
321
+ tr.classList.add('fd-line-lifted');
322
+ body.classList.add('fd-lines-dragging');
323
+ document.addEventListener('keydown', escape, true);
324
+ }
325
+ tr.style.transform = `translateY(${by}px)`;
326
+ // Its place: after every other line whose middle its own middle has passed.
327
+ const middle = box.top + box.height / 2 + by;
328
+ to = middles.filter((at) => at < middle).length;
329
+ others.forEach((row, i) => {
330
+ row.style.transform = i >= from && i < to ? `translateY(${-box.height}px)` : i < from && i >= to ? `translateY(${box.height}px)` : '';
331
+ });
332
+ },
333
+ drop() {
334
+ const moved = lifted && to !== from;
335
+ settle();
336
+ if (moved)
337
+ move(key, to);
338
+ },
339
+ settle,
340
+ };
341
+ }
129
342
  body.addEventListener('keydown', (event) => {
130
343
  const key = event.target.closest('tr')?.dataset['line'];
131
344
  if (!key || !event.altKey || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown'))
@@ -135,10 +348,19 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
135
348
  });
136
349
  function makeCell(tr, line, column, sub, span = 1) {
137
350
  const cellId = `${id}-${line.key}-${column}`;
138
- const subNode = { type: 'field', id: `${node.id}.${line.key}.${column}`, field: column };
351
+ // A cell the page draws as a pill in its tone (Flectra's widget="badge"), never edited, or with a widget of its own.
352
+ const rules = lineKind(def, line.values) ? undefined : node.cells?.[column];
353
+ // Its settings reach its widget even without a widget named: a calendar's week numbers.
354
+ const look = rules?.badge ? { widget: 'badge' } : rules?.widget ? { widget: rules.widget, ...(rules.options ? { options: rules.options } : {}) } : rules?.options ? { options: rules.options } : {};
355
+ const subNode = { type: 'field', id: `${node.id}.${line.key}.${column}`, field: column, ...look };
139
356
  const widget = createWidget({ form: lineForm(form, name, line.key), name: column, field: sub, node: subNode, id: cellId, document, labels, locale, dialogs });
140
357
  const td = document.createElement('td');
141
358
  td.colSpan = span;
359
+ td.dataset['column'] = column;
360
+ if (FIXED_WIDTH.has(sub.type))
361
+ td.dataset['fit'] = sub.type;
362
+ // Its column's label, which a card shows before its value.
363
+ td.dataset['label'] = sub.label;
142
364
  const label = document.createElement('label');
143
365
  label.className = 'fd-sr-only';
144
366
  label.htmlFor = cellId;
@@ -148,7 +370,7 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
148
370
  error.hidden = true;
149
371
  td.append(label, widget.element, error);
150
372
  tr.append(td);
151
- return { name: column, def: sub, widget, error };
373
+ return { name: column, def: sub, widget, error, td };
152
374
  }
153
375
  function makeRow(line) {
154
376
  const tr = document.createElement('tr');
@@ -156,25 +378,35 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
156
378
  const kind = lineKind(def, line.values);
157
379
  if (kind)
158
380
  tr.className = `fd-line-${kind}`;
159
- // Dragged by its grip, the line takes the place the pointer is at.
381
+ // Dragged by its grip, the line follows the pointer and takes its place when let go.
160
382
  const grip = make('span', { class: 'fd-line-grip fd-rank-grip', 'aria-hidden': 'true' });
161
- let dragging = false;
383
+ let drag = null;
162
384
  grip.addEventListener('pointerdown', (event) => {
163
385
  if (readonly || event.button !== 0)
164
386
  return;
165
387
  event.preventDefault();
166
388
  grip.setPointerCapture?.(event.pointerId);
167
- dragging = true;
389
+ drag = lift(tr, line.key, event.clientY);
168
390
  });
169
- grip.addEventListener('pointermove', (event) => {
170
- if (!dragging)
171
- return;
172
- // The place among the others: as many as have their middle above the pointer.
173
- move(line.key, [...body.children].filter((row) => row !== tr && row.getBoundingClientRect().top + row.getBoundingClientRect().height / 2 < event.clientY).length);
391
+ grip.addEventListener('pointermove', (event) => drag?.follow(event.clientY));
392
+ grip.addEventListener('pointerup', () => {
393
+ drag?.drop();
394
+ drag = null;
395
+ });
396
+ grip.addEventListener('pointercancel', () => {
397
+ drag?.settle();
398
+ drag = null;
174
399
  });
175
- for (const end of ['pointerup', 'pointercancel'])
176
- grip.addEventListener(end, () => (dragging = false));
177
- tr.append(make('td', { class: 'fd-lines-grip' }, grip));
400
+ // Its tick for the table's buttons for chosen lines, before the grip.
401
+ const pick = choosing && !kind ? make('input', { type: 'checkbox', class: 'fd-checkbox fd-line-pick' }) : null;
402
+ pick?.addEventListener('change', () => {
403
+ if (pick.checked)
404
+ chosen.add(line.key);
405
+ else
406
+ chosen.delete(line.key);
407
+ showChosen();
408
+ });
409
+ tr.append(make('td', { class: 'fd-lines-grip' }, ...(pick ? [make('span', { class: 'fd-line-lead' }, pick, grip)] : [grip])));
178
410
  const cells = kind && kinds
179
411
  ? [makeCell(tr, line, kinds.text, kindTextField(def.fields[kinds.text], kind), columns.length)]
180
412
  : columns.map((column) => makeCell(tr, line, column, def.fields[column]));
@@ -199,9 +431,55 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
199
431
  (next ? rows.get(next.key)?.remove : adds.querySelector('button'))?.focus();
200
432
  });
201
433
  });
434
+ // The line's own buttons, each shown by its condition on the line; a press runs with the line.
435
+ const buttons = new Map();
436
+ for (const own of kind ? [] : (node.rowButtons ?? [])) {
437
+ const icon = drawIcon(document, own.icon);
438
+ const button = make('button', { type: 'button', class: `fd-button fd-button-link fd-line-button${icon ? ' fd-line-button-icon' : ''}`, 'data-row-button': own.id, 'aria-label': own.label, title: own.label });
439
+ button.append(icon ?? own.label);
440
+ button.addEventListener('click', async () => {
441
+ if (button.hasAttribute('aria-busy'))
442
+ return;
443
+ button.setAttribute('aria-busy', 'true');
444
+ try {
445
+ await form.runRowAction(node.id, own.id, line.key);
446
+ }
447
+ finally {
448
+ button.removeAttribute('aria-busy');
449
+ }
450
+ });
451
+ buttons.set(own.id, button);
452
+ tools.append(button);
453
+ }
454
+ // A copy of the line, its values too, right after it: never its saved id or its place in the order.
455
+ const copy = options['copy'] === true && !kind ? make('button', { type: 'button', class: 'fd-line-copy' }, '⧉') : null;
456
+ copy?.addEventListener('click', () => {
457
+ const all = current();
458
+ const at = all.findIndex((l) => l.key === line.key);
459
+ if (readonly || at < 0 || all.length >= max)
460
+ return;
461
+ const values = { ...all[at].values };
462
+ if (def.sequenceField)
463
+ delete values[def.sequenceField];
464
+ const made = form.addLine(name, values);
465
+ moveLineTo(form, name, def, made, at + 1);
466
+ rows.get(made)?.cells[0]?.widget.focus();
467
+ });
468
+ if (copy)
469
+ tools.append(copy);
470
+ // Its own page, its own form (lineForm), or its every field, in a dialog (lineOpens).
471
+ if ((node.lineOpens || node.lineForm) && dialogs && !kind) {
472
+ const open = make('button', { type: 'button', class: 'fd-line-open', 'aria-label': labels.openLine, title: labels.openLine }, '↗');
473
+ open.addEventListener('click', () => {
474
+ const now = current().find((l) => l.key === line.key);
475
+ if (now)
476
+ void openLineDialog(form, name, node, now, dialogs, readonly);
477
+ });
478
+ tools.append(open);
479
+ }
202
480
  tools.append(remove);
203
481
  tr.append(tools);
204
- return { element: tr, kind, cells, remove, grip };
482
+ return { element: tr, kind, cells, remove, grip, buttons, pick, copy };
205
483
  }
206
484
  return {
207
485
  element,
@@ -215,6 +493,37 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
215
493
  if (empty)
216
494
  empty.hidden = current.length > 0;
217
495
  const errors = form.getState().errors;
496
+ // Columns the record hides now: their head, cells and total go with them.
497
+ const gone = new Set(ruled ? columns.filter((column) => form.columnHidden(node.id, column)) : []);
498
+ for (const [column, th] of heads)
499
+ th.hidden = gone.has(column);
500
+ for (const [column, td] of footCells)
501
+ td.hidden = gone.has(column);
502
+ // A column of words or links as wide as its longest value, as a list of words is: a long name has the room a code does not need.
503
+ // Measured as lines come and go, not as a cell is typed in: the columns never move under the person's hands.
504
+ const sizedFor = current.map((line) => line.key).join(' ');
505
+ const resize = sizedFor !== columnsSizedFor;
506
+ columnsSizedFor = sizedFor;
507
+ for (const [column, th] of resize ? heads : new Map()) {
508
+ if (node.cells?.[column]?.width)
509
+ continue;
510
+ const sub = def.fields[column];
511
+ if (th.dataset['fit']) {
512
+ // A number keeps its kind's width at the least, and has room for its longest value too ("E£ 21,600,000.00"), never cut.
513
+ if (!NUMERIC.has(sub.type))
514
+ continue;
515
+ const widest = current.reduce((most, line) => (lineKind(def, line.values) ? most : Math.max(most, cellText(sub, line.values[column], line.values, locale).length)), 0);
516
+ th.style.setProperty('--fd-fit-need', `${widest + 4}ch`);
517
+ continue;
518
+ }
519
+ const longest = current.reduce((most, line) => (lineKind(def, line.values) ? most : Math.max(most, cellText(sub, line.values[column], line.values, locale).length)), 0);
520
+ // A link's box holds its open and clear buttons too.
521
+ const buttons = sub.type === 'many2one' ? 5 : 0;
522
+ // Its head may wrap: a long label over short codes does not widen it.
523
+ th.style.width = `${Math.min(Math.max(longest, Math.min(sub.label.length, 10), 6), 32) + 4 + buttons}ch`;
524
+ // Short values keep their whole width when the table is tight: the long ones give way.
525
+ th.style.minWidth = `${Math.min(Math.max(longest, 6), 14) + 4}ch`;
526
+ }
218
527
  const keep = new Set(current.map((line) => line.key));
219
528
  for (const [key, row] of rows) {
220
529
  if (!keep.has(key)) {
@@ -235,30 +544,59 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
235
544
  }
236
545
  if (body.children[index] !== row.element)
237
546
  body.insertBefore(row.element, body.children[index] ?? null);
238
- row.remove.hidden = readonly || current.length <= min;
547
+ const now = ruled ? form.lineState(node.id, line.key) : null;
548
+ // A line the table keeps (lineDelete) shows no ×.
549
+ row.remove.hidden = readonly || current.length <= min || now?.deletable === false;
239
550
  row.grip.hidden = readonly;
551
+ // Named by where it is now: it moves.
552
+ const lineName = fillIn(labels.lineN, { n: index + 1 });
553
+ row.pick?.setAttribute('aria-label', fillIn(labels.chooseLine, { name: lineName }));
554
+ if (row.copy) {
555
+ row.copy.setAttribute('aria-label', fillIn(labels.copy, { name: lineName }));
556
+ row.copy.title = row.copy.getAttribute('aria-label');
557
+ row.copy.hidden = readonly || current.length >= max;
558
+ }
240
559
  if (readonly)
241
560
  row.remove.remove();
242
561
  else if (!row.remove.isConnected)
243
562
  row.element.lastElementChild?.append(row.remove);
563
+ setData(row.element, 'tone', now?.tone);
564
+ row.element.classList.toggle('fd-line-bold', !!now?.bold);
565
+ // A line's buttons act on the saved line, so a read-only table keeps them, as the grid and Flectra's lists do.
566
+ for (const [id, button] of row.buttons)
567
+ button.hidden = !now?.buttons[id];
568
+ // A section's or note's one cell spans the columns shown.
569
+ if (row.kind)
570
+ row.cells[0].td.colSpan = columns.length - gone.size;
244
571
  for (const cell of row.cells) {
245
572
  const message = errors[`${name}.${line.key}.${cell.name}`];
573
+ const rules = row.kind ? undefined : now?.cells[cell.name];
574
+ if (!row.kind)
575
+ cell.td.hidden = gone.has(cell.name);
576
+ // Hidden by its line, the cell stays, empty, so the column lines up.
577
+ cell.widget.element.hidden = !!rules?.invisible;
578
+ setData(cell.td, 'tone', rules?.tone);
579
+ cell.td.classList.toggle('fd-cell-bold', !!rules?.bold);
246
580
  cell.error.hidden = !message;
247
581
  cell.error.textContent = message ?? '';
248
582
  cell.widget.update({
249
583
  value: line.values[cell.name],
250
584
  values: line.values,
585
+ parent: state.values,
251
586
  // A value worked out from the line's others is shown, never typed.
252
- readonly: readonly || cell.def.readonly === true || cell.def.compute !== undefined,
253
- required: cell.def.required === true,
587
+ readonly: readonly || cell.def.readonly === true || cell.def.compute !== undefined || !!rules?.readonly,
588
+ required: cell.def.required === true || !!rules?.required,
254
589
  invalid: !!message,
255
590
  });
256
591
  }
257
592
  });
258
593
  for (const [column, td] of sums) {
259
594
  const sum = current.filter((line) => !lineKind(def, line.values)).reduce((total, line) => total + Number(line.values[column] ?? 0), 0);
260
- td.textContent = displayValue(def.fields[column], sum, current[0]?.values ?? {}, locale);
595
+ td.textContent = cellText(def.fields[column], sum, current[0]?.values ?? {}, locale, state.values, node.cells?.[column]);
261
596
  }
597
+ for (const { id, button } of controls)
598
+ button.hidden = form.node(id).invisible;
599
+ showChosen();
262
600
  if (focusNew && rows.has(focusNew)) {
263
601
  rows.get(focusNew)?.cells[0]?.widget.focus();
264
602
  focusNew = null;
@@ -266,4 +604,11 @@ export const linesWidget = ({ form, name, field, node, id, document, labels = WI
266
604
  },
267
605
  };
268
606
  };
607
+ /** A data attribute set, or taken away when there is nothing to say. */
608
+ function setData(element, key, value) {
609
+ if (value)
610
+ element.dataset[key] = value;
611
+ else
612
+ delete element.dataset[key];
613
+ }
269
614
  //# sourceMappingURL=lines.mjs.map