@fusion.dev/helix-core 0.26.0 → 0.30.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/custom-elements.json +833 -114
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +23 -1
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +23 -26
- package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +27 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.js +17 -2
- package/dist/controllers/floating-controller/floating-controller.d.ts +13 -0
- package/dist/controllers/floating-controller/floating-controller.js +3 -1
- package/dist/controllers/indicator-controller/indicator-controller.d.ts +16 -0
- package/dist/controllers/indicator-controller/indicator-controller.js +7 -0
- package/dist/controllers/popover-controller/popover-controller.d.ts +22 -0
- package/dist/controllers/popover-controller/popover-controller.js +12 -0
- package/dist/controllers/source-controller/source-controller.d.ts +45 -3
- package/dist/controllers/source-controller/source-controller.js +33 -21
- package/dist/helix-element/helix-element.d.ts +19 -0
- package/dist/helix-element/helix-element.js +5 -94
- package/dist/helix-element/react-bridge.d.ts +104 -0
- package/dist/helix-element/react-bridge.js +94 -0
- package/dist/helix-form-element/helix-form-element.d.ts +19 -0
- package/dist/helix-form-element/helix-form-element.js +6 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +1 -1
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +2 -5
- package/dist/mixins/mixin-controller-value/mixin-controller-value.js +2 -2
- package/dist/mixins/mixin-field/mixin-field.js +2 -2
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +48 -0
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +43 -0
- package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +118 -0
- package/dist/mixins/mixin-picker-field/mixin-picker-field.js +79 -0
- package/dist/utils/draw-icon/draw-icon.d.ts +1 -1
- package/dist/utils/draw-tooltip/draw-tooltip.js +1 -0
- package/package.json +4 -4
- package/dist/mixins/mixin-collection/index.d.ts +0 -1
- package/dist/mixins/mixin-collection/index.js +0 -1
- package/dist/utils/close-markdown/close-markdown.d.ts +0 -39
- package/dist/utils/close-markdown/close-markdown.js +0 -51
|
@@ -35,7 +35,9 @@ export interface ChartPointerHost extends ReactiveControllerHost, HTMLElement {
|
|
|
35
35
|
* `pointerType` is not fine.
|
|
36
36
|
*/
|
|
37
37
|
export declare class ChartPointerController implements ReactiveController {
|
|
38
|
-
|
|
38
|
+
private host;
|
|
39
|
+
private moved;
|
|
40
|
+
private plot;
|
|
39
41
|
/** The reading under the pointer, or -1 when the pointer is elsewhere. */
|
|
40
42
|
at: number;
|
|
41
43
|
/**
|
|
@@ -73,6 +75,26 @@ export declare class ChartPointerController implements ReactiveController {
|
|
|
73
75
|
* position until the render that follows.
|
|
74
76
|
*/
|
|
75
77
|
hostUpdated(): void;
|
|
78
|
+
/**
|
|
79
|
+
* The box the readings are laid out in, which is the plot and not the host.
|
|
80
|
+
*
|
|
81
|
+
* The host also holds the row of labels, so measuring against it would map
|
|
82
|
+
* the pointer over a box taller than the drawing. That mattered the moment a
|
|
83
|
+
* chart started measuring down the box rather than across it, which is what
|
|
84
|
+
* `axis` is for: on a horizontal bar chart the labels are beside the plot
|
|
85
|
+
* rather than under it, and the host is wider than the drawing by exactly
|
|
86
|
+
* their column.
|
|
87
|
+
*
|
|
88
|
+
* **Held only while it is still in the tree.** Every chart renders a
|
|
89
|
+
* different template for its empty state, so the plot a pointer crossed
|
|
90
|
+
* before the table filled is a node Lit has since replaced: held forever, it
|
|
91
|
+
* measures a detached box and the tooltip never appears. Looked up again
|
|
92
|
+
* whenever the held one has left, which is once per data arrival rather than
|
|
93
|
+
* once per move.
|
|
94
|
+
*/
|
|
95
|
+
private box;
|
|
76
96
|
/** Puts the pointer on a reading, for a chart that resolves it some other way. */
|
|
77
97
|
set(at: number): void;
|
|
98
|
+
private readonly move;
|
|
99
|
+
private readonly leave;
|
|
78
100
|
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
export class ChartPointerController {
|
|
2
|
-
#host;
|
|
3
|
-
#moved;
|
|
4
|
-
#plot;
|
|
5
2
|
constructor(host) {
|
|
6
|
-
this
|
|
3
|
+
this.moved = false;
|
|
7
4
|
this.at = -1;
|
|
8
5
|
this.axis = 'inline';
|
|
9
|
-
this
|
|
6
|
+
this.move = (event) => {
|
|
10
7
|
if (this.axis === 'none' || (event.pointerType !== 'mouse' && event.pointerType !== 'pen')) {
|
|
11
8
|
return;
|
|
12
9
|
}
|
|
13
|
-
const box = this
|
|
14
|
-
const count = this
|
|
10
|
+
const box = this.box();
|
|
11
|
+
const count = this.host.readings;
|
|
15
12
|
const along = this.axis === 'block' ? box?.height : box?.width;
|
|
16
13
|
if (!box || !along || count === 0) {
|
|
17
14
|
return;
|
|
@@ -21,48 +18,48 @@ export class ChartPointerController {
|
|
|
21
18
|
const next = Math.min(Math.max(slot, 0), count - 1);
|
|
22
19
|
if (next !== this.at) {
|
|
23
20
|
this.at = next;
|
|
24
|
-
this
|
|
25
|
-
this
|
|
21
|
+
this.moved = true;
|
|
22
|
+
this.host.requestUpdate();
|
|
26
23
|
}
|
|
27
24
|
};
|
|
28
|
-
this
|
|
25
|
+
this.leave = () => {
|
|
29
26
|
if (this.at !== -1) {
|
|
30
27
|
this.at = -1;
|
|
31
|
-
this
|
|
28
|
+
this.host.requestUpdate();
|
|
32
29
|
}
|
|
33
30
|
};
|
|
34
|
-
this
|
|
31
|
+
this.host = host;
|
|
35
32
|
host.addController(this);
|
|
36
33
|
}
|
|
37
34
|
hostConnected() {
|
|
38
|
-
this
|
|
39
|
-
this
|
|
35
|
+
this.host.addEventListener('pointermove', this.move);
|
|
36
|
+
this.host.addEventListener('pointerleave', this.leave);
|
|
40
37
|
}
|
|
41
38
|
hostDisconnected() {
|
|
42
|
-
this
|
|
43
|
-
this
|
|
39
|
+
this.host.removeEventListener('pointermove', this.move);
|
|
40
|
+
this.host.removeEventListener('pointerleave', this.leave);
|
|
44
41
|
this.at = -1;
|
|
45
42
|
}
|
|
46
43
|
hostUpdated() {
|
|
47
|
-
if (!this
|
|
44
|
+
if (!this.moved) {
|
|
48
45
|
return;
|
|
49
46
|
}
|
|
50
|
-
this
|
|
51
|
-
this
|
|
47
|
+
this.moved = false;
|
|
48
|
+
this.host.renderRoot
|
|
52
49
|
.querySelector('[part="tooltip-anchor"]')
|
|
53
50
|
?.reposition?.();
|
|
54
51
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
box() {
|
|
53
|
+
if (!this.plot?.isConnected) {
|
|
54
|
+
this.plot = this.host.renderRoot.querySelector('[part="plot"]') ?? undefined;
|
|
55
|
+
}
|
|
56
|
+
return this.plot?.getBoundingClientRect();
|
|
58
57
|
}
|
|
59
58
|
set(at) {
|
|
60
59
|
if (at !== this.at) {
|
|
61
60
|
this.at = at;
|
|
62
|
-
this
|
|
63
|
-
this
|
|
61
|
+
this.moved = true;
|
|
62
|
+
this.host.requestUpdate();
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
|
-
#move;
|
|
67
|
-
#leave;
|
|
68
65
|
}
|
|
@@ -74,8 +74,35 @@ export declare class DismissController implements ReactiveController {
|
|
|
74
74
|
* null in some browsers, so the read is guarded and the frame is simply
|
|
75
75
|
* skipped. Nothing is lost by skipping it that was not already lost: there is
|
|
76
76
|
* no listener to add to a document this one may not touch.
|
|
77
|
+
*
|
|
78
|
+
* **And walked only where the page holds a frame at all.** The walk is every
|
|
79
|
+
* element on the page, through every shadow root, and it ran on every
|
|
80
|
+
* `activate`, which is every tooltip hover and every menu opening. Measured
|
|
81
|
+
* in Chromium on 14 Sep 2026, on a page of 10,006 elements across 500 shadow
|
|
82
|
+
* roots, which is the shape of a docs page: 1.36ms per walk, against a
|
|
83
|
+
* length read on `getElementsByTagName('iframe')` too small for
|
|
84
|
+
* `performance.now` to resolve over 20,000 runs. That collection is live and
|
|
85
|
+
* the engine keeps its count, so asking it is a read rather than a search.
|
|
86
|
+
*
|
|
87
|
+
* **What the count cannot see is a frame inside a shadow root**, and that is
|
|
88
|
+
* a trade made knowingly rather than an oversight. A page whose only frame
|
|
89
|
+
* sits in a shadow root reads as having none and skips the walk, so a
|
|
90
|
+
* same-origin frame rendered by a component is not listened to there. The
|
|
91
|
+
* one frame this library renders inside a shadow root is the sandbox's
|
|
92
|
+
* preview, which is sandboxed without `allow-same-origin` and so is a
|
|
93
|
+
* document this one may not read whether or not it is found: the blur
|
|
94
|
+
* signal is what covers it, and the walk never could. A page with a frame in
|
|
95
|
+
* the light DOM still walks, shadow roots included.
|
|
96
|
+
*
|
|
97
|
+
* Held for the rest of the run as well, so overlays that activate together,
|
|
98
|
+
* a dialog opening a select from its own `updated`, walk once between them.
|
|
99
|
+
* A microtask and no longer: what a frame gathered here is good for is the
|
|
100
|
+
* lifetime of one open, and the task after this one is a page that may have
|
|
101
|
+
* gained a frame.
|
|
77
102
|
*/
|
|
78
103
|
private static frameDocuments;
|
|
104
|
+
/** What the last walk found, for the rest of the run it was made in. */
|
|
105
|
+
private static found;
|
|
79
106
|
/**
|
|
80
107
|
* The frame documents being listened to, held so the same ones are released.
|
|
81
108
|
*
|
|
@@ -8,7 +8,12 @@ const deepDepth = (node) => {
|
|
|
8
8
|
}
|
|
9
9
|
return depth;
|
|
10
10
|
};
|
|
11
|
-
const
|
|
11
|
+
const REGISTRY = Symbol.for('fusion.helix.dismiss.listening');
|
|
12
|
+
const globals = globalThis;
|
|
13
|
+
if (globals[REGISTRY] === undefined) {
|
|
14
|
+
globals[REGISTRY] = new Set();
|
|
15
|
+
}
|
|
16
|
+
const listening = globals[REGISTRY];
|
|
12
17
|
export class DismissController {
|
|
13
18
|
constructor(host, { boundary, onDismiss }) {
|
|
14
19
|
this.active = false;
|
|
@@ -54,6 +59,12 @@ export class DismissController {
|
|
|
54
59
|
host.addController(this);
|
|
55
60
|
}
|
|
56
61
|
static frameDocuments() {
|
|
62
|
+
if (DismissController.found) {
|
|
63
|
+
return DismissController.found;
|
|
64
|
+
}
|
|
65
|
+
if (document.getElementsByTagName('iframe').length === 0) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
57
68
|
const found = [];
|
|
58
69
|
const walk = (root) => {
|
|
59
70
|
for (const element of root.querySelectorAll('*')) {
|
|
@@ -73,6 +84,10 @@ export class DismissController {
|
|
|
73
84
|
}
|
|
74
85
|
};
|
|
75
86
|
walk(document);
|
|
87
|
+
DismissController.found = found;
|
|
88
|
+
queueMicrotask(() => {
|
|
89
|
+
DismissController.found = undefined;
|
|
90
|
+
});
|
|
76
91
|
return found;
|
|
77
92
|
}
|
|
78
93
|
activate() {
|
|
@@ -84,7 +99,7 @@ export class DismissController {
|
|
|
84
99
|
document.addEventListener('keydown', this.handleKeydown);
|
|
85
100
|
document.addEventListener('pointerdown', this.handlePointerDown);
|
|
86
101
|
window.addEventListener('blur', this.handleWindowBlur);
|
|
87
|
-
this.frames = DismissController.frameDocuments();
|
|
102
|
+
this.frames = [...DismissController.frameDocuments()];
|
|
88
103
|
for (const inner of this.frames) {
|
|
89
104
|
inner.addEventListener('pointerdown', this.handlePointerDown);
|
|
90
105
|
}
|
|
@@ -112,6 +112,19 @@ export declare class FloatingController implements ReactiveController {
|
|
|
112
112
|
constructor(host: ReactiveControllerHost & HTMLElement, options: FloatingOptions);
|
|
113
113
|
/** The placement in use after the last measurement. */
|
|
114
114
|
placement: Placement;
|
|
115
|
+
/**
|
|
116
|
+
* How far the arrow keeps from the floating element's ends, measured once.
|
|
117
|
+
*
|
|
118
|
+
* `autoUpdate` runs `update` on every scroll frame of every ancestor, and
|
|
119
|
+
* `corner` is a `getComputedStyle` and four parses, which was a style read
|
|
120
|
+
* per frame for a number that does not change while the surface is up. So it
|
|
121
|
+
* is read as placing starts, when the surface has just been shown and has a
|
|
122
|
+
* computed style to read, and forgotten as placing stops, so a surface
|
|
123
|
+
* restyled between opens is measured again. `update` measures for itself
|
|
124
|
+
* where nothing started it, which is a `reposition` on a surface placed by
|
|
125
|
+
* its own stylesheet.
|
|
126
|
+
*/
|
|
127
|
+
private padding;
|
|
115
128
|
/** Starts placing, and keeps placing until stopped. */
|
|
116
129
|
start(): void;
|
|
117
130
|
/** Stops placing and releases everything it was tracking. */
|
|
@@ -53,7 +53,7 @@ export class FloatingController {
|
|
|
53
53
|
}));
|
|
54
54
|
}
|
|
55
55
|
if (pointer) {
|
|
56
|
-
middleware.push(arrow({ element: pointer, padding: corner(floating) }));
|
|
56
|
+
middleware.push(arrow({ element: pointer, padding: this.padding ?? corner(floating) }));
|
|
57
57
|
}
|
|
58
58
|
computePosition(anchor, floating, {
|
|
59
59
|
middleware,
|
|
@@ -92,12 +92,14 @@ export class FloatingController {
|
|
|
92
92
|
if (!anchor || !floating || this.cleanup) {
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
|
+
this.padding = this.options.arrow?.() ? corner(floating) : 0;
|
|
95
96
|
this.cleanup = autoUpdate(anchor, floating, this.update);
|
|
96
97
|
}
|
|
97
98
|
stop() {
|
|
98
99
|
this.run += 1;
|
|
99
100
|
this.cleanup?.();
|
|
100
101
|
this.cleanup = undefined;
|
|
102
|
+
this.padding = undefined;
|
|
101
103
|
this.options.floating()?.removeAttribute('data-placed');
|
|
102
104
|
}
|
|
103
105
|
hostDisconnected() {
|
|
@@ -7,6 +7,20 @@ export interface IndicatorOptions {
|
|
|
7
7
|
active: () => HTMLElement | null | undefined;
|
|
8
8
|
/** The traveling mark. Positioned against the surface. */
|
|
9
9
|
indicator: () => HTMLElement | null | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Elements whose own size moves the mark, beyond the host.
|
|
12
|
+
*
|
|
13
|
+
* The host is always watched, and it is not enough on its own: an item can
|
|
14
|
+
* change width inside a strip that keeps its size, and the host never fires
|
|
15
|
+
* for that. So a component hands over its items, and they are watched too.
|
|
16
|
+
*
|
|
17
|
+
* Read again on every placement rather than once, because the set is not
|
|
18
|
+
* settled when the controller connects: a tab strip's tabs have not
|
|
19
|
+
* upgraded by its first update, so a list read then is empty, and watching
|
|
20
|
+
* nothing is silent. Observing the same element twice is a no-op, so the
|
|
21
|
+
* re-read costs nothing and needs no bookkeeping about who is watched.
|
|
22
|
+
*/
|
|
23
|
+
observe?: () => readonly Element[];
|
|
10
24
|
/**
|
|
11
25
|
* The box the mark is positioned against, which is its containing block.
|
|
12
26
|
*
|
|
@@ -61,6 +75,8 @@ export declare class IndicatorController implements ReactiveController {
|
|
|
61
75
|
private resize;
|
|
62
76
|
constructor(host: ReactiveControllerHost & HTMLElement, options: IndicatorOptions);
|
|
63
77
|
hostConnected(): void;
|
|
78
|
+
/** Watches whatever `observe` names now, which `place` asks for on every placement. */
|
|
79
|
+
private watch;
|
|
64
80
|
hostDisconnected(): void;
|
|
65
81
|
/** Called after every update, which is when the marked child may have moved. */
|
|
66
82
|
hostUpdated(): void;
|
|
@@ -7,6 +7,12 @@ export class IndicatorController {
|
|
|
7
7
|
hostConnected() {
|
|
8
8
|
this.resize = new ResizeObserver(() => this.place());
|
|
9
9
|
this.resize.observe(this.host);
|
|
10
|
+
this.watch();
|
|
11
|
+
}
|
|
12
|
+
watch() {
|
|
13
|
+
for (const element of this.options.observe?.() ?? []) {
|
|
14
|
+
this.resize?.observe(element);
|
|
15
|
+
}
|
|
10
16
|
}
|
|
11
17
|
hostDisconnected() {
|
|
12
18
|
this.resize?.disconnect();
|
|
@@ -17,6 +23,7 @@ export class IndicatorController {
|
|
|
17
23
|
this.place();
|
|
18
24
|
}
|
|
19
25
|
place() {
|
|
26
|
+
this.watch();
|
|
20
27
|
const indicator = this.options.indicator();
|
|
21
28
|
const surface = this.options.surface();
|
|
22
29
|
const active = this.options.active();
|
|
@@ -56,6 +56,26 @@ export interface PopoverOptions {
|
|
|
56
56
|
* select puts focus back on its trigger, and a tooltip does neither.
|
|
57
57
|
*/
|
|
58
58
|
onDismiss: () => void;
|
|
59
|
+
/**
|
|
60
|
+
* Whether the host wants the surface up, read after every update.
|
|
61
|
+
*
|
|
62
|
+
* **The open state is a property, and a consumer may set it directly**, so
|
|
63
|
+
* raising the surface from a `show()` method alone leaves a panel opened by
|
|
64
|
+
* declaring `open` visible, out of the top layer and unanchored: every
|
|
65
|
+
* imperative path works and every declarative one is half-open. Ten
|
|
66
|
+
* families wrote the same `if (this.open) show() else hide()` into their
|
|
67
|
+
* `updated()` for that reason, and this is that paragraph once.
|
|
68
|
+
*
|
|
69
|
+
* After the update and never before it, because the render has to have
|
|
70
|
+
* been written: until then the surface is still `display: none` and
|
|
71
|
+
* `showPopover` on it measures zeroes. A controller's `hostUpdated` runs
|
|
72
|
+
* after the commit and before the host's own `updated`, so a host that has
|
|
73
|
+
* to act on a surface that is up, focusing into it or centering a wheel,
|
|
74
|
+
* finds it up.
|
|
75
|
+
*
|
|
76
|
+
* Left unset, the host drives `show()` and `hide()` itself.
|
|
77
|
+
*/
|
|
78
|
+
open?: () => boolean;
|
|
59
79
|
/** Preferred side. It moves if that side does not fit. */
|
|
60
80
|
placement?: Placement | (() => Placement);
|
|
61
81
|
/** The panel that floats. */
|
|
@@ -99,6 +119,8 @@ export declare class PopoverController implements ReactiveController {
|
|
|
99
119
|
constructor(host: ReactiveControllerHost & HTMLElement, options: PopoverOptions);
|
|
100
120
|
/** Whether the surface is showing. */
|
|
101
121
|
get open(): boolean;
|
|
122
|
+
/** Puts the surface where the host's `open` says, however it got there. */
|
|
123
|
+
hostUpdated(): void;
|
|
102
124
|
/** Whether this one is placed by measurement, read fresh so it may change. */
|
|
103
125
|
private get places();
|
|
104
126
|
/**
|
|
@@ -21,6 +21,18 @@ export class PopoverController {
|
|
|
21
21
|
get open() {
|
|
22
22
|
return this.shown;
|
|
23
23
|
}
|
|
24
|
+
hostUpdated() {
|
|
25
|
+
const wanted = this.options.open?.();
|
|
26
|
+
if (wanted === undefined) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (wanted) {
|
|
30
|
+
this.show();
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
this.hide();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
24
36
|
get places() {
|
|
25
37
|
const asked = this.options.place;
|
|
26
38
|
if (asked === undefined) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
-
import { type Series } from '../../utils/read-series/read-series.js';
|
|
2
|
+
import { type Row, type Series } from '../../utils/read-series/read-series.js';
|
|
3
3
|
/** A host that names the element it reads from. */
|
|
4
4
|
export interface SourceHost extends ReactiveControllerHost, HTMLElement {
|
|
5
5
|
/**
|
|
@@ -28,7 +28,20 @@ export interface SourceHost extends ReactiveControllerHost, HTMLElement {
|
|
|
28
28
|
* and nothing in a page has reason to know a chart is reading it.
|
|
29
29
|
*/
|
|
30
30
|
export declare class SourceController implements ReactiveController {
|
|
31
|
-
|
|
31
|
+
private host;
|
|
32
|
+
private observer;
|
|
33
|
+
/** Whether a hydrating render was told there was nothing, and needs the render after. */
|
|
34
|
+
private deferred;
|
|
35
|
+
/**
|
|
36
|
+
* Which edit of the table the parses below belong to.
|
|
37
|
+
*
|
|
38
|
+
* Moved by the watcher and by `watch()`, which are the two things that can
|
|
39
|
+
* change what a chart reads without the chart doing anything: an edit to the
|
|
40
|
+
* table, and `for` naming another one.
|
|
41
|
+
*/
|
|
42
|
+
private version;
|
|
43
|
+
private heldRows;
|
|
44
|
+
private heldSeries;
|
|
32
45
|
constructor(host: SourceHost);
|
|
33
46
|
/**
|
|
34
47
|
* The element `for` names, or null where there is nothing to name it in.
|
|
@@ -42,8 +55,37 @@ export declare class SourceController implements ReactiveController {
|
|
|
42
55
|
* a template Lit throws on. The update after it, which this asks for, draws.
|
|
43
56
|
*/
|
|
44
57
|
get element(): Element | null;
|
|
45
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* The element `for` names, whatever the host is rendering.
|
|
60
|
+
*
|
|
61
|
+
* Apart from `element` because the watcher needs the table while a render
|
|
62
|
+
* cannot have it: attached on connect, which on a server's page is before
|
|
63
|
+
* the render that hydrates, it resolved nothing there and never watched, so a
|
|
64
|
+
* table a page filled afterwards drew no line.
|
|
65
|
+
*/
|
|
66
|
+
private resolve;
|
|
67
|
+
/**
|
|
68
|
+
* The series it holds, or none.
|
|
69
|
+
*
|
|
70
|
+
* **Parsed once per edit, not once per read.** A chart's render asks for
|
|
71
|
+
* these several times over, and the pointer asks for the row count on every
|
|
72
|
+
* move, so parsing the whole table on each read was a table walk per
|
|
73
|
+
* pointermove. The parse is held against the table it came from and the
|
|
74
|
+
* edit it saw, and let go when either moves: `element` is compared by
|
|
75
|
+
* identity, so a table replaced under the same id parses again, and the
|
|
76
|
+
* watcher moves the version on every mutation it delivers.
|
|
77
|
+
*
|
|
78
|
+
* Which is a microtask after the edit, the observer being what it is. A
|
|
79
|
+
* read in the same run as the edit answers the parse before it, and every
|
|
80
|
+
* render that follows, which is what the watcher exists to ask for, answers
|
|
81
|
+
* the one after. The same array comes back until then, so a consumer may
|
|
82
|
+
* compare by identity.
|
|
83
|
+
*/
|
|
46
84
|
get series(): Series[];
|
|
85
|
+
/** Every row it holds, text and number alike, or none. Held the way `series` is. */
|
|
86
|
+
get rows(): Row[];
|
|
87
|
+
/** A held parse where it still describes the table, or a fresh one. */
|
|
88
|
+
private parsed;
|
|
47
89
|
hostConnected(): void;
|
|
48
90
|
hostUpdated(): void;
|
|
49
91
|
hostDisconnected(): void;
|
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import { readSeries } from '../../utils/read-series/read-series.js';
|
|
1
|
+
import { readRows, readSeries } from '../../utils/read-series/read-series.js';
|
|
2
2
|
export class SourceController {
|
|
3
|
-
#host;
|
|
4
|
-
#observer;
|
|
5
|
-
#deferred = false;
|
|
6
3
|
constructor(host) {
|
|
7
|
-
this
|
|
4
|
+
this.deferred = false;
|
|
5
|
+
this.version = 0;
|
|
6
|
+
this.host = host;
|
|
8
7
|
host.addController(this);
|
|
9
8
|
}
|
|
10
9
|
get element() {
|
|
11
|
-
if (this
|
|
12
|
-
this
|
|
10
|
+
if (this.host.hydrating === true) {
|
|
11
|
+
this.deferred = this.host.for !== '';
|
|
13
12
|
return null;
|
|
14
13
|
}
|
|
15
|
-
return this
|
|
14
|
+
return this.resolve();
|
|
16
15
|
}
|
|
17
|
-
|
|
18
|
-
const host = this
|
|
16
|
+
resolve() {
|
|
17
|
+
const host = this.host;
|
|
19
18
|
if (host.for === '') {
|
|
20
19
|
return null;
|
|
21
20
|
}
|
|
@@ -33,32 +32,45 @@ export class SourceController {
|
|
|
33
32
|
: null;
|
|
34
33
|
}
|
|
35
34
|
get series() {
|
|
35
|
+
this.heldSeries = this.parsed(this.heldSeries, (table) => (table ? readSeries(table) : []));
|
|
36
|
+
return this.heldSeries.value;
|
|
37
|
+
}
|
|
38
|
+
get rows() {
|
|
39
|
+
this.heldRows = this.parsed(this.heldRows, (table) => (table ? readRows(table) : []));
|
|
40
|
+
return this.heldRows.value;
|
|
41
|
+
}
|
|
42
|
+
parsed(held, read) {
|
|
36
43
|
const element = this.element;
|
|
37
|
-
|
|
44
|
+
if (held && held.element === element && held.version === this.version) {
|
|
45
|
+
return held;
|
|
46
|
+
}
|
|
47
|
+
return { element, value: read(element), version: this.version };
|
|
38
48
|
}
|
|
39
49
|
hostConnected() {
|
|
40
50
|
this.watch();
|
|
41
51
|
}
|
|
42
52
|
hostUpdated() {
|
|
43
|
-
if (this
|
|
44
|
-
this
|
|
45
|
-
this
|
|
53
|
+
if (this.deferred) {
|
|
54
|
+
this.deferred = false;
|
|
55
|
+
this.host.requestUpdate();
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
hostDisconnected() {
|
|
49
|
-
this
|
|
50
|
-
this
|
|
59
|
+
this.observer?.disconnect();
|
|
60
|
+
this.observer = undefined;
|
|
51
61
|
}
|
|
52
62
|
watch() {
|
|
53
|
-
this
|
|
54
|
-
|
|
63
|
+
this.observer?.disconnect();
|
|
64
|
+
this.version += 1;
|
|
65
|
+
const element = this.resolve();
|
|
55
66
|
if (!element || typeof MutationObserver === 'undefined') {
|
|
56
67
|
return;
|
|
57
68
|
}
|
|
58
|
-
this
|
|
59
|
-
this
|
|
69
|
+
this.observer ??= new MutationObserver(() => {
|
|
70
|
+
this.version += 1;
|
|
71
|
+
this.host.requestUpdate();
|
|
60
72
|
});
|
|
61
|
-
this
|
|
73
|
+
this.observer.observe(element, {
|
|
62
74
|
characterData: true,
|
|
63
75
|
childList: true,
|
|
64
76
|
subtree: true,
|
|
@@ -50,6 +50,25 @@ export declare class HelixElement extends LitElement {
|
|
|
50
50
|
* template cache on the joined string rather than on the object.
|
|
51
51
|
*/
|
|
52
52
|
protected tag(name: string): StaticValue;
|
|
53
|
+
/**
|
|
54
|
+
* One of this element's own parts, by name, or null where it is not rendered.
|
|
55
|
+
*
|
|
56
|
+
* Not `part`, which every element already has: `Element.part` is the
|
|
57
|
+
* `DOMTokenList` of the names the element itself carries, and a method under
|
|
58
|
+
* that name is a type error on every subclass.
|
|
59
|
+
*
|
|
60
|
+
* **One guard for every lookup.** Lit's server shim has no `querySelector`
|
|
61
|
+
* on the render root, so a lookup reached during a server render, from a
|
|
62
|
+
* getter a template reads or from `willUpdate`, throws there and nowhere
|
|
63
|
+
* else. Four spellings of the same guard had grown across the library, and
|
|
64
|
+
* a fifth site with none. Null on the server, which is what the rest of the
|
|
65
|
+
* code already treats a part that has not rendered as.
|
|
66
|
+
*
|
|
67
|
+
* `helix component`'s `selectors` reads these calls the way it reads a
|
|
68
|
+
* `querySelector('[part="x"]')`, so a name nothing renders is a finding
|
|
69
|
+
* rather than a null an optional chain swallows.
|
|
70
|
+
*/
|
|
71
|
+
protected partNamed<T extends Element = HTMLElement>(name: string): T | null;
|
|
53
72
|
/**
|
|
54
73
|
* The element's semantics, set without touching the DOM.
|
|
55
74
|
*
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { css, LitElement } from 'lit';
|
|
2
2
|
import { unsafeStatic } from 'lit/static-html.js';
|
|
3
|
-
import {
|
|
4
|
-
import { helixEventNames } from '../events/events.js';
|
|
3
|
+
import { applyReactProperties, auditReactProps, bindReactHandler, reactPropsOf, } from './react-bridge.js';
|
|
5
4
|
const BOX_SIZING = css `
|
|
6
5
|
*,
|
|
7
6
|
::before,
|
|
@@ -25,103 +24,15 @@ const SETTLED = css `
|
|
|
25
24
|
transition: none !important;
|
|
26
25
|
}
|
|
27
26
|
`;
|
|
28
|
-
const REACT_PROPS = '__reactProps$';
|
|
29
|
-
let reactPropsKey;
|
|
30
|
-
const reactPropsOf = (node) => {
|
|
31
|
-
const stash = node;
|
|
32
|
-
if (reactPropsKey !== undefined && stash[reactPropsKey] !== undefined) {
|
|
33
|
-
return stash[reactPropsKey];
|
|
34
|
-
}
|
|
35
|
-
const found = Object.keys(stash).find((key) => key.startsWith(REACT_PROPS));
|
|
36
|
-
if (found === undefined) {
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
reactPropsKey = found;
|
|
40
|
-
return stash[found];
|
|
41
|
-
};
|
|
42
|
-
const audited = new WeakSet();
|
|
43
|
-
const REMOVED = new Set(['onActiveChange', 'onCheckedChange', 'onOpenChange', 'onValueChange']);
|
|
44
|
-
const auditReactProps = (host, props) => {
|
|
45
|
-
if (audited.has(host)) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
audited.add(host);
|
|
49
|
-
const tag = host instanceof Element ? `<${host.localName}>` : 'A Helix element';
|
|
50
|
-
for (const key of Object.keys(props)) {
|
|
51
|
-
if (key.startsWith('on:')) {
|
|
52
|
-
console.warn(`${tag} was given \`${key}\`, which is Solid's syntax. React binds the name after \`on\` ` +
|
|
53
|
-
`verbatim, so this listens for an event called \`${key.slice(2)}\`. Write \`on${key.slice(3)}\`.`);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (REMOVED.has(key)) {
|
|
57
|
-
console.warn(`${tag} was given \`${key}\`, which was removed. It never worked from JSX, because React ` +
|
|
58
|
-
`binds any \`on\` prop as an event of that name. Listen for the element's change event instead.`);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (!key.startsWith('on') || !key.includes('-')) {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const name = key.slice(2);
|
|
65
|
-
if (!helixEventNames.has(name)) {
|
|
66
|
-
console.warn(`${tag} was given \`${key}\`, and nothing dispatches \`${name}\`. TypeScript cannot ` +
|
|
67
|
-
`check a hyphenated JSX prop, so this is the only place it shows up.`);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
const mine = host instanceof Element ? eventsForTag(host.localName) : undefined;
|
|
71
|
-
if (mine && !mine.has(name)) {
|
|
72
|
-
console.warn(`${tag} was given \`${key}\`, but it dispatches ${[...mine].map((one) => `\`${one}\``).join(', ')}. ` +
|
|
73
|
-
`\`${name}\` is sent by a different element, and React only wires an \`on\` prop on the ` +
|
|
74
|
-
'element that dispatches, so this handler will never run. Put it on that element.');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
const REACT_WIRES = new Set(['scroll', 'scrollend']);
|
|
79
|
-
const reactPropFor = (event) => {
|
|
80
|
-
if (event.type.includes('-')) {
|
|
81
|
-
return `on${event.type}`;
|
|
82
|
-
}
|
|
83
|
-
if (event.bubbles || REACT_WIRES.has(event.type)) {
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
return `on${event.type.charAt(0).toUpperCase()}${event.type.slice(1)}`;
|
|
87
|
-
};
|
|
88
|
-
const bindReactHandler = (host, event) => {
|
|
89
|
-
const props = reactPropsOf(host);
|
|
90
|
-
if (props === undefined) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
auditReactProps(host, props);
|
|
94
|
-
const key = reactPropFor(event);
|
|
95
|
-
const handler = key === undefined ? undefined : props[key];
|
|
96
|
-
if (typeof handler === 'function') {
|
|
97
|
-
host.addEventListener(event.type, handler);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
const applyReactProperties = (host) => {
|
|
101
|
-
const props = reactPropsOf(host);
|
|
102
|
-
if (props === undefined) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
const target = host;
|
|
106
|
-
for (const name of host.constructor.elementProperties.keys()) {
|
|
107
|
-
if (typeof name !== 'string' || !Object.hasOwn(props, name)) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const value = props[name];
|
|
111
|
-
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (target[name] !== value) {
|
|
115
|
-
target[name] = value;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return true;
|
|
119
|
-
};
|
|
120
27
|
export class HelixElement extends LitElement {
|
|
121
28
|
static { this.tier = 'helix-headless-'; }
|
|
122
29
|
tag(name) {
|
|
123
30
|
return unsafeStatic(`${this.constructor.tier}${name}`);
|
|
124
31
|
}
|
|
32
|
+
partNamed(name) {
|
|
33
|
+
const root = this.renderRoot;
|
|
34
|
+
return typeof root?.querySelector === 'function' ? root.querySelector(`[part="${name}"]`) : null;
|
|
35
|
+
}
|
|
125
36
|
toggleState(name, on) {
|
|
126
37
|
if ('states' in this.internals) {
|
|
127
38
|
this.internals.states[on ? 'add' : 'delete'](name);
|