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