@react-x11/components 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 (126) hide show
  1. package/dist/flow/node.d.ts +14 -6
  2. package/dist/flow/node.d.ts.map +1 -1
  3. package/dist/flow/node.js +82 -64
  4. package/dist/flow/node.js.map +1 -1
  5. package/dist/html/controls.d.ts +38 -10
  6. package/dist/html/controls.d.ts.map +1 -1
  7. package/dist/html/controls.js +43 -58
  8. package/dist/html/controls.js.map +1 -1
  9. package/dist/html/css/cascade.d.ts +134 -20
  10. package/dist/html/css/cascade.d.ts.map +1 -1
  11. package/dist/html/css/cascade.js +538 -128
  12. package/dist/html/css/cascade.js.map +1 -1
  13. package/dist/html/css/color.d.ts +19 -0
  14. package/dist/html/css/color.d.ts.map +1 -1
  15. package/dist/html/css/color.js +86 -0
  16. package/dist/html/css/color.js.map +1 -1
  17. package/dist/html/css/parse.d.ts +40 -2
  18. package/dist/html/css/parse.d.ts.map +1 -1
  19. package/dist/html/css/parse.js +391 -13
  20. package/dist/html/css/parse.js.map +1 -1
  21. package/dist/html/css/style.d.ts +52 -1
  22. package/dist/html/css/style.d.ts.map +1 -1
  23. package/dist/html/css/style.js +328 -11
  24. package/dist/html/css/style.js.map +1 -1
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -7
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +10 -1
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js +27 -2
  32. package/dist/html/css/values.js.map +1 -1
  33. package/dist/html/css/vars.d.ts +5 -0
  34. package/dist/html/css/vars.d.ts.map +1 -1
  35. package/dist/html/css/vars.js +2 -2
  36. package/dist/html/css/vars.js.map +1 -1
  37. package/dist/html/dom.d.ts.map +1 -1
  38. package/dist/html/dom.js +38 -3
  39. package/dist/html/dom.js.map +1 -1
  40. package/dist/html/fonts.d.ts +25 -1
  41. package/dist/html/fonts.d.ts.map +1 -1
  42. package/dist/html/fonts.js +99 -6
  43. package/dist/html/fonts.js.map +1 -1
  44. package/dist/html/form.d.ts +200 -0
  45. package/dist/html/form.d.ts.map +1 -0
  46. package/dist/html/form.js +801 -0
  47. package/dist/html/form.js.map +1 -0
  48. package/dist/html/index.d.ts +12 -0
  49. package/dist/html/index.d.ts.map +1 -1
  50. package/dist/html/index.js +30 -264
  51. package/dist/html/index.js.map +1 -1
  52. package/dist/html/layout/block.d.ts +11 -2
  53. package/dist/html/layout/block.d.ts.map +1 -1
  54. package/dist/html/layout/block.js +186 -19
  55. package/dist/html/layout/block.js.map +1 -1
  56. package/dist/html/layout/boxes.d.ts +65 -7
  57. package/dist/html/layout/boxes.d.ts.map +1 -1
  58. package/dist/html/layout/boxes.js +87 -22
  59. package/dist/html/layout/boxes.js.map +1 -1
  60. package/dist/html/layout/cache.d.ts +2 -2
  61. package/dist/html/layout/cache.d.ts.map +1 -1
  62. package/dist/html/layout/cache.js +8 -3
  63. package/dist/html/layout/cache.js.map +1 -1
  64. package/dist/html/layout/flex.js +4 -1
  65. package/dist/html/layout/flex.js.map +1 -1
  66. package/dist/html/layout/floats.d.ts +4 -0
  67. package/dist/html/layout/floats.d.ts.map +1 -1
  68. package/dist/html/layout/floats.js +26 -0
  69. package/dist/html/layout/floats.js.map +1 -1
  70. package/dist/html/layout/inline.d.ts +26 -2
  71. package/dist/html/layout/inline.d.ts.map +1 -1
  72. package/dist/html/layout/inline.js +1232 -115
  73. package/dist/html/layout/inline.js.map +1 -1
  74. package/dist/html/node.d.ts +120 -15
  75. package/dist/html/node.d.ts.map +1 -1
  76. package/dist/html/node.js +993 -174
  77. package/dist/html/node.js.map +1 -1
  78. package/dist/html/paint.d.ts +75 -2
  79. package/dist/html/paint.d.ts.map +1 -1
  80. package/dist/html/paint.js +836 -200
  81. package/dist/html/paint.js.map +1 -1
  82. package/dist/html/surfaces.d.ts +7 -2
  83. package/dist/html/surfaces.d.ts.map +1 -1
  84. package/dist/html/surfaces.js +16 -0
  85. package/dist/html/surfaces.js.map +1 -1
  86. package/dist/html/widgets.d.ts +29 -0
  87. package/dist/html/widgets.d.ts.map +1 -0
  88. package/dist/html/widgets.js +620 -0
  89. package/dist/html/widgets.js.map +1 -0
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/internal/window.d.ts +22 -0
  94. package/dist/internal/window.d.ts.map +1 -1
  95. package/dist/internal/window.js +35 -8
  96. package/dist/internal/window.js.map +1 -1
  97. package/dist/richtext/node.d.ts +7 -0
  98. package/dist/richtext/node.d.ts.map +1 -1
  99. package/dist/richtext/node.js.map +1 -1
  100. package/package.json +6 -4
  101. package/src/flow/node.ts +80 -64
  102. package/src/html/controls.ts +79 -58
  103. package/src/html/css/cascade.ts +610 -146
  104. package/src/html/css/color.ts +87 -0
  105. package/src/html/css/parse.ts +431 -13
  106. package/src/html/css/style.ts +367 -11
  107. package/src/html/css/ua.ts +76 -8
  108. package/src/html/css/values.ts +31 -3
  109. package/src/html/css/vars.ts +2 -2
  110. package/src/html/dom.ts +41 -3
  111. package/src/html/fonts.ts +116 -6
  112. package/src/html/form.ts +962 -0
  113. package/src/html/index.ts +49 -300
  114. package/src/html/layout/block.ts +231 -16
  115. package/src/html/layout/boxes.ts +133 -32
  116. package/src/html/layout/cache.ts +8 -3
  117. package/src/html/layout/flex.ts +4 -1
  118. package/src/html/layout/floats.ts +24 -0
  119. package/src/html/layout/inline.ts +1445 -108
  120. package/src/html/node.ts +1010 -166
  121. package/src/html/paint.ts +980 -270
  122. package/src/html/surfaces.ts +21 -1
  123. package/src/html/widgets.ts +735 -0
  124. package/src/index.ts +1 -0
  125. package/src/internal/window.ts +36 -8
  126. package/src/richtext/node.ts +7 -0
