@react-x11/components 0.16.0 → 0.17.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/css/animation.d.ts +91 -0
- package/dist/html/css/animation.d.ts.map +1 -0
- package/dist/html/css/animation.js +430 -0
- package/dist/html/css/animation.js.map +1 -0
- package/dist/html/css/cascade.d.ts +27 -4
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +94 -6
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +35 -0
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +115 -2
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +5 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +57 -7
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.js +12 -8
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +13 -0
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +28 -0
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +237 -67
- 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 +41 -11
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +12 -0
- package/dist/html/node.js.map +1 -1
- package/package.json +1 -1
- package/src/html/css/animation.ts +466 -0
- package/src/html/css/cascade.ts +130 -7
- package/src/html/css/parse.ts +151 -2
- package/src/html/css/style.ts +69 -7
- package/src/html/css/ua.ts +12 -8
- package/src/html/layout/boxes.ts +40 -0
- package/src/html/layout/flex.ts +251 -75
- package/src/html/layout/inline.ts +43 -9
- package/src/html/node.ts +12 -0
package/src/html/css/parse.ts
CHANGED
|
@@ -77,6 +77,43 @@ export interface Stylesheet {
|
|
|
77
77
|
/** `@counter-style` rules, in order: a name and its descriptors, which
|
|
78
78
|
* the cascade reads into counter styles (CSS Counter Styles 3, 3). */
|
|
79
79
|
counterStyles?: { prelude: string; declarations: Declaration[] }[];
|
|
80
|
+
/** `@keyframes` rules, in order (`KeyframesRule`). */
|
|
81
|
+
keyframes?: KeyframesRule[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* One `@keyframes` rule (CSS Animations 1, 3): the frames of the animation
|
|
86
|
+
* an `animation-name` names. Which rule a name finds is the cascade's to
|
|
87
|
+
* say (`Cascade.keyframes`): the last of its name whose media hold, in the
|
|
88
|
+
* latest layer, and never a prefixed one over one that is not.
|
|
89
|
+
*/
|
|
90
|
+
export interface KeyframesRule {
|
|
91
|
+
/** As written: a name is case-sensitive, as a custom ident is. */
|
|
92
|
+
name: string;
|
|
93
|
+
/** `@-webkit-keyframes`, which Chrome reads and which never takes the
|
|
94
|
+
* place of an `@keyframes` of the same name, wherever either comes. */
|
|
95
|
+
prefixed: boolean;
|
|
96
|
+
/** The frames in the order written, which is the order two at one
|
|
97
|
+
* offset cascade in. */
|
|
98
|
+
frames: Keyframe[];
|
|
99
|
+
/** The `@media` blocks the rule sits under, as a style rule's. */
|
|
100
|
+
media: MediaCondition[][] | null;
|
|
101
|
+
/** The cascade layer the rule is in, as a style rule's. */
|
|
102
|
+
layer: readonly number[] | null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** One frame of a `@keyframes`: a block of declarations at the offsets its
|
|
106
|
+
* selector names. */
|
|
107
|
+
export interface Keyframe {
|
|
108
|
+
/** Where in an iteration the frame is, 0 to 1: one per selector in its
|
|
109
|
+
* list, `from` 0 and `to` 1. */
|
|
110
|
+
offsets: number[];
|
|
111
|
+
/** What the frame sets: no `!important` one, which a frame ignores, and
|
|
112
|
+
* none of the animation's or a transition's own properties. */
|
|
113
|
+
declarations: Declaration[];
|
|
114
|
+
/** The `animation-timing-function` the frame eases to the next one by,
|
|
115
|
+
* as written; null where it gives none and the animation's is used. */
|
|
116
|
+
easing: string | null;
|
|
80
117
|
}
|
|
81
118
|
|
|
82
119
|
/**
|
|
@@ -322,9 +359,22 @@ export function parseStylesheet(
|
|
|
322
359
|
prelude: at.prelude,
|
|
323
360
|
declarations: parseDeclarations(at.block),
|
|
324
361
|
});
|
|
362
|
+
} else if (
|
|
363
|
+
(name === 'keyframes' || name === '-webkit-keyframes') &&
|
|
364
|
+
at.block !== null
|
|
365
|
+
) {
|
|
366
|
+
const keyframes = parseKeyframes(at.prelude, at.block);
|
|
367
|
+
if (keyframes) {
|
|
368
|
+
(sheet.keyframes ??= []).push({
|
|
369
|
+
...keyframes,
|
|
370
|
+
prefixed: name !== 'keyframes',
|
|
371
|
+
media,
|
|
372
|
+
layer,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
325
375
|
}
|
|
326
|
-
// @
|
|
327
|
-
// consumed.
|
|
376
|
+
// @page, and an at-rule nobody knows: nothing to do, and the block
|
|
377
|
+
// was already consumed.
|
|
328
378
|
continue;
|
|
329
379
|
}
|
|
330
380
|
|
|
@@ -1764,6 +1814,105 @@ const GENERIC_FAMILIES = new Set([
|
|
|
1764
1814
|
'default',
|
|
1765
1815
|
]);
|
|
1766
1816
|
|
|
1817
|
+
/** The properties a keyframe does not set: the animation's own, and a
|
|
1818
|
+
* transition's (CSS Animations 1, 3). */
|
|
1819
|
+
const NOT_IN_KEYFRAMES = /^(?:-webkit-)?(?:animation|transition)(?:-|$)/;
|
|
1820
|
+
|
|
1821
|
+
/** A `<number-token>` percentage, `+12.5%` or `1e2%`. */
|
|
1822
|
+
const KEYFRAME_PERCENT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?%$/i;
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* A `@keyframes` block's name and frames (CSS Animations 1, 3), or null
|
|
1826
|
+
* where its prelude is no name. A frame whose selector is not a list of
|
|
1827
|
+
* `from`, `to` and percentages from 0 to 100 is dropped whole, as a style
|
|
1828
|
+
* rule with a bad selector is, and the frames around it stand.
|
|
1829
|
+
*/
|
|
1830
|
+
function parseKeyframes(
|
|
1831
|
+
prelude: string,
|
|
1832
|
+
block: string,
|
|
1833
|
+
): { name: string; frames: Keyframe[] } | null {
|
|
1834
|
+
const name = keyframesName(prelude);
|
|
1835
|
+
if (name === null) return null;
|
|
1836
|
+
const frames: Keyframe[] = [];
|
|
1837
|
+
let i = 0;
|
|
1838
|
+
const n = block.length;
|
|
1839
|
+
while (i < n) {
|
|
1840
|
+
i = skipTrivia(block, i);
|
|
1841
|
+
if (i >= n) break;
|
|
1842
|
+
// a frame holds declarations and nothing else
|
|
1843
|
+
if (block[i] === '@' && startsIdent(block, i + 1)) {
|
|
1844
|
+
i = readAtRule(block, i).end;
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
const braceAt = scanTo(block, i, '{');
|
|
1848
|
+
if (braceAt >= n) break;
|
|
1849
|
+
const selector = block.slice(i, braceAt);
|
|
1850
|
+
const body = readBlock(block, braceAt);
|
|
1851
|
+
i = body.end;
|
|
1852
|
+
const offsets = keyframeOffsets(selector);
|
|
1853
|
+
if (!offsets) continue;
|
|
1854
|
+
let easing: string | null = null;
|
|
1855
|
+
const declarations: Declaration[] = [];
|
|
1856
|
+
for (const d of parseDeclarations(body.body)) {
|
|
1857
|
+
if (d.important) continue;
|
|
1858
|
+
if (
|
|
1859
|
+
d.prop === 'animation-timing-function' ||
|
|
1860
|
+
d.prop === '-webkit-animation-timing-function'
|
|
1861
|
+
) {
|
|
1862
|
+
easing = d.value;
|
|
1863
|
+
} else if (!NOT_IN_KEYFRAMES.test(d.prop)) {
|
|
1864
|
+
declarations.push(d);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
frames.push({ offsets, declarations, easing });
|
|
1868
|
+
}
|
|
1869
|
+
return { name, frames };
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/** A `@keyframes` prelude's name: a string, or an identifier that is not
|
|
1873
|
+
* `none` or a CSS-wide keyword; null for anything else. */
|
|
1874
|
+
function keyframesName(prelude: string): string | null {
|
|
1875
|
+
const v = prelude.trim();
|
|
1876
|
+
if (!v) return null;
|
|
1877
|
+
if (v[0] === '"' || v[0] === "'") {
|
|
1878
|
+
const name = unquote(v);
|
|
1879
|
+
return name !== v && name ? name : null;
|
|
1880
|
+
}
|
|
1881
|
+
if (!startsIdent(v, 0)) return null;
|
|
1882
|
+
const ident = readIdent(v, 0);
|
|
1883
|
+
if (ident.end !== v.length) return null;
|
|
1884
|
+
return KEYFRAMES_RESERVED.has(ident.value.toLowerCase()) ? null : ident.value;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
const KEYFRAMES_RESERVED = new Set([
|
|
1888
|
+
'none',
|
|
1889
|
+
'inherit',
|
|
1890
|
+
'initial',
|
|
1891
|
+
'unset',
|
|
1892
|
+
'revert',
|
|
1893
|
+
'revert-layer',
|
|
1894
|
+
'default',
|
|
1895
|
+
]);
|
|
1896
|
+
|
|
1897
|
+
/** A keyframe selector's offsets, 0 to 1, or null where any of its list is
|
|
1898
|
+
* not one. */
|
|
1899
|
+
function keyframeOffsets(selector: string): number[] | null {
|
|
1900
|
+
const out: number[] = [];
|
|
1901
|
+
for (const part of selector.split(',')) {
|
|
1902
|
+
const v = part.trim().toLowerCase();
|
|
1903
|
+
if (v === 'from') out.push(0);
|
|
1904
|
+
else if (v === 'to') out.push(1);
|
|
1905
|
+
else if (KEYFRAME_PERCENT.test(v)) {
|
|
1906
|
+
const pct = Number(v.slice(0, -1));
|
|
1907
|
+
if (!(pct >= 0 && pct <= 100)) return null;
|
|
1908
|
+
out.push(pct / 100);
|
|
1909
|
+
} else {
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
return out;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1767
1916
|
/**
|
|
1768
1917
|
* A `@font-face` block, or null when it names no family or no source — a
|
|
1769
1918
|
* rule CSS Fonts 4 (4.3) drops whole. A rule whose sources are all
|
package/src/html/css/style.ts
CHANGED
|
@@ -41,6 +41,13 @@ import type { ContentItem, CounterChange } from './content.js';
|
|
|
41
41
|
import type { CustomProps } from './vars.js';
|
|
42
42
|
import { parseRotate, parseScale, parseTransform } from './transform.js';
|
|
43
43
|
import type { TransformFunction } from './transform.js';
|
|
44
|
+
import {
|
|
45
|
+
ANIMATION_LONGHANDS,
|
|
46
|
+
NO_ANIMATIONS,
|
|
47
|
+
animationLonghand,
|
|
48
|
+
parseAnimation,
|
|
49
|
+
} from './animation.js';
|
|
50
|
+
import type { Animations } from './animation.js';
|
|
44
51
|
import {
|
|
45
52
|
LIGHT_DARK,
|
|
46
53
|
SYSTEM_COLOR,
|
|
@@ -590,6 +597,10 @@ export interface ComputedStyle {
|
|
|
590
597
|
/** `transform-origin`: the point a transform turns and scales about,
|
|
591
598
|
* from the border box's top left, a percentage of its width and height. */
|
|
592
599
|
transformOrigin: [Len, Len];
|
|
600
|
+
/** `animation` and its longhands, each a list (`css/animation.ts`):
|
|
601
|
+
* `NO_ANIMATIONS`, shared, where nothing sets one. What they leave on
|
|
602
|
+
* the style is in its other fields already (`Cascade._computeStyle`). */
|
|
603
|
+
animations: Animations;
|
|
593
604
|
zIndex: number | 'auto';
|
|
594
605
|
/** A keyword, a length to raise the box by, or a percentage of its own
|
|
595
606
|
* line height. */
|
|
@@ -1085,6 +1096,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
|
|
|
1085
1096
|
scale: null,
|
|
1086
1097
|
transform: null,
|
|
1087
1098
|
transformOrigin: [{ pct: 50 }, { pct: 50 }],
|
|
1099
|
+
animations: NO_ANIMATIONS,
|
|
1088
1100
|
zIndex: AUTO,
|
|
1089
1101
|
verticalAlign: 'baseline',
|
|
1090
1102
|
|
|
@@ -1438,13 +1450,23 @@ const BORDER_WIDTH_KEYWORDS: Record<string, number> = {
|
|
|
1438
1450
|
thick: 5,
|
|
1439
1451
|
};
|
|
1440
1452
|
|
|
1441
|
-
/** `-webkit-` names that are the multicol properties' own
|
|
1442
|
-
|
|
1453
|
+
/** `-webkit-` names that are the multicol properties' own, and the
|
|
1454
|
+
* animation's. */
|
|
1455
|
+
const PREFIXED_ALIASES: Record<string, string> = {
|
|
1443
1456
|
'-webkit-columns': 'columns',
|
|
1444
1457
|
'-webkit-column-count': 'column-count',
|
|
1445
1458
|
'-webkit-column-width': 'column-width',
|
|
1446
1459
|
'-webkit-column-gap': 'column-gap',
|
|
1447
1460
|
'-webkit-column-break-inside': 'break-inside',
|
|
1461
|
+
'-webkit-animation': 'animation',
|
|
1462
|
+
'-webkit-animation-name': 'animation-name',
|
|
1463
|
+
'-webkit-animation-duration': 'animation-duration',
|
|
1464
|
+
'-webkit-animation-timing-function': 'animation-timing-function',
|
|
1465
|
+
'-webkit-animation-delay': 'animation-delay',
|
|
1466
|
+
'-webkit-animation-iteration-count': 'animation-iteration-count',
|
|
1467
|
+
'-webkit-animation-direction': 'animation-direction',
|
|
1468
|
+
'-webkit-animation-fill-mode': 'animation-fill-mode',
|
|
1469
|
+
'-webkit-animation-play-state': 'animation-play-state',
|
|
1448
1470
|
};
|
|
1449
1471
|
|
|
1450
1472
|
/**
|
|
@@ -1463,11 +1485,11 @@ export function applyDeclaration(
|
|
|
1463
1485
|
ctx: UnitContext,
|
|
1464
1486
|
): void {
|
|
1465
1487
|
const written = prop.toLowerCase();
|
|
1466
|
-
// the multicol properties under the names WebKit had them
|
|
1467
|
-
// Blink still reads, each an alias of the property (`-webkit-
|
|
1468
|
-
// count` in its `css_properties.json5`): a page written for it
|
|
1469
|
-
// names no other
|
|
1470
|
-
const name =
|
|
1488
|
+
// the multicol and animation properties under the names WebKit had them
|
|
1489
|
+
// by, which Blink still reads, each an alias of the property (`-webkit-
|
|
1490
|
+
// column-count` in its `css_properties.json5`): a page written for it
|
|
1491
|
+
// alone names no other
|
|
1492
|
+
const name = PREFIXED_ALIASES[written] ?? written;
|
|
1471
1493
|
let value = rawValue.trim();
|
|
1472
1494
|
if (!value) return;
|
|
1473
1495
|
// a logical property is the physical one it stands for, the CSS-wide
|
|
@@ -1906,6 +1928,27 @@ export function applyDeclaration(
|
|
|
1906
1928
|
if (a !== null) style.opacity = a;
|
|
1907
1929
|
return;
|
|
1908
1930
|
}
|
|
1931
|
+
case 'animation': {
|
|
1932
|
+
const animations = parseAnimation(value);
|
|
1933
|
+
if (animations) style.animations = animations;
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
case 'animation-name':
|
|
1937
|
+
case 'animation-duration':
|
|
1938
|
+
case 'animation-timing-function':
|
|
1939
|
+
case 'animation-delay':
|
|
1940
|
+
case 'animation-iteration-count':
|
|
1941
|
+
case 'animation-direction':
|
|
1942
|
+
case 'animation-fill-mode':
|
|
1943
|
+
case 'animation-play-state': {
|
|
1944
|
+
const animations = animationLonghand(
|
|
1945
|
+
style.animations,
|
|
1946
|
+
ANIMATION_LONGHANDS[name],
|
|
1947
|
+
value,
|
|
1948
|
+
);
|
|
1949
|
+
if (animations) style.animations = animations;
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1909
1952
|
case 'translate': {
|
|
1910
1953
|
// `none`, or across, down, and a depth nothing here has
|
|
1911
1954
|
if (value.trim().toLowerCase() === 'none') {
|
|
@@ -5646,6 +5689,14 @@ export function initialOne(
|
|
|
5646
5689
|
style.mask = { ...style.mask, [list]: initial.mask[list] };
|
|
5647
5690
|
return;
|
|
5648
5691
|
}
|
|
5692
|
+
const animation = ANIMATION_LONGHANDS[PREFIXED_ALIASES[name] ?? name];
|
|
5693
|
+
if (animation) {
|
|
5694
|
+
style.animations = {
|
|
5695
|
+
...style.animations,
|
|
5696
|
+
[animation]: initial.animations[animation],
|
|
5697
|
+
};
|
|
5698
|
+
return;
|
|
5699
|
+
}
|
|
5649
5700
|
const keys = INHERIT_TARGETS[name];
|
|
5650
5701
|
if (!keys) return;
|
|
5651
5702
|
for (const key of keys) {
|
|
@@ -5663,6 +5714,15 @@ function inheritOne(
|
|
|
5663
5714
|
style.mask = { ...style.mask, [list]: parent.mask[list] };
|
|
5664
5715
|
return;
|
|
5665
5716
|
}
|
|
5717
|
+
// each longhand of the animation is a list of its own, as the mask's are
|
|
5718
|
+
const animation = ANIMATION_LONGHANDS[PREFIXED_ALIASES[name] ?? name];
|
|
5719
|
+
if (animation) {
|
|
5720
|
+
style.animations = {
|
|
5721
|
+
...style.animations,
|
|
5722
|
+
[animation]: parent.animations[animation],
|
|
5723
|
+
};
|
|
5724
|
+
return;
|
|
5725
|
+
}
|
|
5666
5726
|
const keys = INHERIT_TARGETS[name];
|
|
5667
5727
|
if (!keys) return;
|
|
5668
5728
|
// A border colour left to `currentColor` inherits as the keyword and
|
|
@@ -5717,6 +5777,8 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
|
|
|
5717
5777
|
'border-image': ['borderImage'],
|
|
5718
5778
|
mask: ['mask'],
|
|
5719
5779
|
'-webkit-mask': ['mask'],
|
|
5780
|
+
animation: ['animations'],
|
|
5781
|
+
'-webkit-animation': ['animations'],
|
|
5720
5782
|
'border-image-source': ['borderImage'],
|
|
5721
5783
|
'border-image-slice': ['borderImage'],
|
|
5722
5784
|
'border-image-width': ['borderImage'],
|
package/src/html/css/ua.ts
CHANGED
|
@@ -211,12 +211,16 @@ col { display: table-column; }
|
|
|
211
211
|
/* The form controls are real widgets rather than drawn boxes, so what the
|
|
212
212
|
UA sheet owes them is a *box* of about the right size in the flow — the
|
|
213
213
|
widget is painted into it by the component above. 'inline-block' is what
|
|
214
|
-
makes a label and its input share a line.
|
|
214
|
+
makes a label and its input share a line. Chrome's margins and alignment:
|
|
215
|
+
none round a field, a select, a button or a text area, each on its line's
|
|
216
|
+
baseline — a field's text on the text beside it (\`CONTROL_BASELINES\`) —
|
|
217
|
+
and a meter or a progress bar a fifth of an em under it. */
|
|
215
218
|
input, button, select, textarea, meter, progress {
|
|
216
219
|
display: inline-block;
|
|
217
|
-
vertical-align:
|
|
218
|
-
margin:
|
|
220
|
+
vertical-align: baseline;
|
|
221
|
+
margin: 0;
|
|
219
222
|
}
|
|
223
|
+
meter, progress { vertical-align: -0.2em; }
|
|
220
224
|
/* A control's text is a system font's, not its parent's: Chrome gives these
|
|
221
225
|
four \`font: -webkit-small-control\`, Arial at the default size less 2pt
|
|
222
226
|
(13.33px) whatever the text around it is set in, and Gecko \`-moz-field\`
|
|
@@ -231,8 +235,6 @@ input, button, select, textarea {
|
|
|
231
235
|
font-size: ${look.controlFontSize ?? look.fontSize}px;
|
|
232
236
|
}
|
|
233
237
|
textarea { font-family: ${mono}; }
|
|
234
|
-
/* Chrome's own UA margins for the checkables, near enough: they are the
|
|
235
|
-
controls that sit hard against their label text otherwise. */
|
|
236
238
|
/* HTML's rendering section (15.3.10): a control's text keeps none of the
|
|
237
239
|
spacing, the line height, the case or the indent of the text around it.
|
|
238
240
|
A button in a paragraph of 'line-height: 1.5' is its own font's line
|
|
@@ -258,8 +260,11 @@ input:focus-visible, textarea:focus-visible {
|
|
|
258
260
|
outline: auto 1px -webkit-focus-ring-color;
|
|
259
261
|
outline-offset: 0;
|
|
260
262
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
/* Chrome's own margins for the checkables and a range, which sit on their
|
|
264
|
+
border box's bottom edge, the margin under the line */
|
|
265
|
+
input[type=checkbox] { margin: 3px 3px 3px 4px; }
|
|
266
|
+
input[type=radio] { margin: 3px 3px 0 5px; }
|
|
267
|
+
input[type=range] { margin: 2px; }
|
|
263
268
|
input[type=hidden] { display: none; }
|
|
264
269
|
/* A <button> is drawn rather than mounted: its content is the document's —
|
|
265
270
|
an icon, a label in spans, a pill of the page's own design, which is what
|
|
@@ -280,7 +285,6 @@ button {
|
|
|
280
285
|
cursor: pointer;
|
|
281
286
|
}
|
|
282
287
|
button[disabled] { color: ${look.mutedColor}; cursor: default; }
|
|
283
|
-
textarea { vertical-align: top; }
|
|
284
288
|
fieldset { margin: 0 2px; padding: 0.35em 0.75em 0.6em; border: 1px solid ${look.borderColor}; }
|
|
285
289
|
legend { display: block; padding: 0 2px; }
|
|
286
290
|
label { cursor: pointer; }
|
package/src/html/layout/boxes.ts
CHANGED
|
@@ -562,6 +562,40 @@ export type ReplacedKind =
|
|
|
562
562
|
/** An inline `<svg>`: a drawing, sized by what it says of its size. */
|
|
563
563
|
| 'svg';
|
|
564
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Where a control sits on the line, as Chrome sets it: a field, a select
|
|
567
|
+
* or an input button on the baseline of the text it shows, its line in its
|
|
568
|
+
* face centred in its content box — that face's ascent and descent — and a
|
|
569
|
+
* checkbox, a radio button or a range on its border box's bottom, its
|
|
570
|
+
* bottom margin under the line. A `<textarea>` is none of these, and sits
|
|
571
|
+
* on its bottom margin edge as a box that scrolls does.
|
|
572
|
+
*/
|
|
573
|
+
export const CONTROL_BASELINES = new WeakMap<
|
|
574
|
+
Box,
|
|
575
|
+
InlineDecoration | 'border'
|
|
576
|
+
>();
|
|
577
|
+
|
|
578
|
+
function controlBaseline(
|
|
579
|
+
el: Element,
|
|
580
|
+
kind: ReplacedKind,
|
|
581
|
+
style: ComputedStyle,
|
|
582
|
+
options: BuildOptions,
|
|
583
|
+
): InlineDecoration | 'border' | null {
|
|
584
|
+
if (kind === 'checkbox' || kind === 'radio') return 'border';
|
|
585
|
+
if (kind === 'input' && (attr(el, 'type') ?? '').toLowerCase() === 'range') {
|
|
586
|
+
return 'border';
|
|
587
|
+
}
|
|
588
|
+
if (kind !== 'input' && kind !== 'select' && kind !== 'button') return null;
|
|
589
|
+
// a list box shows rows, and sits on its bottom edge
|
|
590
|
+
if (kind === 'select' && Number(attr(el, 'size') ?? 1) > 1) return null;
|
|
591
|
+
return (
|
|
592
|
+
options.faceExtent?.(style) ?? {
|
|
593
|
+
ascent: style.fontSize * 0.8,
|
|
594
|
+
descent: style.fontSize * 0.2,
|
|
595
|
+
}
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
565
599
|
/** A replaced box's intrinsic dimensions, in device pixels. */
|
|
566
600
|
export interface Intrinsic {
|
|
567
601
|
/** Its size — or, on an axis `missing` names, the default object size of
|
|
@@ -853,6 +887,10 @@ export interface BuildOptions {
|
|
|
853
887
|
* pixels: what a list marker's image with no size of its own is sized
|
|
854
888
|
* by (`_markerImageSize`). Where it is not known, 0.8em. */
|
|
855
889
|
faceAscent?(style: ComputedStyle): number | undefined;
|
|
890
|
+
/** How far a style's first face reaches above and below its baseline,
|
|
891
|
+
* in device pixels: where a control sets its text, which is where its
|
|
892
|
+
* baseline is (`CONTROL_BASELINES`). */
|
|
893
|
+
faceExtent?(style: ComputedStyle): InlineDecoration | undefined;
|
|
856
894
|
/** The size a real widget wants, so the box in the flow is the size the
|
|
857
895
|
* control will be drawn at. */
|
|
858
896
|
controlSize(
|
|
@@ -1486,6 +1524,8 @@ class Builder {
|
|
|
1486
1524
|
ratio: 0,
|
|
1487
1525
|
};
|
|
1488
1526
|
this._controls.push(box);
|
|
1527
|
+
const baseline = controlBaseline(el, replaced, style, this._options);
|
|
1528
|
+
if (baseline) CONTROL_BASELINES.set(box, baseline);
|
|
1489
1529
|
// A control's value is the widget's, not the document's: putting it in
|
|
1490
1530
|
// the selection index would make Ctrl+A copy the contents of every text
|
|
1491
1531
|
// field, which no document viewer does.
|