@react-x11/components 0.12.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.
- package/dist/html/controls.d.ts +12 -3
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +15 -55
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +134 -20
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +519 -122
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +29 -2
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +277 -9
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +14 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +2 -3
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.d.ts +11 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +59 -7
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +3 -1
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +25 -1
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +99 -6
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/form.d.ts +200 -0
- package/dist/html/form.d.ts.map +1 -0
- package/dist/html/form.js +801 -0
- package/dist/html/form.js.map +1 -0
- package/dist/html/index.d.ts +12 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +30 -301
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +5 -1
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +31 -1
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +52 -8
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/flex.js +4 -1
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +10 -0
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +87 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +510 -152
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +22 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +290 -20
- package/dist/html/paint.js.map +1 -1
- package/dist/html/widgets.d.ts +29 -0
- package/dist/html/widgets.d.ts.map +1 -0
- package/dist/html/widgets.js +620 -0
- package/dist/html/widgets.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/html/controls.ts +29 -49
- package/src/html/css/cascade.ts +591 -140
- package/src/html/css/parse.ts +324 -9
- package/src/html/css/style.ts +24 -3
- package/src/html/css/ua.ts +62 -8
- package/src/html/dom.ts +3 -1
- package/src/html/fonts.ts +116 -6
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -338
- package/src/html/layout/block.ts +3 -0
- package/src/html/layout/boxes.ts +78 -11
- package/src/html/layout/flex.ts +4 -1
- package/src/html/layout/inline.ts +10 -0
- package/src/html/node.ts +498 -135
- package/src/html/paint.ts +330 -29
- package/src/html/widgets.ts +735 -0
- package/src/index.ts +1 -0
package/src/html/index.ts
CHANGED
|
@@ -14,14 +14,7 @@
|
|
|
14
14
|
// is a property of the design rather than a setting.
|
|
15
15
|
import React from 'react';
|
|
16
16
|
import type { ReactElement, ReactNode } from 'react';
|
|
17
|
-
import {
|
|
18
|
-
Button,
|
|
19
|
-
Checkbox,
|
|
20
|
-
Radio,
|
|
21
|
-
RadioGroup,
|
|
22
|
-
Select,
|
|
23
|
-
useTheme,
|
|
24
|
-
} from 'react-x11';
|
|
17
|
+
import { useTheme } from 'react-x11';
|
|
25
18
|
import type { DrawnNode, MouseEvent as X11MouseEvent, Rect } from 'react-x11';
|
|
26
19
|
import { tint } from 'react-x11/style';
|
|
27
20
|
import type { Style } from 'react-x11/style';
|
|
@@ -31,18 +24,13 @@ import type {} from 'react-x11/jsx-runtime';
|
|
|
31
24
|
import { useLinkClicks, useSelectionMenu } from '../richtext/index.js';
|
|
32
25
|
import { useFontPrewarm } from '../internal/prewarm.js';
|
|
33
26
|
import { hx } from './hx.js';
|
|
34
|
-
import { attr, tagOf } from './dom.js';
|
|
35
27
|
import type { Document, Element } from './dom.js';
|
|
36
28
|
import { ELEMENT, HtmlViewNode, registerHtmlView } from './node.js';
|
|
37
29
|
import type { HtmlViewProps, ScriptRequest } from './node.js';
|
|
38
30
|
import type { RootLook } from './css/style.js';
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
selectedOption,
|
|
43
|
-
textareaValue,
|
|
44
|
-
} from './controls.js';
|
|
45
|
-
import type { BareField, ControlRect } from './controls.js';
|
|
31
|
+
import type { ControlRect } from './controls.js';
|
|
32
|
+
import type { FormSubmission } from './form.js';
|
|
33
|
+
import { useForms } from './widgets.js';
|
|
46
34
|
import type { ResourceRequest, ResourceResult } from './resources.js';
|
|
47
35
|
|
|
48
36
|
export {
|
|
@@ -53,6 +41,13 @@ export {
|
|
|
53
41
|
export type { HtmlViewProps, ScriptRequest } from './node.js';
|
|
54
42
|
export type { ResourceRequest, ResourceResult } from './resources.js';
|
|
55
43
|
export type { BareField, ControlRect } from './controls.js';
|
|
44
|
+
export type {
|
|
45
|
+
FormEnctype,
|
|
46
|
+
FormMethod,
|
|
47
|
+
FormSubmission,
|
|
48
|
+
SubmitContext,
|
|
49
|
+
} from './form.js';
|
|
50
|
+
export { formSubmission } from './form.js';
|
|
56
51
|
export type { ComputedStyle, RootLook } from './css/style.js';
|
|
57
52
|
export type {
|
|
58
53
|
AnyNode,
|
|
@@ -154,6 +149,15 @@ export interface HtmlProps {
|
|
|
154
149
|
* calls the handle's `refresh()`.
|
|
155
150
|
*/
|
|
156
151
|
onControlChange?: (element: Element, value: string | boolean) => void;
|
|
152
|
+
/**
|
|
153
|
+
* A form was submitted — a submit button pressed, or Enter in one of its
|
|
154
|
+
* text fields. Handed over as the request it makes: the method, the URL
|
|
155
|
+
* (resolved as `onLink`'s `href` is, and for a GET with the form's entries
|
|
156
|
+
* as its query), a POST's encoded body and its content type, and the
|
|
157
|
+
* entries themselves. Absent means submitting does nothing: this
|
|
158
|
+
* component never sends anything by itself, any more than it navigates.
|
|
159
|
+
*/
|
|
160
|
+
onSubmit?: (submission: FormSubmission) => void;
|
|
157
161
|
/** Base text style. Defaults: theme `fontSize` (14), `sans-serif`. */
|
|
158
162
|
fontSize?: number;
|
|
159
163
|
fontFamily?: string;
|
|
@@ -230,6 +234,10 @@ function deriveLook(
|
|
|
230
234
|
controlPadY: Number(theme.paddingY ?? 6),
|
|
231
235
|
controlBorder: Number(theme.borderWidth ?? 1),
|
|
232
236
|
controlRadius: Number(theme.radius ?? 4),
|
|
237
|
+
// not `fontSize` and `fontFamily`, which a host sets to the web's
|
|
238
|
+
// `medium` and the face it reads pages in
|
|
239
|
+
controlFontSize: Number(theme.fontSize ?? 14),
|
|
240
|
+
controlFontFamily: String(theme.fontFamily ?? 'sans-serif'),
|
|
233
241
|
};
|
|
234
242
|
}
|
|
235
243
|
|
|
@@ -285,14 +293,27 @@ export function Html(props: HtmlProps): ReactElement {
|
|
|
285
293
|
onScript,
|
|
286
294
|
onDocument,
|
|
287
295
|
onControlChange,
|
|
296
|
+
onSubmit,
|
|
288
297
|
style,
|
|
289
298
|
} = props;
|
|
290
299
|
|
|
291
300
|
const theme = useTheme() as unknown as Record<string, unknown>;
|
|
292
301
|
const links = useLinkClicks(onLink);
|
|
293
|
-
const buttons = useButtonPresses(onControlChange);
|
|
294
302
|
const menu = useSelectionMenu(selectable);
|
|
295
303
|
|
|
304
|
+
// The element, for the document's base when a form is submitted, as well
|
|
305
|
+
// as wherever the application's `ref` wants it.
|
|
306
|
+
const viewNode = React.useRef<HtmlViewNode | null>(null);
|
|
307
|
+
const outerRef = props.ref;
|
|
308
|
+
const viewRef = React.useCallback(
|
|
309
|
+
(node: HtmlViewNode | null) => {
|
|
310
|
+
viewNode.current = node;
|
|
311
|
+
if (typeof outerRef === 'function') outerRef(node);
|
|
312
|
+
else if (outerRef) (outerRef as React.RefObject<unknown>).current = node;
|
|
313
|
+
},
|
|
314
|
+
[outerRef],
|
|
315
|
+
);
|
|
316
|
+
|
|
296
317
|
const look = React.useMemo(
|
|
297
318
|
() => deriveLook(theme, props),
|
|
298
319
|
[theme, props.fontSize, props.fontFamily, props.monoFamily],
|
|
@@ -305,6 +326,13 @@ export function Html(props: HtmlProps): ReactElement {
|
|
|
305
326
|
const documentRef = React.useRef<Document | null>(null);
|
|
306
327
|
const [controls, setControls] = React.useState<ControlRect[]>([]);
|
|
307
328
|
const [domRevision, setDomRevision] = React.useState(0);
|
|
329
|
+
const forms = useForms({
|
|
330
|
+
view: viewNode,
|
|
331
|
+
baseUrl,
|
|
332
|
+
onControlChange,
|
|
333
|
+
onSubmit,
|
|
334
|
+
touch: () => setDomRevision((n) => n + 1),
|
|
335
|
+
});
|
|
308
336
|
|
|
309
337
|
const handleDocument = React.useCallback(
|
|
310
338
|
(doc: Document) => {
|
|
@@ -338,7 +366,7 @@ export function Html(props: HtmlProps): ReactElement {
|
|
|
338
366
|
onDocument: handleDocument,
|
|
339
367
|
onControls: handleControls,
|
|
340
368
|
domRevision,
|
|
341
|
-
ref:
|
|
369
|
+
ref: viewRef as React.Ref<unknown>,
|
|
342
370
|
// grown with the component, where an application grows it: the root's
|
|
343
371
|
// background covers the whole of it, as a page's covers the window
|
|
344
372
|
style: { alignSelf: 'stretch', flexGrow: 1 },
|
|
@@ -347,16 +375,7 @@ export function Html(props: HtmlProps): ReactElement {
|
|
|
347
375
|
const children: ReactNode[] = [
|
|
348
376
|
h(ELEMENT, { key: 'view', ...viewProps } as Record<string, unknown>),
|
|
349
377
|
];
|
|
350
|
-
|
|
351
|
-
children.push(
|
|
352
|
-
renderControl(
|
|
353
|
-
rect,
|
|
354
|
-
look,
|
|
355
|
-
() => setDomRevision((n) => n + 1),
|
|
356
|
-
onControlChange,
|
|
357
|
-
),
|
|
358
|
-
);
|
|
359
|
-
}
|
|
378
|
+
children.push(...forms.render(controls, look));
|
|
360
379
|
|
|
361
380
|
const rootStyle: Style = { flexDirection: 'column', position: 'relative' };
|
|
362
381
|
return hx(
|
|
@@ -374,11 +393,11 @@ export function Html(props: HtmlProps): ReactElement {
|
|
|
374
393
|
...links,
|
|
375
394
|
onMouseDown: (ev: X11MouseEvent<DrawnNode>) => {
|
|
376
395
|
links.onMouseDown(ev);
|
|
377
|
-
|
|
396
|
+
forms.onMouseDown(ev);
|
|
378
397
|
},
|
|
379
398
|
onMouseUp: (ev: X11MouseEvent<DrawnNode>) => {
|
|
380
399
|
links.onMouseUp(ev);
|
|
381
|
-
|
|
400
|
+
forms.onMouseUp(ev);
|
|
382
401
|
},
|
|
383
402
|
...menu,
|
|
384
403
|
'data-testname': props['data-testname'],
|
|
@@ -428,314 +447,6 @@ export function useHtmlHandle(): HtmlHandle & { ref: React.Ref<unknown> } {
|
|
|
428
447
|
);
|
|
429
448
|
}
|
|
430
449
|
|
|
431
|
-
/**
|
|
432
|
-
* A press and a release on the same `<button>` the document draws, reported
|
|
433
|
-
* as a pressed widget reports one: `onControlChange` with the button and its
|
|
434
|
-
* `value`. A `<button>` is laid out and drawn like any box (the UA sheet's
|
|
435
|
-
* `button` rule) — its content is the page's, and most of the buttons on
|
|
436
|
-
* the web are an icon or a pill of a page's own design — so it is the
|
|
437
|
-
* element that finds out it was pressed, not a widget. A disabled one, or
|
|
438
|
-
* a press that lands in a link inside one, is not a press of it.
|
|
439
|
-
*/
|
|
440
|
-
function useButtonPresses(onControlChange: HtmlProps['onControlChange']): {
|
|
441
|
-
onMouseDown: (ev: X11MouseEvent<DrawnNode>) => void;
|
|
442
|
-
onMouseUp: (ev: X11MouseEvent<DrawnNode>) => void;
|
|
443
|
-
} {
|
|
444
|
-
const press = React.useRef<Element | null>(null);
|
|
445
|
-
const buttonAt = (ev: X11MouseEvent<DrawnNode>): Element | null => {
|
|
446
|
-
const target = ev.target as {
|
|
447
|
-
elementAtPoint?: (x: number, y: number) => Element | null;
|
|
448
|
-
} | null;
|
|
449
|
-
let node =
|
|
450
|
-
typeof target?.elementAtPoint === 'function'
|
|
451
|
-
? target.elementAtPoint(ev.x, ev.y)
|
|
452
|
-
: null;
|
|
453
|
-
for (
|
|
454
|
-
;
|
|
455
|
-
node;
|
|
456
|
-
node = node.parent?.type === 'tag' ? (node.parent as Element) : null
|
|
457
|
-
) {
|
|
458
|
-
if (tagOf(node) === 'a' && attr(node, 'href') !== undefined) return null;
|
|
459
|
-
if (tagOf(node) === 'button') {
|
|
460
|
-
return attr(node, 'disabled') === undefined ? node : null;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
return null;
|
|
464
|
-
};
|
|
465
|
-
return {
|
|
466
|
-
onMouseDown: (ev) => {
|
|
467
|
-
press.current = onControlChange && ev.button === 1 ? buttonAt(ev) : null;
|
|
468
|
-
},
|
|
469
|
-
onMouseUp: (ev) => {
|
|
470
|
-
const pressed = press.current;
|
|
471
|
-
press.current = null;
|
|
472
|
-
if (!pressed || !onControlChange || ev.button !== 1) return;
|
|
473
|
-
if (buttonAt(ev) !== pressed) return;
|
|
474
|
-
onControlChange(pressed, attr(pressed, 'value') ?? '');
|
|
475
|
-
},
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// --- the widgets ------------------------------------------------------------
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* One form control, as a real widget at the rectangle layout reserved for it.
|
|
483
|
-
*
|
|
484
|
-
* Every one of these is a **core** widget rather than something drawn here:
|
|
485
|
-
* a `<select>` in a document drops the same menu as a `<Select>` in the
|
|
486
|
-
* window around it, a `<textinput>` gets the same caret, the same IME and the
|
|
487
|
-
* same edit menu, and all of them join the window's focus order. The
|
|
488
|
-
* alternative — drawing them in the paint pass — would be a picture of a form.
|
|
489
|
-
*/
|
|
490
|
-
function renderControl(
|
|
491
|
-
rect: ControlRect,
|
|
492
|
-
look: RootLook,
|
|
493
|
-
touch: () => void,
|
|
494
|
-
onChange: HtmlProps['onControlChange'],
|
|
495
|
-
): ReactNode {
|
|
496
|
-
const el = rect.element;
|
|
497
|
-
const key = `${rect.kind}:${rect.x},${rect.y}`;
|
|
498
|
-
const disabled = attr(el, 'disabled') !== undefined;
|
|
499
|
-
const readOnly = attr(el, 'readonly') !== undefined;
|
|
500
|
-
// a field whose box the document draws takes its content box
|
|
501
|
-
const at = rect.bare ?? rect;
|
|
502
|
-
const frame: Style = {
|
|
503
|
-
position: 'absolute',
|
|
504
|
-
left: Math.round(at.x),
|
|
505
|
-
top: Math.round(at.y),
|
|
506
|
-
width: Math.round(at.width),
|
|
507
|
-
height: Math.round(at.height),
|
|
508
|
-
// core's `opacity` is CSS's: the widget faded as a group, and at 0 not
|
|
509
|
-
// drawn and still hit
|
|
510
|
-
...(rect.opacity !== undefined && { opacity: rect.opacity }),
|
|
511
|
-
};
|
|
512
|
-
const field = rect.bare ? bareField(rect.bare) : fieldChrome(look);
|
|
513
|
-
const report = (value: string | boolean): void => {
|
|
514
|
-
onChange?.(el, value);
|
|
515
|
-
touch();
|
|
516
|
-
};
|
|
517
|
-
// A text edit does NOT touch: the value lives in the widget and is echoed
|
|
518
|
-
// into the DOM attribute, and neither changes any box — while `touch()`
|
|
519
|
-
// would re-run the cascade and relayout the whole document *per
|
|
520
|
-
// keystroke*. This also matches HTML's own semantics: typing updates the
|
|
521
|
-
// value property, not the attribute selectors match against. The
|
|
522
|
-
// checkables keep the full touch, because `[checked]` is a selector
|
|
523
|
-
// documents really use.
|
|
524
|
-
const reportText = (value: string): void => {
|
|
525
|
-
onChange?.(el, value);
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
let widget: ReactNode;
|
|
529
|
-
switch (rect.kind) {
|
|
530
|
-
case 'checkbox':
|
|
531
|
-
widget = h(Checkbox, {
|
|
532
|
-
checked: attr(el, 'checked') !== undefined,
|
|
533
|
-
disabled,
|
|
534
|
-
// the element's whole box takes the press, as it does in a browser:
|
|
535
|
-
// a page that sizes one over its label, invisible, means the label
|
|
536
|
-
style: { width: '100%', height: '100%' },
|
|
537
|
-
onChange: (ev) => {
|
|
538
|
-
const next = ev.value;
|
|
539
|
-
if (next) el.attribs.checked = '';
|
|
540
|
-
else delete el.attribs.checked;
|
|
541
|
-
report(next);
|
|
542
|
-
},
|
|
543
|
-
});
|
|
544
|
-
break;
|
|
545
|
-
case 'radio': {
|
|
546
|
-
// Core's radio is a group member and HTML's is a free-standing input
|
|
547
|
-
// that happens to share a `name`. Each one is therefore its own
|
|
548
|
-
// one-member `RadioGroup`, and the exclusivity that makes it a group
|
|
549
|
-
// is done where HTML actually keeps it: in the DOM.
|
|
550
|
-
const value = attr(el, 'value') ?? 'on';
|
|
551
|
-
widget = h(
|
|
552
|
-
RadioGroup,
|
|
553
|
-
{
|
|
554
|
-
value: attr(el, 'checked') !== undefined ? value : undefined,
|
|
555
|
-
onChange: () => {
|
|
556
|
-
clearRadioGroup(el);
|
|
557
|
-
el.attribs.checked = '';
|
|
558
|
-
report(value);
|
|
559
|
-
},
|
|
560
|
-
},
|
|
561
|
-
h(Radio, { key: 'r', value, disabled }),
|
|
562
|
-
);
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
case 'button':
|
|
566
|
-
widget = h(Button, {
|
|
567
|
-
label: buttonLabel(el),
|
|
568
|
-
disabled,
|
|
569
|
-
style: { width: '100%', height: '100%' },
|
|
570
|
-
onPress: () => report(attr(el, 'value') ?? ''),
|
|
571
|
-
});
|
|
572
|
-
break;
|
|
573
|
-
case 'select': {
|
|
574
|
-
const options = optionsOf(el);
|
|
575
|
-
widget = h(Select, {
|
|
576
|
-
options: options.map((o) => ({ value: o.value, label: o.label })),
|
|
577
|
-
value: selectedOption(el) ?? undefined,
|
|
578
|
-
disabled,
|
|
579
|
-
style: rect.bare
|
|
580
|
-
? [BARE_TRIGGER, { width: '100%', height: '100%' }]
|
|
581
|
-
: { width: '100%', height: '100%' },
|
|
582
|
-
// the slots choose the drawn trigger on every backend, and put the
|
|
583
|
-
// caption and the arrow in the page's ink
|
|
584
|
-
...(rect.bare && {
|
|
585
|
-
labelStyle: {
|
|
586
|
-
color: rect.bare.color,
|
|
587
|
-
fontFamily: rect.bare.fontFamily,
|
|
588
|
-
fontSize: rect.bare.fontSize,
|
|
589
|
-
},
|
|
590
|
-
chevronStyle: rect.bare.chevron
|
|
591
|
-
? { color: rect.bare.color }
|
|
592
|
-
: { display: 'none' },
|
|
593
|
-
}),
|
|
594
|
-
onChange: (ev) => {
|
|
595
|
-
const next = String(ev.value ?? '');
|
|
596
|
-
setSelectedOption(el, next);
|
|
597
|
-
report(next);
|
|
598
|
-
},
|
|
599
|
-
});
|
|
600
|
-
break;
|
|
601
|
-
}
|
|
602
|
-
case 'textarea':
|
|
603
|
-
// Uncontrolled on purpose: the widget owns the live text the way a
|
|
604
|
-
// browser's does, and a re-render from any other cause remounts it
|
|
605
|
-
// with whatever was last echoed into the DOM.
|
|
606
|
-
widget = hx('textarea', {
|
|
607
|
-
defaultValue: textareaValue(el),
|
|
608
|
-
style: [field, { width: '100%', height: '100%' }],
|
|
609
|
-
onChange: readOnly ? undefined : (ev) => reportText(ev.value),
|
|
610
|
-
});
|
|
611
|
-
break;
|
|
612
|
-
case 'input': {
|
|
613
|
-
const type = (attr(el, 'type') ?? 'text').toLowerCase();
|
|
614
|
-
widget = hx('textinput', {
|
|
615
|
-
defaultValue: attr(el, 'value') ?? '',
|
|
616
|
-
placeholder: attr(el, 'placeholder'),
|
|
617
|
-
// Core's word for a password field: nothing in it reaches a
|
|
618
|
-
// selection, PRIMARY included.
|
|
619
|
-
sensitive: type === 'password',
|
|
620
|
-
style: [field, { width: '100%', height: '100%' }],
|
|
621
|
-
onChange: readOnly
|
|
622
|
-
? undefined
|
|
623
|
-
: (ev) => {
|
|
624
|
-
el.attribs.value = ev.value;
|
|
625
|
-
reportText(ev.value);
|
|
626
|
-
},
|
|
627
|
-
});
|
|
628
|
-
break;
|
|
629
|
-
}
|
|
630
|
-
default:
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
return hx('box', { key, style: frame, selectable: false }, widget);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* The chrome a text field needs.
|
|
638
|
-
*
|
|
639
|
-
* `<textinput>` and `<textarea>` are core *elements* rather than components,
|
|
640
|
-
* so they draw no frame of their own — an application supplies one, which is
|
|
641
|
-
* why core's own `<Button>` and `<Select>` are components and these are not.
|
|
642
|
-
* The values are the palette's, so a field in a document and a `<Select>`
|
|
643
|
-
* beside it are the same height with the same corner and the same edge.
|
|
644
|
-
*/
|
|
645
|
-
function fieldChrome(look: RootLook): Style {
|
|
646
|
-
return {
|
|
647
|
-
backgroundColor: look.surface,
|
|
648
|
-
borderWidth: look.controlBorder,
|
|
649
|
-
borderColor: look.borderColor,
|
|
650
|
-
borderRadius: look.controlRadius,
|
|
651
|
-
paddingLeft: 6,
|
|
652
|
-
paddingRight: 6,
|
|
653
|
-
color: look.color,
|
|
654
|
-
fontFamily: look.fontFamily,
|
|
655
|
-
fontSize: look.fontSize,
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* A text field whose box the author styled: the document draws the border
|
|
661
|
-
* and the background, so the widget draws neither, and its text is the
|
|
662
|
-
* element's colour and font, which the author chose to go on that
|
|
663
|
-
* background, rather than the theme's.
|
|
664
|
-
*/
|
|
665
|
-
function bareField(bare: BareField): Style {
|
|
666
|
-
return {
|
|
667
|
-
backgroundColor: 'transparent',
|
|
668
|
-
borderWidth: 0,
|
|
669
|
-
borderRadius: 0,
|
|
670
|
-
paddingLeft: 0,
|
|
671
|
-
paddingRight: 0,
|
|
672
|
-
color: bare.color,
|
|
673
|
-
fontFamily: bare.fontFamily,
|
|
674
|
-
fontSize: bare.fontSize,
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* The trigger of a `<select>` whose box the page styled: no frame, no fill
|
|
680
|
-
* and none of its own insets, and no wash under the pointer, since the box
|
|
681
|
-
* it would tint is the document's. Core's focus ring still marks it for the
|
|
682
|
-
* keyboard.
|
|
683
|
-
*/
|
|
684
|
-
const BARE_TRIGGER: Style = {
|
|
685
|
-
paddingTop: 0,
|
|
686
|
-
paddingBottom: 0,
|
|
687
|
-
paddingLeft: 0,
|
|
688
|
-
paddingRight: 0,
|
|
689
|
-
borderWidth: 0,
|
|
690
|
-
borderRadius: 0,
|
|
691
|
-
backgroundColor: 'transparent',
|
|
692
|
-
':hover': { backgroundColor: 'transparent' },
|
|
693
|
-
':active': { backgroundColor: 'transparent' },
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
function clearRadioGroup(el: Element): void {
|
|
697
|
-
const name = attr(el, 'name');
|
|
698
|
-
if (!name) return;
|
|
699
|
-
let root: Element | null = el;
|
|
700
|
-
while (root?.parent && root.parent.type === 'tag')
|
|
701
|
-
root = root.parent as Element;
|
|
702
|
-
if (!root) return;
|
|
703
|
-
const walk = (node: Element): void => {
|
|
704
|
-
for (const child of node.children) {
|
|
705
|
-
if (child.type !== 'tag') continue;
|
|
706
|
-
const tag = child.name.toLowerCase();
|
|
707
|
-
if (
|
|
708
|
-
tag === 'input' &&
|
|
709
|
-
(child.attribs.type ?? '').toLowerCase() === 'radio' &&
|
|
710
|
-
child.attribs.name === name
|
|
711
|
-
) {
|
|
712
|
-
delete child.attribs.checked;
|
|
713
|
-
}
|
|
714
|
-
walk(child);
|
|
715
|
-
}
|
|
716
|
-
};
|
|
717
|
-
walk(root);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
function setSelectedOption(el: Element, value: string): void {
|
|
721
|
-
const walk = (node: Element): void => {
|
|
722
|
-
for (const child of node.children) {
|
|
723
|
-
if (child.type !== 'tag') continue;
|
|
724
|
-
if (child.name.toLowerCase() === 'option') {
|
|
725
|
-
let label = '';
|
|
726
|
-
for (const kid of child.children) {
|
|
727
|
-
if (kid.type === 'text') label += kid.data;
|
|
728
|
-
}
|
|
729
|
-
const own = child.attribs.value ?? label.trim();
|
|
730
|
-
if (own === value) child.attribs.selected = '';
|
|
731
|
-
else delete child.attribs.selected;
|
|
732
|
-
}
|
|
733
|
-
walk(child);
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
walk(el);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
450
|
declare module 'react-x11/jsx-runtime' {
|
|
740
451
|
namespace JSX {
|
|
741
452
|
interface IntrinsicElements {
|
package/src/html/layout/block.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
BOX_RAISES,
|
|
39
39
|
Box,
|
|
40
40
|
CLAMPED,
|
|
41
|
+
CLEARED_FROM,
|
|
41
42
|
CUT_BLOCKS,
|
|
42
43
|
GRID_TRACKS,
|
|
43
44
|
FIRST_LINE,
|
|
@@ -927,6 +928,8 @@ function layoutChildren(
|
|
|
927
928
|
}
|
|
928
929
|
first = false;
|
|
929
930
|
const moved = childY !== at;
|
|
931
|
+
if (moved) CLEARED_FROM.set(child, at);
|
|
932
|
+
else CLEARED_FROM.delete(child);
|
|
930
933
|
if (!moved && collapsesThrough(child)) {
|
|
931
934
|
// nothing in it parts its margins: they and the ones either side of
|
|
932
935
|
// it are one (CSS 2.1 8.3.1), still hanging for what comes next —
|
package/src/html/layout/boxes.ts
CHANGED
|
@@ -28,7 +28,12 @@ import {
|
|
|
28
28
|
NON_RENDERED,
|
|
29
29
|
tagOf,
|
|
30
30
|
} from '../dom.js';
|
|
31
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
Cascade,
|
|
33
|
+
FirstLetterRules,
|
|
34
|
+
KeptStyles,
|
|
35
|
+
SelectionStyle,
|
|
36
|
+
} from '../css/cascade.js';
|
|
32
37
|
import type { CollapsedTable } from './collapse.js';
|
|
33
38
|
import { quoteAt } from '../css/content.js';
|
|
34
39
|
import type { CounterStyles } from '../css/counter-styles.js';
|
|
@@ -556,6 +561,10 @@ export type FirstLineStyler = (
|
|
|
556
561
|
parent: ComputedStyle,
|
|
557
562
|
) => ComputedStyle;
|
|
558
563
|
|
|
564
|
+
/** The `::selection` of the element a box's text is in, or null for the
|
|
565
|
+
* palette's highlight (`Cascade.selectionStyle`). */
|
|
566
|
+
export type SelectionStyler = (box: Box | null) => SelectionStyle | null;
|
|
567
|
+
|
|
559
568
|
/** The url of an image generated content put in a pseudo-element: it has
|
|
560
569
|
* no element of its own to name it (`imageUrlOf`). */
|
|
561
570
|
export const CONTENT_IMAGES = new WeakMap<Box, string>();
|
|
@@ -641,6 +650,12 @@ export const PAINT_ORDER = new WeakMap<Box, Box[]>();
|
|
|
641
650
|
* a browser's does (`elementRect`). */
|
|
642
651
|
export const CUT_BLOCKS = new WeakMap<Box, Box[]>();
|
|
643
652
|
|
|
653
|
+
/** Where a block that clearance moved below the floats would have stood
|
|
654
|
+
* without it, its border edge's top (CSS 2.1 9.5.2): a browser's
|
|
655
|
+
* fragment of an inline box around such a block starts there, and takes
|
|
656
|
+
* the clearance in (`elementRect`). Only the blocks that had clearance. */
|
|
657
|
+
export const CLEARED_FROM = new WeakMap<Box, number>();
|
|
658
|
+
|
|
644
659
|
/** The element a pseudo-element's box is generated from, which has no
|
|
645
660
|
* element of its own (`Box.pseudo`): whose the images its styles name are,
|
|
646
661
|
* when the host is asked for them. */
|
|
@@ -673,6 +688,10 @@ export interface BoxTree {
|
|
|
673
688
|
controls: Box[];
|
|
674
689
|
/** Every box carrying an `href`, for click and hover. */
|
|
675
690
|
links: Box[];
|
|
691
|
+
/** The style of an anonymous box under a parent's (`anonymousStyles`):
|
|
692
|
+
* the one the fix-up gave each it made, and what such a box takes when
|
|
693
|
+
* its parent's style changes where it is (`HtmlViewNode._hoverInPlace`). */
|
|
694
|
+
anonymous: AnonymousStyle;
|
|
676
695
|
/** Every box with a `background-image`, a border image or a mask image,
|
|
677
696
|
* an element's or a pseudo-element's, for the host to be asked for: a
|
|
678
697
|
* document has a handful, and finding them was a walk over every box
|
|
@@ -694,6 +713,10 @@ export interface BoxTree {
|
|
|
694
713
|
/** Each box's style on a first line (`FirstLineStyler`), where any
|
|
695
714
|
* block has a `::first-line`. */
|
|
696
715
|
firstLineStyler: FirstLineStyler | null;
|
|
716
|
+
/** Each box's `::selection` (`SelectionStyler`), where any rule styles
|
|
717
|
+
* one: worked out as a selection is first painted over it, not as the
|
|
718
|
+
* tree is built, since most builds are never selected in. */
|
|
719
|
+
selectionStyler: SelectionStyler | null;
|
|
697
720
|
/** Whether a float or an out-of-flow box sits in an inline box: where
|
|
698
721
|
* none does, layout looks for them among a block's own children and
|
|
699
722
|
* goes through no inline box to find them. */
|
|
@@ -714,6 +737,9 @@ export interface BoxTree {
|
|
|
714
737
|
|
|
715
738
|
export interface BuildOptions {
|
|
716
739
|
cascade: Cascade;
|
|
740
|
+
/** The styles of the tree this one replaces, where only some elements'
|
|
741
|
+
* can have changed since (`Cascade.beginSharing`). */
|
|
742
|
+
kept?: KeptStyles | null;
|
|
717
743
|
/** Device pixels per CSS pixel. An image's pixels and a `width="600"`
|
|
718
744
|
* attribute are CSS pixels; every box is device, so both are multiplied
|
|
719
745
|
* on the way in. Default 1. */
|
|
@@ -813,7 +839,7 @@ class Builder {
|
|
|
813
839
|
|
|
814
840
|
run(root: Element): BoxTree {
|
|
815
841
|
const cascade = this._options.cascade;
|
|
816
|
-
cascade.beginSharing();
|
|
842
|
+
cascade.beginSharing(this._options.kept ?? null);
|
|
817
843
|
const { style: rootStyle, html: impliedHtml } = cascade.rootStyle(
|
|
818
844
|
hasBody(root),
|
|
819
845
|
hasHtml(root),
|
|
@@ -832,10 +858,13 @@ class Builder {
|
|
|
832
858
|
// it carries no margins of its own and cannot collapse with anything.
|
|
833
859
|
this._children(root, rootBox, rootStyle, false, null, ROOT_SHARE_KEY);
|
|
834
860
|
this._endLine();
|
|
835
|
-
|
|
861
|
+
cascade.endSharing();
|
|
862
|
+
const anonymous = anonymousStyles(cascade.initial);
|
|
863
|
+
fixUp(rootBox, anonymous);
|
|
836
864
|
assignSubtreeRanges(rootBox);
|
|
837
865
|
return {
|
|
838
866
|
root: rootBox,
|
|
867
|
+
anonymous,
|
|
839
868
|
text: this._chunks.join(''),
|
|
840
869
|
textBoxes: this._textBoxes,
|
|
841
870
|
textStyles: this._textStyles,
|
|
@@ -848,6 +877,7 @@ class Builder {
|
|
|
848
877
|
negative: this._negative,
|
|
849
878
|
firstLine: this._firstLine,
|
|
850
879
|
firstLineStyler: this._firstLine ? this._firstLineStyler() : null,
|
|
880
|
+
selectionStyler: cascade.hasSelection ? this._selectionStyler() : null,
|
|
851
881
|
nestedOutOfLine: this._nestedOutOfLine,
|
|
852
882
|
movedInline: this._movedInline,
|
|
853
883
|
clipText: this._clipText,
|
|
@@ -855,6 +885,30 @@ class Builder {
|
|
|
855
885
|
};
|
|
856
886
|
}
|
|
857
887
|
|
|
888
|
+
/** `BoxTree.selectionStyler`, over this build's cascade: an element's
|
|
889
|
+
* over its parent's, each once. A box with no element of its own — text,
|
|
890
|
+
* an anonymous box — is its nearest element's. */
|
|
891
|
+
private _selectionStyler(): SelectionStyler {
|
|
892
|
+
const cascade = this._options.cascade;
|
|
893
|
+
const styles = this._styles;
|
|
894
|
+
const known = new Map<Element, SelectionStyle | null>();
|
|
895
|
+
const of = (el: Element): SelectionStyle | null => {
|
|
896
|
+
let found = known.get(el);
|
|
897
|
+
if (found !== undefined) return found;
|
|
898
|
+
// the parser keeps a document to 256 elements deep (`dom.ts`)
|
|
899
|
+
const parent = isElement(el.parent) ? of(el.parent) : null;
|
|
900
|
+
const own = styles.get(el)?.style;
|
|
901
|
+
found = own ? cascade.selectionStyle(el, own, parent) : parent;
|
|
902
|
+
known.set(el, found);
|
|
903
|
+
return found;
|
|
904
|
+
};
|
|
905
|
+
return (box) => {
|
|
906
|
+
let at = box;
|
|
907
|
+
while (at && !at.el) at = at.parent;
|
|
908
|
+
return at?.el ? of(at.el) : null;
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
858
912
|
/** `BoxTree.firstLineStyler`, over this build's cascade. */
|
|
859
913
|
private _firstLineStyler(): FirstLineStyler {
|
|
860
914
|
const cascade = this._options.cascade;
|
|
@@ -1024,7 +1078,9 @@ class Builder {
|
|
|
1024
1078
|
}
|
|
1025
1079
|
|
|
1026
1080
|
// an `<object>` is its image once it has one, and its content until;
|
|
1027
|
-
// an `<embed>` its image, and a `<video>` its poster, or a frame
|
|
1081
|
+
// an `<embed>` its image, and a `<video>` its poster, or a frame; an
|
|
1082
|
+
// image button its image, and a button saying what it is for until it
|
|
1083
|
+
// has one, so it can be pressed either way
|
|
1028
1084
|
const replaced =
|
|
1029
1085
|
tag === 'object'
|
|
1030
1086
|
? this._options.imageSize(el)
|
|
@@ -1032,7 +1088,11 @@ class Builder {
|
|
|
1032
1088
|
: 'none'
|
|
1033
1089
|
: (tag === 'embed' || tag === 'video') && this._options.imageSize(el)
|
|
1034
1090
|
? 'image'
|
|
1035
|
-
:
|
|
1091
|
+
: isImageButton(el, tag)
|
|
1092
|
+
? this._options.imageSize(el)
|
|
1093
|
+
? 'image'
|
|
1094
|
+
: 'button'
|
|
1095
|
+
: replacedKind(el, tag);
|
|
1036
1096
|
if (replaced !== 'none') {
|
|
1037
1097
|
this._replaced(el, tag, replaced, style, into);
|
|
1038
1098
|
return;
|
|
@@ -2359,6 +2419,13 @@ function setIntrinsics(box: Box, size: IntrinsicSize, scale: number): void {
|
|
|
2359
2419
|
};
|
|
2360
2420
|
}
|
|
2361
2421
|
|
|
2422
|
+
/** An `<input type=image>`: a picture that submits its form. */
|
|
2423
|
+
function isImageButton(el: Element, tag: string): boolean {
|
|
2424
|
+
return (
|
|
2425
|
+
tag === 'input' && (attr(el, 'type') ?? '').trim().toLowerCase() === 'image'
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2362
2429
|
function replacedKind(el: Element, tag: string): ReplacedKind {
|
|
2363
2430
|
switch (tag) {
|
|
2364
2431
|
case 'img':
|
|
@@ -2662,24 +2729,24 @@ function namesImages(style: ComputedStyle): boolean {
|
|
|
2662
2729
|
* paragraph after it the height of the div further down. One per parent
|
|
2663
2730
|
* style, which the cascade already shares between elements.
|
|
2664
2731
|
*/
|
|
2665
|
-
type AnonymousStyle = (
|
|
2666
|
-
parent:
|
|
2732
|
+
export type AnonymousStyle = (
|
|
2733
|
+
parent: ComputedStyle,
|
|
2667
2734
|
display: ComputedStyle['display'],
|
|
2668
2735
|
) => ComputedStyle;
|
|
2669
2736
|
|
|
2670
2737
|
function anonymousStyles(initial: ComputedStyle): AnonymousStyle {
|
|
2671
2738
|
const made = new WeakMap<ComputedStyle, Map<string, ComputedStyle>>();
|
|
2672
2739
|
return (parent, display) => {
|
|
2673
|
-
let byDisplay = made.get(parent
|
|
2740
|
+
let byDisplay = made.get(parent);
|
|
2674
2741
|
if (!byDisplay) {
|
|
2675
2742
|
byDisplay = new Map();
|
|
2676
|
-
made.set(parent
|
|
2743
|
+
made.set(parent, byDisplay);
|
|
2677
2744
|
}
|
|
2678
2745
|
let style = byDisplay.get(display);
|
|
2679
2746
|
if (!style) {
|
|
2680
2747
|
// `display` is the one property it does not start from: it is the
|
|
2681
2748
|
// box the fix-up made, and layout asks the style what a box is
|
|
2682
|
-
style = inherit(parent
|
|
2749
|
+
style = inherit(parent, initial);
|
|
2683
2750
|
style.display = display;
|
|
2684
2751
|
byDisplay.set(display, style);
|
|
2685
2752
|
}
|
|
@@ -2787,7 +2854,7 @@ function anonymousOf(
|
|
|
2787
2854
|
anonymous: AnonymousStyle,
|
|
2788
2855
|
display: ComputedStyle['display'] = kind as ComputedStyle['display'],
|
|
2789
2856
|
): Box {
|
|
2790
|
-
const box = new Box(kind, null, anonymous(parent, display));
|
|
2857
|
+
const box = new Box(kind, null, anonymous(parent.style, display));
|
|
2791
2858
|
box.parent = parent;
|
|
2792
2859
|
for (const child of run) {
|
|
2793
2860
|
child.parent = box;
|