@@ -0,0 +1,801 @@
1
+ import { attr, childrenOf, elementsIn, isElement, tagOf } from './dom.js';
2
+ import { resolveUrl } from './url.js';
3
+ // --- which controls, in which form ---------------------------------------------
4
+ /** The elements that can be submitted (HTML 4.10.2, "submittable"). */
5
+ const SUBMITTABLE = new Set(['button', 'input', 'select', 'textarea']);
6
+ /** Every `<input type>` HTML knows. Any other type, or none, is a text
7
+ * field. */
8
+ const INPUT_TYPES = new Set([
9
+ 'hidden',
10
+ 'text',
11
+ 'search',
12
+ 'tel',
13
+ 'url',
14
+ 'email',
15
+ 'password',
16
+ 'date',
17
+ 'month',
18
+ 'week',
19
+ 'time',
20
+ 'datetime-local',
21
+ 'number',
22
+ 'range',
23
+ 'color',
24
+ 'checkbox',
25
+ 'radio',
26
+ 'file',
27
+ 'submit',
28
+ 'image',
29
+ 'reset',
30
+ 'button',
31
+ ]);
32
+ /** The fields that make Enter in a form with no submit button do nothing
33
+ * when there is more than one of them (HTML 4.10.21.2). */
34
+ const BLOCKS_IMPLICIT = new Set([
35
+ 'text',
36
+ 'search',
37
+ 'email',
38
+ 'url',
39
+ 'tel',
40
+ 'password',
41
+ 'date',
42
+ 'month',
43
+ 'week',
44
+ 'time',
45
+ 'datetime-local',
46
+ 'number',
47
+ ]);
48
+ /** An `<input>`'s type state: its `type`, lowercased, or `text` where it
49
+ * names none HTML knows. */
50
+ export function inputType(el) {
51
+ const type = (attr(el, 'type') ?? '').trim().toLowerCase();
52
+ return INPUT_TYPES.has(type) ? type : 'text';
53
+ }
54
+ /**
55
+ * What pressing a control does: submits its form, resets it, nothing
56
+ * (`'button'`) — or null for something that is not a button at all. A
57
+ * `<button>` with no `type`, or one HTML does not know, submits.
58
+ */
59
+ export function buttonType(el) {
60
+ const tag = tagOf(el);
61
+ if (tag === 'button') {
62
+ const type = (attr(el, 'type') ?? '').trim().toLowerCase();
63
+ return type === 'reset' || type === 'button' ? type : 'submit';
64
+ }
65
+ if (tag !== 'input')
66
+ return null;
67
+ const type = inputType(el);
68
+ if (type === 'submit' || type === 'image')
69
+ return 'submit';
70
+ if (type === 'reset' || type === 'button')
71
+ return type;
72
+ return null;
73
+ }
74
+ /** The top of the tree an element is in: its document. */
75
+ export function rootOf(el) {
76
+ let at = el;
77
+ while (at.parent)
78
+ at = at.parent;
79
+ return at;
80
+ }
81
+ /**
82
+ * The form a control belongs to (HTML 4.10.17.3): the one its `form`
83
+ * attribute names by id, or else the nearest `<form>` around it. A `form`
84
+ * attribute that names nothing, or names something that is not a form,
85
+ * leaves it with none — it does not fall back to the form around it.
86
+ */
87
+ export function formOwner(el, root = rootOf(el)) {
88
+ return ownerOf(el, root, null);
89
+ }
90
+ function ownerOf(el, root, byId) {
91
+ const id = attr(el, 'form');
92
+ if (id !== undefined) {
93
+ const named = byId ? (byId.get(id) ?? null) : elementById(root, id);
94
+ return named && tagOf(named) === 'form' ? named : null;
95
+ }
96
+ for (let at = el.parent; at && isElement(at); at = at.parent) {
97
+ if (tagOf(at) === 'form')
98
+ return at;
99
+ }
100
+ return null;
101
+ }
102
+ function elementById(root, id) {
103
+ for (const el of elementsIn(root))
104
+ if (attr(el, 'id') === id)
105
+ return el;
106
+ return null;
107
+ }
108
+ /**
109
+ * The submittable controls a form owns, in tree order — those inside it and
110
+ * those elsewhere that name it with `form`. Not those in a `<template>`,
111
+ * whose content is inert, or a `<datalist>`, whose are its suggestions.
112
+ */
113
+ export function controlsOf(form) {
114
+ const root = rootOf(form);
115
+ let byId = null;
116
+ const out = [];
117
+ for (const el of elementsIn(root)) {
118
+ if (!SUBMITTABLE.has(tagOf(el)))
119
+ continue;
120
+ // one walk for every id, and only when something asks by one
121
+ if (attr(el, 'form') !== undefined && !byId) {
122
+ byId = new Map();
123
+ for (const any of elementsIn(root)) {
124
+ const id = attr(any, 'id');
125
+ if (id !== undefined && !byId.has(id))
126
+ byId.set(id, any);
127
+ }
128
+ }
129
+ if (ownerOf(el, root, byId) !== form || inert(el))
130
+ continue;
131
+ out.push(el);
132
+ }
133
+ return out;
134
+ }
135
+ function inert(el) {
136
+ for (let at = el.parent; at && isElement(at); at = at.parent) {
137
+ const tag = tagOf(at);
138
+ if (tag === 'template' || tag === 'datalist')
139
+ return true;
140
+ }
141
+ return false;
142
+ }
143
+ /**
144
+ * Whether a control is disabled (HTML 4.10.18.5): by its own `disabled`, or
145
+ * by a disabled `<fieldset>` around it — except inside that fieldset's first
146
+ * `<legend>`, which stays live, so a checkbox there can switch the rest on.
147
+ */
148
+ export function isDisabled(el) {
149
+ if (attr(el, 'disabled') !== undefined)
150
+ return true;
151
+ let child = el;
152
+ for (let at = el.parent; at && isElement(at); at = at.parent) {
153
+ if (tagOf(at) === 'fieldset' &&
154
+ attr(at, 'disabled') !== undefined &&
155
+ !(tagOf(child) === 'legend' && firstLegend(at) === child)) {
156
+ return true;
157
+ }
158
+ child = at;
159
+ }
160
+ return false;
161
+ }
162
+ function firstLegend(fieldset) {
163
+ for (const child of childrenOf(fieldset)) {
164
+ if (isElement(child) && tagOf(child) === 'legend')
165
+ return child;
166
+ }
167
+ return null;
168
+ }
169
+ // --- what each control holds -------------------------------------------------------
170
+ /** A `<select>`'s `<option>`s, in tree order, through its `<optgroup>`s. */
171
+ export function optionElements(select) {
172
+ const out = [];
173
+ const walk = (node) => {
174
+ for (const child of childrenOf(node)) {
175
+ if (!isElement(child))
176
+ continue;
177
+ const tag = tagOf(child);
178
+ if (tag === 'option')
179
+ out.push(child);
180
+ else if (tag === 'optgroup' && node === select)
181
+ walk(child);
182
+ }
183
+ };
184
+ walk(select);
185
+ return out;
186
+ }
187
+ /** An option's text, its white space collapsed, as HTML's `text` IDL
188
+ * attribute reads it. */
189
+ export function optionLabel(option) {
190
+ let text = '';
191
+ const walk = (node) => {
192
+ if (node.type === 'text')
193
+ text += node.data;
194
+ else
195
+ for (const child of childrenOf(node))
196
+ walk(child);
197
+ };
198
+ walk(option);
199
+ return text.replace(/[\t\n\f\r ]+/g, ' ').trim();
200
+ }
201
+ /** An option's value: its `value`, or else its text. */
202
+ export function optionValue(option) {
203
+ return attr(option, 'value') ?? optionLabel(option);
204
+ }
205
+ function optionDisabled(option) {
206
+ if (attr(option, 'disabled') !== undefined)
207
+ return true;
208
+ const parent = option.parent;
209
+ return (!!parent &&
210
+ isElement(parent) &&
211
+ tagOf(parent) === 'optgroup' &&
212
+ attr(parent, 'disabled') !== undefined);
213
+ }
214
+ /** Whether a `<select>` is a list box that takes many options. */
215
+ export function isMultiple(select) {
216
+ return attr(select, 'multiple') !== undefined;
217
+ }
218
+ /**
219
+ * The options a `<select>` has selected. A drop-down one has exactly one
220
+ * where it has any: the last marked `selected`, as the parser leaves it, or
221
+ * else the first that is not disabled (HTML 4.10.7, "selectedness setting").
222
+ * A `multiple` one has those marked, and no default.
223
+ */
224
+ export function selectedOptions(select) {
225
+ const options = optionElements(select);
226
+ const marked = options.filter((o) => attr(o, 'selected') !== undefined);
227
+ if (isMultiple(select))
228
+ return marked;
229
+ if (marked.length)
230
+ return [marked[marked.length - 1]];
231
+ const first = options.find((o) => !optionDisabled(o));
232
+ return first ? [first] : [];
233
+ }
234
+ /** A `<textarea>`'s text as its markup has it — HTML drops one newline
235
+ * straight after the open tag, so a pretty-printed one does not start on
236
+ * a blank line. */
237
+ export function textareaDefault(el) {
238
+ let text = '';
239
+ for (const child of childrenOf(el)) {
240
+ if (child.type === 'text')
241
+ text += child.data;
242
+ }
243
+ return text.replace(/^\r?\n/, '');
244
+ }
245
+ /** HTML's valid floating-point number (2.3.4.3): what a number field may
246
+ * hold, where `1.` and `+1` are not one. */
247
+ const FLOAT = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?$/;
248
+ /**
249
+ * What a text control holds: typed text where there is some, and else its
250
+ * markup's — a `<textarea>`'s content, an `<input>`'s `value` — put through
251
+ * the type's value sanitization (HTML 4.10.5.1): a single-line field holds no
252
+ * line breaks, an email or a URL no white space at its ends, and a number
253
+ * field nothing that is not a number.
254
+ */
255
+ export function controlValue(el, live) {
256
+ const typed = live?.(el);
257
+ if (tagOf(el) === 'textarea')
258
+ return typed ?? textareaDefault(el);
259
+ const raw = typed ?? attr(el, 'value') ?? '';
260
+ switch (inputType(el)) {
261
+ case 'text':
262
+ case 'search':
263
+ case 'tel':
264
+ case 'password':
265
+ return raw.replace(/[\r\n]/g, '');
266
+ case 'email':
267
+ case 'url':
268
+ return raw
269
+ .replace(/[\r\n]/g, '')
270
+ .replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, '');
271
+ case 'number':
272
+ return FLOAT.test(raw) ? raw : '';
273
+ default:
274
+ return raw;
275
+ }
276
+ }
277
+ /**
278
+ * The entries a form submits (HTML 4.10.21.4): each enabled control it owns
279
+ * that has a name, with its value — the checked checkboxes and radios, a
280
+ * `<select>`'s selected options, the button that submitted it and no other.
281
+ */
282
+ function entriesOf(form, submitter, live, point) {
283
+ const out = [];
284
+ const add = (name, value, file = false) => {
285
+ out.push({ name, value, file });
286
+ };
287
+ for (const el of controlsOf(form)) {
288
+ if (isDisabled(el))
289
+ continue;
290
+ const tag = tagOf(el);
291
+ const type = tag === 'input' ? inputType(el) : '';
292
+ if (buttonType(el) !== null && el !== submitter)
293
+ continue;
294
+ if (type === 'image') {
295
+ // an image button is its point, named after it where it has a name
296
+ const name = attr(el, 'name');
297
+ const prefix = name ? `${name}.` : '';
298
+ add(`${prefix}x`, String(Math.round(point?.x ?? 0)));
299
+ add(`${prefix}y`, String(Math.round(point?.y ?? 0)));
300
+ continue;
301
+ }
302
+ const name = attr(el, 'name');
303
+ if (!name)
304
+ continue;
305
+ if (tag === 'select') {
306
+ for (const option of selectedOptions(el)) {
307
+ if (!optionDisabled(option))
308
+ add(name, optionValue(option));
309
+ }
310
+ continue;
311
+ }
312
+ if (type === 'checkbox' || type === 'radio') {
313
+ if (attr(el, 'checked') !== undefined) {
314
+ add(name, attr(el, 'value') ?? 'on');
315
+ }
316
+ continue;
317
+ }
318
+ if (type === 'file') {
319
+ // no file is ever chosen here: the field goes as one with no name
320
+ add(name, '', true);
321
+ continue;
322
+ }
323
+ if (type === 'hidden' && name.toLowerCase() === '_charset_') {
324
+ add(name, 'UTF-8');
325
+ continue;
326
+ }
327
+ if (tag === 'button' || type === 'submit' || type === 'reset') {
328
+ add(name, attr(el, 'value') ?? '');
329
+ continue;
330
+ }
331
+ add(name, controlValue(el, live));
332
+ const dirname = attr(el, 'dirname');
333
+ if (dirname &&
334
+ (tag === 'textarea' || type === 'text' || type === 'search')) {
335
+ add(dirname, attr(el, 'dir')?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr');
336
+ }
337
+ }
338
+ return out;
339
+ }
340
+ // --- encoding ---------------------------------------------------------------------
341
+ /** Line breaks as a form sends them: every lone CR and lone LF becomes
342
+ * CRLF. */
343
+ function crlf(text) {
344
+ return text.replace(/\r\n|\r|\n/g, '\r\n');
345
+ }
346
+ /** A string with no lone surrogates: what encoding it as UTF-8 needs, and
347
+ * what a USVString is. */
348
+ function wellFormed(text) {
349
+ return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, '�');
350
+ }
351
+ /**
352
+ * `application/x-www-form-urlencoded`'s byte serializer over UTF-8 (URL
353
+ * 5.2): letters, digits and `*-._` as they are, a space as `+`, everything
354
+ * else percent-encoded. `encodeURIComponent` spares `!~'()` besides, so
355
+ * those are encoded after it.
356
+ */
357
+ function formEncode(text) {
358
+ return encodeURIComponent(wellFormed(text))
359
+ .replace(/%20/g, '+')
360
+ .replace(/[!'()~]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
361
+ }
362
+ /** The entries as `application/x-www-form-urlencoded`: a query string, and
363
+ * a POST body of that type. */
364
+ export function urlencoded(entries) {
365
+ return entries
366
+ .map(([name, value]) => `${formEncode(crlf(name))}=${formEncode(crlf(value))}`)
367
+ .join('&');
368
+ }
369
+ /** The entries as `text/plain`: a line each, `name=value`, unescaped. */
370
+ export function plainText(entries) {
371
+ return entries
372
+ .map(([name, value]) => `${crlf(name)}=${crlf(value)}\r\n`)
373
+ .join('');
374
+ }
375
+ /** A name in a `Content-Disposition` header: its quote and its line breaks
376
+ * percent-encoded, as browsers write them (HTML 4.10.21.8). */
377
+ function dispositionName(name) {
378
+ return crlf(name).replace(/[\r\n"]/g, (c) => c === '"' ? '%22' : c === '\r' ? '%0D' : '%0A');
379
+ }
380
+ function multipart(entries, boundary) {
381
+ let body = '';
382
+ for (const { name, value, file } of entries) {
383
+ body += `--${boundary}\r\nContent-Disposition: form-data; name="${dispositionName(name)}"`;
384
+ body += file
385
+ ? `; filename="${dispositionName(value)}"\r\nContent-Type: application/octet-stream\r\n\r\n\r\n`
386
+ : `\r\n\r\n${crlf(value)}\r\n`;
387
+ }
388
+ return `${body}--${boundary}--\r\n`;
389
+ }
390
+ function makeBoundary() {
391
+ let tail = '';
392
+ while (tail.length < 24)
393
+ tail += Math.random().toString(36).slice(2);
394
+ return `----react-x11-form-${tail.slice(0, 24)}`;
395
+ }
396
+ /** `url` with its query replaced by `query` and its fragment kept — HTML's
397
+ * "mutate action URL". A GET always carries a query, if an empty one. */
398
+ function withQuery(url, query) {
399
+ const hash = url.indexOf('#');
400
+ const fragment = hash < 0 ? '' : url.slice(hash);
401
+ const head = hash < 0 ? url : url.slice(0, hash);
402
+ const q = head.indexOf('?');
403
+ return `${q < 0 ? head : head.slice(0, q)}?${query}${fragment}`;
404
+ }
405
+ // --- a submission -----------------------------------------------------------------
406
+ function enctypeOf(raw) {
407
+ const type = (raw ?? '').trim().toLowerCase();
408
+ return type === 'multipart/form-data' || type === 'text/plain'
409
+ ? type
410
+ : 'application/x-www-form-urlencoded';
411
+ }
412
+ /** The first `<base target>` in the document, or empty. */
413
+ function baseTarget(root) {
414
+ for (const el of elementsIn(root)) {
415
+ if (tagOf(el) === 'base' && attr(el, 'target') !== undefined) {
416
+ return attr(el, 'target') ?? '';
417
+ }
418
+ }
419
+ return '';
420
+ }
421
+ /**
422
+ * A form's submission by `submitter` (HTML 4.10.21.3): its entries, and the
423
+ * request they make. The button's `formaction`, `formmethod`, `formenctype`
424
+ * and `formtarget` win over the form's own. Null for `method="dialog"`,
425
+ * which closes a dialog rather than submitting anything, and for a
426
+ * submitter the form does not own.
427
+ */
428
+ export function formSubmission(form, submitter, context) {
429
+ if (submitter && formOwner(submitter) !== form)
430
+ return null;
431
+ const pick = (own, formName) => (submitter ? attr(submitter, own) : undefined) ?? attr(form, formName);
432
+ const methodName = (pick('formmethod', 'method') ?? '').trim().toLowerCase();
433
+ if (methodName === 'dialog')
434
+ return null;
435
+ const method = methodName === 'post' ? 'post' : 'get';
436
+ const written = pick('formaction', 'action') ?? '';
437
+ const documentUrl = context.documentUrl ?? context.base ?? '';
438
+ const action = written.trim()
439
+ ? resolveUrl(written, context.base)
440
+ : documentUrl;
441
+ const enctype = enctypeOf(pick('formenctype', 'enctype'));
442
+ const target = pick('formtarget', 'target') ?? baseTarget(rootOf(form));
443
+ const list = entriesOf(form, submitter, context.live, context.point);
444
+ const entries = list.map(({ name, value }) => [
445
+ name,
446
+ value,
447
+ ]);
448
+ const base = { form, submitter, enctype, entries, target };
449
+ if (method === 'get') {
450
+ // a `mailto:` form writes its entries as headers, which spell a space
451
+ // `%20` (HTML 4.10.21.3, "mail with headers")
452
+ const query = /^mailto:/i.test(action)
453
+ ? urlencoded(entries).replace(/\+/g, '%20')
454
+ : urlencoded(entries);
455
+ return {
456
+ ...base,
457
+ method,
458
+ url: withQuery(action, query),
459
+ body: null,
460
+ contentType: null,
461
+ };
462
+ }
463
+ if (enctype === 'multipart/form-data') {
464
+ const boundary = context.boundary ?? makeBoundary();
465
+ return {
466
+ ...base,
467
+ method,
468
+ url: action,
469
+ body: multipart(list, boundary),
470
+ contentType: `multipart/form-data; boundary=${boundary}`,
471
+ };
472
+ }
473
+ return {
474
+ ...base,
475
+ method,
476
+ url: action,
477
+ body: enctype === 'text/plain' ? plainText(entries) : urlencoded(entries),
478
+ contentType: enctype === 'text/plain' ? 'text/plain;charset=UTF-8' : enctype,
479
+ };
480
+ }
481
+ /**
482
+ * What Enter in a field submits (HTML 4.10.21.2, "implicit submission"): its
483
+ * form, by the form's default button — the first submit button it owns —
484
+ * or, where it has none, by no button at all, so long as the form has at
485
+ * most one field of the kinds that would make Enter ambiguous. Null where
486
+ * Enter submits nothing: no form, a disabled default button, or a form of
487
+ * several fields and no button.
488
+ */
489
+ export function implicitSubmission(field) {
490
+ const form = formOwner(field);
491
+ if (!form)
492
+ return null;
493
+ let fields = 0;
494
+ for (const el of controlsOf(form)) {
495
+ if (buttonType(el) === 'submit') {
496
+ return isDisabled(el) ? null : { form, submitter: el };
497
+ }
498
+ if (tagOf(el) === 'input' && BLOCKS_IMPLICIT.has(inputType(el))) {
499
+ fields += 1;
500
+ }
501
+ }
502
+ return fields > 1 ? null : { form, submitter: null };
503
+ }
504
+ // --- labels ------------------------------------------------------------------------
505
+ /** Whether an element can be what a `<label>` labels (HTML 4.10.2). */
506
+ function labelable(el) {
507
+ const tag = tagOf(el);
508
+ if (tag === 'input')
509
+ return inputType(el) !== 'hidden';
510
+ return (tag === 'button' ||
511
+ tag === 'select' ||
512
+ tag === 'textarea' ||
513
+ tag === 'meter' ||
514
+ tag === 'output' ||
515
+ tag === 'progress');
516
+ }
517
+ /**
518
+ * The control a `<label>` is for (HTML 4.10.4): the one its `for` names by
519
+ * id, where that is one a label can label, or else the first such control
520
+ * inside it. A press on the label is a press on that control.
521
+ */
522
+ export function labeledControl(label) {
523
+ const id = attr(label, 'for');
524
+ if (id !== undefined) {
525
+ const named = elementById(rootOf(label), id);
526
+ return named && labelable(named) ? named : null;
527
+ }
528
+ for (const el of elementsIn(label))
529
+ if (labelable(el))
530
+ return el;
531
+ return null;
532
+ }
533
+ // --- constraint validation ----------------------------------------------------------
534
+ /** HTML's valid email address (4.10.5.1.5), as the spec writes it. */
535
+ const EMAIL = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
536
+ /** The kinds of field `pattern`, `minlength` and `maxlength` apply to. */
537
+ const TEXTUAL = new Set(['text', 'search', 'url', 'tel', 'email', 'password']);
538
+ function absoluteUrl(text) {
539
+ if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(text))
540
+ return false;
541
+ const URLClass = globalThis.URL;
542
+ if (!URLClass)
543
+ return true;
544
+ try {
545
+ new URLClass(text);
546
+ return true;
547
+ }
548
+ catch {
549
+ return false;
550
+ }
551
+ }
552
+ /** A `pattern` as HTML compiles it: the whole value, in `v` mode — or `u`
553
+ * where the runtime has no `v` — and no constraint at all where it does
554
+ * not compile. */
555
+ function patternOf(source) {
556
+ for (const flags of ['v', 'u']) {
557
+ try {
558
+ return new RegExp(`^(?:${source})$`, flags);
559
+ }
560
+ catch {
561
+ // try the next, then give up
562
+ }
563
+ }
564
+ return null;
565
+ }
566
+ /** Whether a control's value is checked at all (HTML 4.10.21.2, "barred
567
+ * from constraint validation"). */
568
+ function validated(el) {
569
+ if (isDisabled(el) || inert(el))
570
+ return false;
571
+ const tag = tagOf(el);
572
+ if (tag === 'input') {
573
+ const type = inputType(el);
574
+ if (type === 'hidden' || type === 'reset' || type === 'button') {
575
+ return false;
576
+ }
577
+ if (buttonType(el) === 'submit')
578
+ return false;
579
+ return attr(el, 'readonly') === undefined;
580
+ }
581
+ if (tag === 'textarea')
582
+ return attr(el, 'readonly') === undefined;
583
+ return tag === 'select';
584
+ }
585
+ /**
586
+ * What is wrong with a control's value, in the words a browser uses, or
587
+ * null where nothing is: the constraints a static document can state —
588
+ * `required`, `minlength` and `maxlength`, `pattern`, `min` and `max`, and
589
+ * an email, a URL or a number that is not one (HTML 4.10.20). A length is
590
+ * checked only once the value has been typed, as HTML checks it; a
591
+ * document's own value is the author's to get right.
592
+ */
593
+ export function validationMessage(el, live) {
594
+ if (!validated(el))
595
+ return null;
596
+ const tag = tagOf(el);
597
+ const required = attr(el, 'required') !== undefined;
598
+ if (tag === 'select') {
599
+ if (!required)
600
+ return null;
601
+ const selected = selectedOptions(el);
602
+ const placeholder = !isMultiple(el) &&
603
+ selected.length === 1 &&
604
+ selected[0] === optionElements(el)[0] &&
605
+ selected[0].parent === el &&
606
+ optionValue(selected[0]) === '';
607
+ return !selected.length || placeholder
608
+ ? 'Please select an item in the list.'
609
+ : null;
610
+ }
611
+ const type = tag === 'input' ? inputType(el) : 'textarea';
612
+ if (type === 'checkbox') {
613
+ return required && attr(el, 'checked') === undefined
614
+ ? 'Please check this box if you want to proceed.'
615
+ : null;
616
+ }
617
+ if (type === 'radio') {
618
+ const group = [el, ...radioGroup(el)];
619
+ if (!group.some((r) => attr(r, 'required') !== undefined))
620
+ return null;
621
+ return group.some((r) => attr(r, 'checked') !== undefined)
622
+ ? null
623
+ : 'Please select one of these options.';
624
+ }
625
+ if (type === 'file')
626
+ return required ? 'Please select a file.' : null;
627
+ if (type === 'range' || type === 'color')
628
+ return null;
629
+ const typed = live?.(el);
630
+ const value = controlValue(el, live);
631
+ if (type === 'number' && typed !== undefined && typed.trim() && !value) {
632
+ return 'Please enter a number.';
633
+ }
634
+ if (!value)
635
+ return required ? 'Please fill out this field.' : null;
636
+ if (typed !== undefined && (type === 'textarea' || TEXTUAL.has(type))) {
637
+ const length = value.length;
638
+ const min = Number(attr(el, 'minlength'));
639
+ if (min > 0 && length < min) {
640
+ return `Please lengthen this text to ${min} characters or more (you are currently using ${length} characters).`;
641
+ }
642
+ const max = attr(el, 'maxlength');
643
+ if (max !== undefined && /^\d+$/.test(max.trim()) && length > Number(max)) {
644
+ return `Please shorten this text to ${Number(max)} characters or less (you are currently using ${length} characters).`;
645
+ }
646
+ }
647
+ if (type === 'email') {
648
+ const addresses = attr(el, 'multiple') !== undefined
649
+ ? value.split(',').map((a) => a.trim())
650
+ : [value];
651
+ if (!addresses.every((a) => EMAIL.test(a))) {
652
+ return 'Please enter an email address.';
653
+ }
654
+ }
655
+ if (type === 'url' && !absoluteUrl(value))
656
+ return 'Please enter a URL.';
657
+ const pattern = attr(el, 'pattern');
658
+ if (pattern !== undefined && TEXTUAL.has(type)) {
659
+ const re = patternOf(pattern);
660
+ const values = type === 'email' && attr(el, 'multiple') !== undefined
661
+ ? value.split(',').map((a) => a.trim())
662
+ : [value];
663
+ if (re && !values.every((v) => re.test(v))) {
664
+ const title = attr(el, 'title');
665
+ return title
666
+ ? `Please match the requested format:\n${title}`
667
+ : 'Please match the requested format.';
668
+ }
669
+ }
670
+ if (type === 'number') {
671
+ const n = Number(value);
672
+ const min = attr(el, 'min');
673
+ if (min !== undefined && FLOAT.test(min.trim()) && n < Number(min)) {
674
+ return `Value must be greater than or equal to ${min.trim()}.`;
675
+ }
676
+ const max = attr(el, 'max');
677
+ if (max !== undefined && FLOAT.test(max.trim()) && n > Number(max)) {
678
+ return `Value must be less than or equal to ${max.trim()}.`;
679
+ }
680
+ }
681
+ return null;
682
+ }
683
+ /**
684
+ * The first control a submission would be refused over, and why — HTML's
685
+ * interactive validation (4.10.21.3), which a form's `novalidate` or its
686
+ * submitter's `formnovalidate` turns off. Null where the form may go.
687
+ */
688
+ export function firstInvalid(form, submitter, live) {
689
+ if (attr(form, 'novalidate') !== undefined)
690
+ return null;
691
+ if (submitter && attr(submitter, 'formnovalidate') !== undefined) {
692
+ return null;
693
+ }
694
+ for (const el of controlsOf(form)) {
695
+ const message = validationMessage(el, live);
696
+ if (message)
697
+ return { element: el, message };
698
+ }
699
+ return null;
700
+ }
701
+ /**
702
+ * What a document's controls hold that its markup does not, and what their
703
+ * markup said before. One per `<Html>`, keyed by element, so a re-parse —
704
+ * new elements — starts clean.
705
+ *
706
+ * Typed text lives here rather than in the DOM because HTML's `value`
707
+ * attribute is the field's *default*: it is what a reset puts back, and
708
+ * what a `<textarea>` has is its content, which is not an attribute at all.
709
+ * A checkbox, a radio and a `<select>` do keep what they hold in the DOM —
710
+ * `checked` and `selected` — because `:checked` is a selector documents
711
+ * really use and it reads those; so for them this keeps the markup's
712
+ * attributes, from before the first change, for a reset.
713
+ */
714
+ export class FormState {
715
+ _typed = new WeakMap();
716
+ _markup = new WeakMap();
717
+ /** Typed text, where the field has any — the `live` a submission reads. */
718
+ typed(el) {
719
+ return this._typed.get(el);
720
+ }
721
+ /** What a text control holds now: typed, or its markup's. */
722
+ value(el) {
723
+ return controlValue(el, (e) => this._typed.get(e));
724
+ }
725
+ setTyped(el, text) {
726
+ this.remember(el);
727
+ this._typed.set(el, text);
728
+ }
729
+ /** Keep what `el`'s markup says, before a change to its attributes. */
730
+ remember(el) {
731
+ if (this._markup.has(el))
732
+ return;
733
+ const tag = tagOf(el);
734
+ const snapshot = {
735
+ attribs: {
736
+ checked: attr(el, 'checked'),
737
+ value: attr(el, 'value'),
738
+ },
739
+ };
740
+ if (tag === 'select') {
741
+ snapshot.selected = optionElements(el).filter((o) => attr(o, 'selected') !== undefined);
742
+ }
743
+ this._markup.set(el, snapshot);
744
+ }
745
+ /**
746
+ * Put every control `form` owns back as its markup had it (HTML 4.10.21.5,
747
+ * "reset"). True when anything changed — the widgets then mount again, to
748
+ * show it.
749
+ */
750
+ reset(form) {
751
+ let changed = false;
752
+ for (const el of controlsOf(form)) {
753
+ changed = this._typed.delete(el) || changed;
754
+ const snapshot = this._markup.get(el);
755
+ if (!snapshot)
756
+ continue;
757
+ this._markup.delete(el);
758
+ changed = true;
759
+ for (const [name, value] of Object.entries(snapshot.attribs)) {
760
+ if (value === undefined)
761
+ delete el.attribs[name];
762
+ else
763
+ el.attribs[name] = value;
764
+ }
765
+ if (snapshot.selected) {
766
+ for (const option of optionElements(el)) {
767
+ if (snapshot.selected.includes(option))
768
+ option.attribs.selected = '';
769
+ else
770
+ delete option.attribs.selected;
771
+ }
772
+ }
773
+ }
774
+ return changed;
775
+ }
776
+ }
777
+ /**
778
+ * The other radios in `radio`'s group (HTML 4.10.5.1.18): the inputs of type
779
+ * radio with the same name, in the same form — or, for one in no form, in
780
+ * no form either. Checking one unchecks these.
781
+ */
782
+ export function radioGroup(radio) {
783
+ const name = attr(radio, 'name');
784
+ if (!name)
785
+ return [];
786
+ const root = rootOf(radio);
787
+ const form = formOwner(radio, root);
788
+ const out = [];
789
+ const candidates = form ? controlsOf(form) : elementsIn(root);
790
+ for (const el of candidates) {
791
+ if (el !== radio &&
792
+ tagOf(el) === 'input' &&
793
+ inputType(el) === 'radio' &&
794
+ attr(el, 'name') === name &&
795
+ (form || formOwner(el, root) === null)) {
796
+ out.push(el);
797
+ }
798
+ }
799
+ return out;
800
+ }
801
+ //# sourceMappingURL=form.js.map