@react-x11/components 0.13.0 → 0.14.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 +29 -2
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +70 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +26 -4
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +18 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +27 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +18 -4
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +66 -28
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +128 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +561 -144
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/transform.d.ts +85 -0
- package/dist/html/css/transform.d.ts.map +1 -0
- package/dist/html/css/transform.js +425 -0
- package/dist/html/css/transform.js.map +1 -0
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +14 -2
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +4 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/fonts.d.ts +55 -5
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +284 -23
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/layout/axes.d.ts +14 -0
- package/dist/html/layout/axes.d.ts.map +1 -0
- package/dist/html/layout/axes.js +28 -0
- package/dist/html/layout/axes.js.map +1 -0
- package/dist/html/layout/block.d.ts +93 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +389 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +59 -1
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +22 -2
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +1 -1
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +6 -0
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +90 -18
- package/dist/html/layout/css-grid.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +570 -83
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +144 -54
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/multicol.d.ts +42 -0
- package/dist/html/layout/multicol.d.ts.map +1 -0
- package/dist/html/layout/multicol.js +897 -0
- package/dist/html/layout/multicol.js.map +1 -0
- package/dist/html/layout/shaping.d.ts +7 -0
- package/dist/html/layout/shaping.d.ts.map +1 -1
- package/dist/html/layout/shaping.js +25 -20
- package/dist/html/layout/shaping.js.map +1 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +62 -18
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +3 -0
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +263 -36
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +79 -4
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1213 -141
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +15 -2
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +249 -17
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.js +87 -7
- package/dist/html/widgets.js.map +1 -1
- package/dist/html/woff2.d.ts +14 -0
- package/dist/html/woff2.d.ts.map +1 -0
- package/dist/html/woff2.js +608 -0
- package/dist/html/woff2.js.map +1 -0
- package/dist/maps/controller.d.ts +17 -1
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +27 -7
- package/dist/maps/controller.js.map +1 -1
- package/dist/richtext/node.d.ts +8 -1
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +5 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +25 -6
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +240 -24
- package/dist/richtext/runs.js.map +1 -1
- package/package.json +1 -1
- package/src/html/controls.ts +85 -2
- package/src/html/css/cascade.ts +23 -0
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +71 -31
- package/src/html/css/style.ts +664 -147
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +15 -2
- package/src/html/css/values.ts +4 -0
- package/src/html/fonts.ts +312 -29
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +444 -37
- package/src/html/layout/boxes.ts +65 -3
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +95 -22
- package/src/html/layout/flex.ts +671 -77
- package/src/html/layout/inline.ts +149 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +62 -19
- package/src/html/node.ts +300 -37
- package/src/html/paint.ts +1406 -206
- package/src/html/svg.ts +277 -19
- package/src/html/widgets.ts +95 -9
- package/src/html/woff2.ts +612 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
package/src/html/svg.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// as `preserveAspectRatio` says, and clipped to it.
|
|
15
15
|
import { parseDocument } from 'htmlparser2';
|
|
16
16
|
import { Element, Text } from 'domhandler';
|
|
17
|
-
import type { ChildNode } from 'domhandler';
|
|
17
|
+
import type { ChildNode, ParentNode } from 'domhandler';
|
|
18
18
|
import * as ntk from 'react-x11/ntk';
|
|
19
19
|
import { isSvgRoot } from './dom.js';
|
|
20
20
|
import { inkColor, isTransparent, parseColor } from './css/values.js';
|
|
@@ -183,9 +183,18 @@ export class SvgDrawing {
|
|
|
183
183
|
private _seen = -1;
|
|
184
184
|
private _seenLast: ChildNode | null = null;
|
|
185
185
|
private _seenViewport = '';
|
|
186
|
+
/** The `fill` and `stroke` the view's tree was last given its root's. */
|
|
187
|
+
private _seenPaint = '';
|
|
186
188
|
/** Whether a length in the tree is a percentage of the viewport; null
|
|
187
189
|
* until the tree is first read. */
|
|
188
190
|
private _percent: boolean | null = null;
|
|
191
|
+
/** Whether a `<use>` in the tree is one `SvgView` cannot draw as it
|
|
192
|
+
* stands (`needsExpanding`). */
|
|
193
|
+
private _uses = false;
|
|
194
|
+
/** The document's last node when a `<use>` was last looked up and its
|
|
195
|
+
* element not found: a streamed document may bring it yet, and has
|
|
196
|
+
* when its last node is another. Undefined where none was missing. */
|
|
197
|
+
private _missingAt: ChildNode | null | undefined = undefined;
|
|
189
198
|
private _failed = false;
|
|
190
199
|
/** An SVG image's own document, rather than an element of this one. */
|
|
191
200
|
private readonly _standalone: boolean;
|
|
@@ -203,7 +212,11 @@ export class SvgDrawing {
|
|
|
203
212
|
* Draw into a rectangle — the box's content box, in device pixels —
|
|
204
213
|
* clipped to it. The `viewBox`, where there is one, is fitted to it as
|
|
205
214
|
* `preserveAspectRatio` says; without one a user unit is a CSS pixel,
|
|
206
|
-
* which is `scale` device pixels. `color` is `currentColor
|
|
215
|
+
* which is `scale` device pixels. `color` is `currentColor`, and `fill`
|
|
216
|
+
* and `stroke` the root's own where the document's styles set them (SVG
|
|
217
|
+
* 2, 13.2): they are properties, which a style sheet's rule sets over
|
|
218
|
+
* the presentation attribute, and what is in the drawing inherits them
|
|
219
|
+
* from its root.
|
|
207
220
|
*/
|
|
208
221
|
draw(
|
|
209
222
|
ctx: ClipContext,
|
|
@@ -213,6 +226,8 @@ export class SvgDrawing {
|
|
|
213
226
|
h: number,
|
|
214
227
|
scale: number,
|
|
215
228
|
color?: string,
|
|
229
|
+
fill: string | null = null,
|
|
230
|
+
stroke: string | null = null,
|
|
216
231
|
): void {
|
|
217
232
|
if (this._failed || !(w > 0 && h > 0)) return;
|
|
218
233
|
// the mock backend has no path API, and SvgView draws paths
|
|
@@ -242,9 +257,12 @@ export class SvgDrawing {
|
|
|
242
257
|
}
|
|
243
258
|
const box = viewBoxOf(root);
|
|
244
259
|
// the viewport in user units, which a percentage is of
|
|
260
|
+
const paint =
|
|
261
|
+
(fill === null ? '' : `fill:${fill};`) +
|
|
262
|
+
(stroke === null ? '' : `stroke:${stroke};`);
|
|
245
263
|
const view = box
|
|
246
|
-
? this._viewFor(box[2], box[3])
|
|
247
|
-
: this._viewFor(w / scale, h / scale);
|
|
264
|
+
? this._viewFor(box[2], box[3], paint)
|
|
265
|
+
: this._viewFor(w / scale, h / scale, paint);
|
|
248
266
|
if (!view) return;
|
|
249
267
|
ctx.save();
|
|
250
268
|
try {
|
|
@@ -294,14 +312,32 @@ export class SvgDrawing {
|
|
|
294
312
|
}
|
|
295
313
|
}
|
|
296
314
|
|
|
297
|
-
private _viewFor(
|
|
315
|
+
private _viewFor(
|
|
316
|
+
width: number,
|
|
317
|
+
height: number,
|
|
318
|
+
/** The root's `fill` and `stroke` as declarations, or none. */
|
|
319
|
+
paint = '',
|
|
320
|
+
): SvgViewLike | null {
|
|
298
321
|
const root = this._root;
|
|
299
322
|
const count = root.children.length;
|
|
300
323
|
const last = root.lastChild;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
324
|
+
// more of the document has arrived since a `<use>` found nothing
|
|
325
|
+
const arrived =
|
|
326
|
+
this._missingAt !== undefined &&
|
|
327
|
+
lastNode(documentOf(root)) !== this._missingAt;
|
|
328
|
+
const grown = count !== this._seen || last !== this._seenLast || arrived;
|
|
329
|
+
if (grown || this._percent === null) {
|
|
330
|
+
this._percent = hasPercent(root);
|
|
331
|
+
this._uses = needsExpanding(root);
|
|
332
|
+
}
|
|
333
|
+
const sized = this._percent || this._uses;
|
|
334
|
+
const viewport = sized ? `${width}x${height}` : '';
|
|
335
|
+
if (
|
|
336
|
+
this._view &&
|
|
337
|
+
!grown &&
|
|
338
|
+
viewport === this._seenViewport &&
|
|
339
|
+
paint === this._seenPaint
|
|
340
|
+
) {
|
|
305
341
|
return this._view;
|
|
306
342
|
}
|
|
307
343
|
const View = svgViewClass();
|
|
@@ -311,15 +347,18 @@ export class SvgDrawing {
|
|
|
311
347
|
}
|
|
312
348
|
try {
|
|
313
349
|
const view = this._view ?? new View(null);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
350
|
+
const missing = { any: false };
|
|
351
|
+
const tree =
|
|
352
|
+
sized || root.name.includes(':')
|
|
353
|
+
? copyTree(root, sized ? [width, height] : null, missing)
|
|
354
|
+
: root;
|
|
355
|
+
view.setSvgDom(paint ? painted(tree, paint) : tree);
|
|
356
|
+
this._missingAt = missing.any ? lastNode(documentOf(root)) : undefined;
|
|
319
357
|
this._view = view;
|
|
320
358
|
this._seen = count;
|
|
321
359
|
this._seenLast = last;
|
|
322
360
|
this._seenViewport = viewport;
|
|
361
|
+
this._seenPaint = paint;
|
|
323
362
|
return view;
|
|
324
363
|
} catch {
|
|
325
364
|
this._failed = true;
|
|
@@ -328,6 +367,21 @@ export class SvgDrawing {
|
|
|
328
367
|
}
|
|
329
368
|
}
|
|
330
369
|
|
|
370
|
+
/**
|
|
371
|
+
* A drawing's root with its `fill` and `stroke` the document's: a copy of
|
|
372
|
+
* the root, its children the same ones, with the declarations at the end of
|
|
373
|
+
* its `style` — where `SvgView` reads a root's paint, over its presentation
|
|
374
|
+
* attributes, for what is under it to inherit.
|
|
375
|
+
*/
|
|
376
|
+
function painted(root: Element, paint: string): Element {
|
|
377
|
+
const style = root.attribs.style;
|
|
378
|
+
return new Element(
|
|
379
|
+
root.name,
|
|
380
|
+
{ ...root.attribs, style: style ? `${style};${paint}` : paint },
|
|
381
|
+
root.children,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
331
385
|
/**
|
|
332
386
|
* The colour an SVG image's root element sets as its background, in its
|
|
333
387
|
* `style` attribute — the one place an image can say it, since
|
|
@@ -413,13 +467,154 @@ function hasPercent(root: Element): boolean {
|
|
|
413
467
|
return walk(root);
|
|
414
468
|
}
|
|
415
469
|
|
|
470
|
+
/** The element a `<use>` refers to by a fragment, its id: one in another
|
|
471
|
+
* document is none here, where nothing is fetched for a drawing. */
|
|
472
|
+
function useTarget(el: Element): string | null {
|
|
473
|
+
const href = el.attribs.href ?? el.attribs['xlink:href'] ?? '';
|
|
474
|
+
return href.length > 1 && href.startsWith('#') ? href.slice(1) : null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** The elements under a root by their ids, the first of each. */
|
|
478
|
+
function idsUnder(root: Element): Map<string, Element> {
|
|
479
|
+
const ids = new Map<string, Element>();
|
|
480
|
+
const walk = (el: Element): void => {
|
|
481
|
+
for (const child of el.children) {
|
|
482
|
+
if (child.type !== 'tag') continue;
|
|
483
|
+
const tag = child as Element;
|
|
484
|
+
const id = tag.attribs.id;
|
|
485
|
+
if (id && !ids.has(id)) ids.set(id, tag);
|
|
486
|
+
walk(tag);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
walk(root);
|
|
490
|
+
return ids;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Whether a `<use>` under the root is one `SvgView` cannot draw as it
|
|
495
|
+
* stands: it looks a reference up among the root's own elements, and draws
|
|
496
|
+
* a `<symbol>` as its children where they are. So one to an element
|
|
497
|
+
* outside the root — an icon sprite's, a hidden `<svg>` of symbols at the
|
|
498
|
+
* top of the page — and one to a symbol with a `viewBox` of its own are
|
|
499
|
+
* drawn from a copy (`copyTree`).
|
|
500
|
+
*/
|
|
501
|
+
function needsExpanding(root: Element): boolean {
|
|
502
|
+
let ids: Map<string, Element> | null = null;
|
|
503
|
+
const walk = (el: Element): boolean => {
|
|
504
|
+
for (const child of el.children) {
|
|
505
|
+
if (child.type !== 'tag') continue;
|
|
506
|
+
const tag = child as Element;
|
|
507
|
+
if (localName(tag.name) === 'use') {
|
|
508
|
+
const id = useTarget(tag);
|
|
509
|
+
if (id !== null) {
|
|
510
|
+
ids ??= idsUnder(root);
|
|
511
|
+
const target = ids.get(id);
|
|
512
|
+
if (!target) return true;
|
|
513
|
+
if (localName(target.name) === 'symbol' && viewBoxOf(target)) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (walk(tag)) return true;
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
};
|
|
522
|
+
return walk(root);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function documentOf(el: Element): ParentNode {
|
|
526
|
+
let top: ParentNode = el;
|
|
527
|
+
while (top.parent) top = top.parent;
|
|
528
|
+
return top;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** The last node of a document, in its order: what a document that is
|
|
532
|
+
* still arriving has another of once more of it has. */
|
|
533
|
+
function lastNode(top: ParentNode): ChildNode | null {
|
|
534
|
+
let last: ChildNode | null = top.lastChild;
|
|
535
|
+
while (last && 'lastChild' in last && last.lastChild) last = last.lastChild;
|
|
536
|
+
return last;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** The first element of a document with an id, in its order. */
|
|
540
|
+
function elementById(top: ParentNode, id: string): Element | null {
|
|
541
|
+
const stack: ChildNode[] = [...top.children].reverse();
|
|
542
|
+
for (let node = stack.pop(); node; node = stack.pop()) {
|
|
543
|
+
if (node.type !== 'tag') continue;
|
|
544
|
+
const el = node as Element;
|
|
545
|
+
if (el.attribs.id === id) return el;
|
|
546
|
+
for (let i = el.children.length - 1; i >= 0; i -= 1) {
|
|
547
|
+
stack.push(el.children[i]);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** How deep a `<use>` may be of a `<use>`: one that reaches itself stops. */
|
|
554
|
+
const USE_DEPTH = 8;
|
|
555
|
+
|
|
556
|
+
/** A `<use>`'s `width` or `height`, in user units: a number, or a
|
|
557
|
+
* percentage of the viewport's; null for none, and for `auto`. */
|
|
558
|
+
function useLength(value: string | undefined, of: number): number | null {
|
|
559
|
+
if (value === undefined) return null;
|
|
560
|
+
const n = parseFloat(value);
|
|
561
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
562
|
+
return value.trim().endsWith('%') ? (n / 100) * of : n;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** The transform that fits a `viewBox` into a viewport at `x`, `y`, as
|
|
566
|
+
* `preserveAspectRatio` says (SVG 2, 8.2). */
|
|
567
|
+
function fitTransform(
|
|
568
|
+
box: [number, number, number, number],
|
|
569
|
+
x: number,
|
|
570
|
+
y: number,
|
|
571
|
+
width: number,
|
|
572
|
+
height: number,
|
|
573
|
+
preserve: string | undefined,
|
|
574
|
+
): string {
|
|
575
|
+
const fit = (preserve ?? '').trim().split(/\s+/);
|
|
576
|
+
if (fit[0] === 'defer') fit.shift();
|
|
577
|
+
let sx = width / box[2];
|
|
578
|
+
let sy = height / box[3];
|
|
579
|
+
if (fit[0] !== 'none') {
|
|
580
|
+
const align = ALIGN.exec(fit[0] ?? '') ?? ['xMidYMid', 'Mid', 'Mid'];
|
|
581
|
+
const scale = fit[1] === 'slice' ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
582
|
+
x += (width - box[2] * scale) * AT[align[1]];
|
|
583
|
+
y += (height - box[3] * scale) * AT[align[2]];
|
|
584
|
+
sx = scale;
|
|
585
|
+
sy = scale;
|
|
586
|
+
}
|
|
587
|
+
return `translate(${x - box[0] * sx},${y - box[1] * sy}) scale(${sx},${sy})`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** What a `<use>` says of where its element goes, and not of how it looks:
|
|
591
|
+
* the rest is the group's the element is drawn in. */
|
|
592
|
+
const USE_PLACEMENT = new Set([
|
|
593
|
+
'href',
|
|
594
|
+
'xlink:href',
|
|
595
|
+
'x',
|
|
596
|
+
'y',
|
|
597
|
+
'width',
|
|
598
|
+
'height',
|
|
599
|
+
]);
|
|
600
|
+
|
|
416
601
|
/**
|
|
417
602
|
* The tree `SvgView` reads, where the document's own will not do: with
|
|
418
603
|
* local names — `SvgView` knows `rect`, not `svg:rect`, and a prefix is
|
|
419
604
|
* dropped only where it is bound to SVG — and with its percentages resolved
|
|
420
|
-
* against a viewport of `[width, height]` user units, where one is given
|
|
605
|
+
* against a viewport of `[width, height]` user units, where one is given;
|
|
606
|
+
* and with each `<use>` that `SvgView` cannot draw (`needsExpanding`) as a
|
|
607
|
+
* group of what it refers to (SVG 2, 5.5): the element, from wherever in
|
|
608
|
+
* the document it is, or a symbol's children, its `viewBox` fitted to the
|
|
609
|
+
* viewport the `<use>` gives it — its `width` and `height`, and all of the
|
|
610
|
+
* drawing's where it has none. `missing.any` is set where one refers to an
|
|
611
|
+
* element the document does not have.
|
|
421
612
|
*/
|
|
422
|
-
function copyTree(
|
|
613
|
+
function copyTree(
|
|
614
|
+
root: Element,
|
|
615
|
+
viewport: [number, number] | null,
|
|
616
|
+
missing?: { any: boolean },
|
|
617
|
+
): Element {
|
|
423
618
|
const colon = root.name.indexOf(':');
|
|
424
619
|
const prefix = colon < 0 ? null : `${root.name.slice(0, colon)}:`;
|
|
425
620
|
const strip = (name: string): string =>
|
|
@@ -427,7 +622,69 @@ function copyTree(root: Element, viewport: [number, number] | null): Element {
|
|
|
427
622
|
const diagonal = viewport
|
|
428
623
|
? Math.sqrt((viewport[0] ** 2 + viewport[1] ** 2) / 2)
|
|
429
624
|
: 0;
|
|
430
|
-
|
|
625
|
+
let ids: Map<string, Element> | null = null;
|
|
626
|
+
const expand = (
|
|
627
|
+
use: Element,
|
|
628
|
+
resolve: boolean,
|
|
629
|
+
depth: number,
|
|
630
|
+
): Element | null => {
|
|
631
|
+
const id = useTarget(use);
|
|
632
|
+
if (id === null || depth >= USE_DEPTH) return null;
|
|
633
|
+
ids ??= idsUnder(root);
|
|
634
|
+
const inside = ids.get(id);
|
|
635
|
+
const target = inside ?? elementById(documentOf(root), id);
|
|
636
|
+
if (!target) {
|
|
637
|
+
if (missing) missing.any = true;
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
const symbol = localName(target.name) === 'symbol';
|
|
641
|
+
const box = symbol ? viewBoxOf(target) : null;
|
|
642
|
+
// one of the root's own with no viewport to fit: `SvgView` draws it
|
|
643
|
+
if (inside && !box) return null;
|
|
644
|
+
const attribs: Record<string, string> = {};
|
|
645
|
+
for (const name in use.attribs) {
|
|
646
|
+
if (!USE_PLACEMENT.has(name)) attribs[name] = use.attribs[name];
|
|
647
|
+
}
|
|
648
|
+
const x = parseFloat(use.attribs.x ?? '') || 0;
|
|
649
|
+
const y = parseFloat(use.attribs.y ?? '') || 0;
|
|
650
|
+
let placed = '';
|
|
651
|
+
if (box && viewport) {
|
|
652
|
+
const width =
|
|
653
|
+
useLength(use.attribs.width, viewport[0]) ??
|
|
654
|
+
useLength(target.attribs.width, viewport[0]) ??
|
|
655
|
+
viewport[0];
|
|
656
|
+
const height =
|
|
657
|
+
useLength(use.attribs.height, viewport[1]) ??
|
|
658
|
+
useLength(target.attribs.height, viewport[1]) ??
|
|
659
|
+
viewport[1];
|
|
660
|
+
placed = fitTransform(
|
|
661
|
+
box,
|
|
662
|
+
x,
|
|
663
|
+
y,
|
|
664
|
+
width,
|
|
665
|
+
height,
|
|
666
|
+
svgAttr(target, 'preserveAspectRatio'),
|
|
667
|
+
);
|
|
668
|
+
} else if (x || y) placed = `translate(${x},${y})`;
|
|
669
|
+
const transform = `${attribs.transform ?? ''} ${placed}`.trim();
|
|
670
|
+
if (transform) attribs.transform = transform;
|
|
671
|
+
// a length in a symbol with a `viewBox` is of that, and left as it is
|
|
672
|
+
const within = resolve && !box;
|
|
673
|
+
const children: ChildNode[] = [];
|
|
674
|
+
if (symbol) {
|
|
675
|
+
for (const child of target.children) {
|
|
676
|
+
if (child.type === 'tag') {
|
|
677
|
+
children.push(copy(child as Element, within, depth + 1));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} else children.push(copy(target, within, depth + 1));
|
|
681
|
+
return new Element('g', attribs, children);
|
|
682
|
+
};
|
|
683
|
+
const copy = (el: Element, resolve: boolean, depth = 0): Element => {
|
|
684
|
+
if (localName(el.name) === 'use') {
|
|
685
|
+
const group = expand(el, resolve, depth);
|
|
686
|
+
if (group) return group;
|
|
687
|
+
}
|
|
431
688
|
const here = resolve && !OWN_UNITS.has(localName(el.name));
|
|
432
689
|
const attribs = { ...el.attribs };
|
|
433
690
|
if (here && viewport && el !== root) {
|
|
@@ -444,8 +701,9 @@ function copyTree(root: Element, viewport: [number, number] | null): Element {
|
|
|
444
701
|
}
|
|
445
702
|
const children: ChildNode[] = [];
|
|
446
703
|
for (const child of el.children) {
|
|
447
|
-
if (child.type === 'tag')
|
|
448
|
-
|
|
704
|
+
if (child.type === 'tag') {
|
|
705
|
+
children.push(copy(child as Element, here, depth));
|
|
706
|
+
} else if (child.type === 'text') children.push(new Text(child.data));
|
|
449
707
|
}
|
|
450
708
|
return new Element(strip(el.name), attribs, children);
|
|
451
709
|
};
|
package/src/html/widgets.ts
CHANGED
|
@@ -25,11 +25,11 @@ import type {} from 'react-x11/jsx-runtime';
|
|
|
25
25
|
|
|
26
26
|
import { cancelLater, later } from '../internal/timers.js';
|
|
27
27
|
import { hx } from './hx.js';
|
|
28
|
-
import { attr, tagOf } from './dom.js';
|
|
28
|
+
import { attr, isElement, tagOf } from './dom.js';
|
|
29
29
|
import type { Document, Element } from './dom.js';
|
|
30
30
|
import type { HtmlViewNode } from './node.js';
|
|
31
31
|
import type { RootLook } from './css/style.js';
|
|
32
|
-
import { buttonLabel, optionsOf, selectedOption } from './controls.js';
|
|
32
|
+
import { between, buttonLabel, optionsOf, selectedOption } from './controls.js';
|
|
33
33
|
import type { BareField, ControlRect } from './controls.js';
|
|
34
34
|
import {
|
|
35
35
|
FormState,
|
|
@@ -433,12 +433,28 @@ function renderControl(
|
|
|
433
433
|
const readOnly = attr(el, 'readonly') !== undefined;
|
|
434
434
|
// a field whose box the document draws takes its content box
|
|
435
435
|
const at = rect.bare ?? rect;
|
|
436
|
+
const left = Math.round(at.x);
|
|
437
|
+
const top = Math.round(at.y);
|
|
438
|
+
const width = Math.round(at.width);
|
|
439
|
+
const height = Math.round(at.height);
|
|
440
|
+
// What the widget shows through: its own rectangle, or where the
|
|
441
|
+
// document cuts the element, what the cut leaves of it — out to where a
|
|
442
|
+
// focus ring reaches, which a clip around the element cuts as it cuts
|
|
443
|
+
// the element, and one that leaves the box whole does not.
|
|
444
|
+
const port = rect.clip
|
|
445
|
+
? between(rect.clip, {
|
|
446
|
+
x: left - RING_REACH,
|
|
447
|
+
y: top - RING_REACH,
|
|
448
|
+
width: width + 2 * RING_REACH,
|
|
449
|
+
height: height + 2 * RING_REACH,
|
|
450
|
+
})
|
|
451
|
+
: { x: left, y: top, width, height };
|
|
436
452
|
const frame: Style = {
|
|
437
453
|
position: 'absolute',
|
|
438
|
-
left:
|
|
439
|
-
top:
|
|
440
|
-
width
|
|
441
|
-
height
|
|
454
|
+
left: left - port.x,
|
|
455
|
+
top: top - port.y,
|
|
456
|
+
width,
|
|
457
|
+
height,
|
|
442
458
|
// The face and the size the box was measured in, the element's: the
|
|
443
459
|
// palette's from the UA sheet, or the page's where it set its own. A
|
|
444
460
|
// field inherits them, and so does a `<Button>`'s or a `<Select>`'s
|
|
@@ -452,6 +468,7 @@ function renderControl(
|
|
|
452
468
|
...(rect.opacity !== undefined && { opacity: rect.opacity }),
|
|
453
469
|
};
|
|
454
470
|
const field = rect.bare ? bareField(rect.bare) : fieldChrome(look);
|
|
471
|
+
const order = tabOrder(el);
|
|
455
472
|
// A button or a select whose font the page set is the drawn control: a
|
|
456
473
|
// native bezel sets its title at AppKit's size, whatever it is handed,
|
|
457
474
|
// in a bezel only as tall as that title, where the box was measured for
|
|
@@ -474,6 +491,7 @@ function renderControl(
|
|
|
474
491
|
widget = h(Checkbox, {
|
|
475
492
|
checked: attr(el, 'checked') !== undefined,
|
|
476
493
|
disabled,
|
|
494
|
+
...order,
|
|
477
495
|
// the element's whole box takes the press, as it does in a browser:
|
|
478
496
|
// a page that sizes one over its label, invisible, means the label
|
|
479
497
|
style: { width: '100%', height: '100%' },
|
|
@@ -482,6 +500,9 @@ function renderControl(
|
|
|
482
500
|
break;
|
|
483
501
|
case 'radio': {
|
|
484
502
|
const value = attr(el, 'value') ?? 'on';
|
|
503
|
+
// No `tabindex` reaches a radio: core's `<Radio>` takes no props for
|
|
504
|
+
// the node it draws, as the other widgets do, and the group's box is
|
|
505
|
+
// not the one that takes the focus.
|
|
485
506
|
widget = h(
|
|
486
507
|
RadioGroup,
|
|
487
508
|
{
|
|
@@ -496,6 +517,7 @@ function renderControl(
|
|
|
496
517
|
widget = h(Button, {
|
|
497
518
|
label: buttonLabel(el),
|
|
498
519
|
disabled,
|
|
520
|
+
...order,
|
|
499
521
|
...(ownFont && { native: false }),
|
|
500
522
|
style: { width: '100%', height: '100%' },
|
|
501
523
|
onPress: () => ctx.press(el),
|
|
@@ -506,7 +528,11 @@ function renderControl(
|
|
|
506
528
|
widget = h(Select, {
|
|
507
529
|
options: options.map((o) => ({ value: o.value, label: o.label })),
|
|
508
530
|
value: selectedOption(el) ?? undefined,
|
|
531
|
+
// a `<select>` with nothing selected has no options, and shows
|
|
532
|
+
// none: core's "Select…" is an application's prompt, not a page's
|
|
533
|
+
placeholder: '',
|
|
509
534
|
disabled,
|
|
535
|
+
...order,
|
|
510
536
|
style: rect.bare
|
|
511
537
|
? [BARE_TRIGGER, { width: '100%', height: '100%' }]
|
|
512
538
|
: { width: '100%', height: '100%' },
|
|
@@ -547,6 +573,7 @@ function renderControl(
|
|
|
547
573
|
widget = hx('textarea', {
|
|
548
574
|
defaultValue: forms.value(el),
|
|
549
575
|
maxLength: maxLength(el),
|
|
576
|
+
...order,
|
|
550
577
|
style: [field, { width: '100%', height: '100%' }],
|
|
551
578
|
onChange: readOnly ? undefined : (ev) => typed(ev.value),
|
|
552
579
|
});
|
|
@@ -557,6 +584,7 @@ function renderControl(
|
|
|
557
584
|
defaultValue: forms.value(el),
|
|
558
585
|
placeholder: attr(el, 'placeholder'),
|
|
559
586
|
maxLength: maxLength(el),
|
|
587
|
+
...order,
|
|
560
588
|
// Core's word for a password field: nothing in it reaches a
|
|
561
589
|
// selection, PRIMARY included.
|
|
562
590
|
sensitive: type === 'password',
|
|
@@ -578,18 +606,76 @@ function renderControl(
|
|
|
578
606
|
default:
|
|
579
607
|
return null;
|
|
580
608
|
}
|
|
609
|
+
// Two boxes, always: the one the widget shows through, and in it the
|
|
610
|
+
// element's. A widget the document comes to cut, or stops cutting, is
|
|
611
|
+
// the same widget in the same place in the tree, and keeps its focus
|
|
612
|
+
// and its caret.
|
|
581
613
|
return hx(
|
|
582
614
|
'box',
|
|
583
615
|
{
|
|
584
616
|
key,
|
|
585
|
-
ref: ctx.widgets.refOf(el),
|
|
586
|
-
style: frame,
|
|
587
617
|
selectable: false,
|
|
618
|
+
style: {
|
|
619
|
+
position: 'absolute',
|
|
620
|
+
left: port.x,
|
|
621
|
+
top: port.y,
|
|
622
|
+
width: port.width,
|
|
623
|
+
height: port.height,
|
|
624
|
+
// cut to it, and nothing itself: a press beside the widget is the
|
|
625
|
+
// document's
|
|
626
|
+
...(rect.clip && { overflow: 'hidden', pointerEvents: 'box-none' }),
|
|
627
|
+
},
|
|
588
628
|
},
|
|
589
|
-
|
|
629
|
+
hx(
|
|
630
|
+
'box',
|
|
631
|
+
{
|
|
632
|
+
ref: ctx.widgets.refOf(el),
|
|
633
|
+
style: frame,
|
|
634
|
+
selectable: false,
|
|
635
|
+
// a control the page keeps from assistive technology is kept from
|
|
636
|
+
// it here: core leaves the node, and the widget in it, out of the
|
|
637
|
+
// accessibility tree
|
|
638
|
+
...(ariaHidden(el) && { 'aria-hidden': true }),
|
|
639
|
+
},
|
|
640
|
+
widget,
|
|
641
|
+
),
|
|
590
642
|
);
|
|
591
643
|
}
|
|
592
644
|
|
|
645
|
+
/**
|
|
646
|
+
* What an element's `tabindex` says of its widget's place in the Tab order
|
|
647
|
+
* (HTML 6.6.3): a negative one is focusable — by a press, by its label, by
|
|
648
|
+
* `autofocus` — and not reached by Tab, which is core's `tabIndex={-1}`
|
|
649
|
+
* too. Radix lays a native `<select tabindex="-1" aria-hidden="true">`
|
|
650
|
+
* beside the picker it draws, cut to nothing: it was a stop the eye could
|
|
651
|
+
* not find, and Space on it opened an empty menu.
|
|
652
|
+
*
|
|
653
|
+
* Nothing else is handed over. Zero is where a control already is, and a
|
|
654
|
+
* positive one would put a page's control ahead of the application's own,
|
|
655
|
+
* whose window it is: the order between a document and what is around it
|
|
656
|
+
* is not the document's to set.
|
|
657
|
+
*/
|
|
658
|
+
function tabOrder(el: Element): { tabIndex: -1 } | undefined {
|
|
659
|
+
// the rules for parsing integers (HTML 2.3.4.1): white space, a sign,
|
|
660
|
+
// digits, and whatever follows them ignored
|
|
661
|
+
const m = /^[ \t\n\f\r]*([+-]?\d+)/.exec(attr(el, 'tabindex') ?? '');
|
|
662
|
+
return m && Number(m[1]) < 0 ? { tabIndex: -1 } : undefined;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Whether an element is hidden from assistive technology: `aria-hidden`
|
|
666
|
+
* is true on it or on an element around it (WAI-ARIA 1.2, 6.6). */
|
|
667
|
+
function ariaHidden(el: Element): boolean {
|
|
668
|
+
for (let at: Element | null = el; at;) {
|
|
669
|
+
if (attr(at, 'aria-hidden')?.trim().toLowerCase() === 'true') return true;
|
|
670
|
+
const parent: Element['parent'] = at.parent;
|
|
671
|
+
at = isElement(parent) ? parent : null;
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** How far past its box a widget's focus ring is drawn, and then some. */
|
|
677
|
+
const RING_REACH = 8;
|
|
678
|
+
|
|
593
679
|
/** A field's `maxlength`, which the widget enforces as it is typed into. */
|
|
594
680
|
function maxLength(el: Element): number | undefined {
|
|
595
681
|
const raw = attr(el, 'maxlength')?.trim();
|