@kgdiff/viewer 0.4.0 → 0.6.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/README.md +43 -16
- package/custom-elements.json +2347 -0
- package/dist/bind.d.ts +41 -0
- package/dist/bind.d.ts.map +1 -0
- package/dist/bind.js +47 -0
- package/dist/bind.js.map +1 -0
- package/dist/context.d.ts +42 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +87 -0
- package/dist/context.js.map +1 -0
- package/dist/controller.d.ts +26 -237
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +33 -1484
- package/dist/controller.js.map +1 -1
- package/dist/dom.d.ts +10 -0
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +29 -0
- package/dist/dom.js.map +1 -1
- package/dist/element.d.ts +51 -42
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +381 -602
- package/dist/element.js.map +1 -1
- package/dist/elements/actions.d.ts +27 -0
- package/dist/elements/actions.d.ts.map +1 -0
- package/dist/elements/actions.js +81 -0
- package/dist/elements/actions.js.map +1 -0
- package/dist/elements/bound.d.ts +63 -0
- package/dist/elements/bound.d.ts.map +1 -0
- package/dist/elements/bound.js +213 -0
- package/dist/elements/bound.js.map +1 -0
- package/dist/elements/change-detail.d.ts +25 -0
- package/dist/elements/change-detail.d.ts.map +1 -0
- package/dist/elements/change-detail.js +19 -0
- package/dist/elements/change-detail.js.map +1 -0
- package/dist/elements/change-list.d.ts +27 -0
- package/dist/elements/change-list.d.ts.map +1 -0
- package/dist/elements/change-list.js +29 -0
- package/dist/elements/change-list.js.map +1 -0
- package/dist/elements/filters.d.ts +25 -0
- package/dist/elements/filters.d.ts.map +1 -0
- package/dist/elements/filters.js +19 -0
- package/dist/elements/filters.js.map +1 -0
- package/dist/elements/legend.d.ts +40 -0
- package/dist/elements/legend.d.ts.map +1 -0
- package/dist/elements/legend.js +87 -0
- package/dist/elements/legend.js.map +1 -0
- package/dist/elements/minimap.d.ts +38 -0
- package/dist/elements/minimap.d.ts.map +1 -0
- package/dist/elements/minimap.js +59 -0
- package/dist/elements/minimap.js.map +1 -0
- package/dist/elements/root.d.ts +40 -0
- package/dist/elements/root.d.ts.map +1 -0
- package/dist/elements/root.js +67 -0
- package/dist/elements/root.js.map +1 -0
- package/dist/elements/search.d.ts +29 -0
- package/dist/elements/search.d.ts.map +1 -0
- package/dist/elements/search.js +23 -0
- package/dist/elements/search.js.map +1 -0
- package/dist/elements/stage-context.d.ts +31 -0
- package/dist/elements/stage-context.d.ts.map +1 -0
- package/dist/elements/stage-context.js +4 -0
- package/dist/elements/stage-context.js.map +1 -0
- package/dist/elements/stage.d.ts +65 -0
- package/dist/elements/stage.d.ts.map +1 -0
- package/dist/elements/stage.js +259 -0
- package/dist/elements/stage.js.map +1 -0
- package/dist/elements/title.d.ts +25 -0
- package/dist/elements/title.d.ts.map +1 -0
- package/dist/elements/title.js +19 -0
- package/dist/elements/title.js.map +1 -0
- package/dist/elements/transport.d.ts +29 -0
- package/dist/elements/transport.d.ts.map +1 -0
- package/dist/elements/transport.js +24 -0
- package/dist/elements/transport.js.map +1 -0
- package/dist/elements/view-bound.d.ts +39 -0
- package/dist/elements/view-bound.d.ts.map +1 -0
- package/dist/elements/view-bound.js +88 -0
- package/dist/elements/view-bound.js.map +1 -0
- package/dist/elements/view-dock.d.ts +28 -0
- package/dist/elements/view-dock.d.ts.map +1 -0
- package/dist/elements/view-dock.js +28 -0
- package/dist/elements/view-dock.js.map +1 -0
- package/dist/elements/zoom.d.ts +28 -0
- package/dist/elements/zoom.d.ts.map +1 -0
- package/dist/elements/zoom.js +22 -0
- package/dist/elements/zoom.js.map +1 -0
- package/dist/i18n.d.ts +10 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +20 -0
- package/dist/i18n.js.map +1 -1
- package/dist/icons.d.ts +1 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +1 -0
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +331 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/keyboard.d.ts +39 -2
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +145 -62
- package/dist/keyboard.js.map +1 -1
- package/dist/layout-store.d.ts +26 -0
- package/dist/layout-store.d.ts.map +1 -0
- package/dist/layout-store.js +46 -0
- package/dist/layout-store.js.map +1 -0
- package/dist/panel-levels.d.ts +18 -0
- package/dist/panel-levels.d.ts.map +1 -0
- package/dist/panel-levels.js +41 -0
- package/dist/panel-levels.js.map +1 -0
- package/dist/panels.d.ts +28 -0
- package/dist/panels.d.ts.map +1 -0
- package/dist/panels.js +73 -0
- package/dist/panels.js.map +1 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +3 -2
- package/dist/pipeline.js.map +1 -1
- package/dist/prefs.d.ts +5 -2
- package/dist/prefs.d.ts.map +1 -1
- package/dist/prefs.js +12 -3
- package/dist/prefs.js.map +1 -1
- package/dist/react.d.ts +460 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +2 -0
- package/dist/react.js.map +1 -0
- package/dist/root-element.d.ts +122 -0
- package/dist/root-element.d.ts.map +1 -0
- package/dist/root-element.js +541 -0
- package/dist/root-element.js.map +1 -0
- package/dist/root.d.ts +54 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +288 -0
- package/dist/root.js.map +1 -0
- package/dist/schedule.d.ts +22 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +41 -0
- package/dist/schedule.js.map +1 -0
- package/dist/session.d.ts +269 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +1237 -0
- package/dist/session.js.map +1 -0
- package/dist/standalone/kgdiff-viewer-compute.js +1 -1
- package/dist/standalone/kgdiff-viewer.js +4 -4
- package/dist/styles.d.ts +17 -6
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +367 -258
- package/dist/styles.js.map +1 -1
- package/dist/theme.d.ts +32 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +152 -0
- package/dist/theme.js.map +1 -0
- package/dist/topics.d.ts +8 -0
- package/dist/topics.d.ts.map +1 -0
- package/dist/topics.js +18 -0
- package/dist/topics.js.map +1 -0
- package/dist/ui/change-list.d.ts +18 -0
- package/dist/ui/change-list.d.ts.map +1 -0
- package/dist/ui/change-list.js +331 -0
- package/dist/ui/change-list.js.map +1 -0
- package/dist/ui/context.d.ts +34 -7
- package/dist/ui/context.d.ts.map +1 -1
- package/dist/ui/context.js.map +1 -1
- package/dist/ui/filters.d.ts +9 -0
- package/dist/ui/filters.d.ts.map +1 -0
- package/dist/ui/filters.js +68 -0
- package/dist/ui/filters.js.map +1 -0
- package/dist/ui/header.d.ts +41 -3
- package/dist/ui/header.d.ts.map +1 -1
- package/dist/ui/header.js +101 -36
- package/dist/ui/header.js.map +1 -1
- package/dist/ui/legend.d.ts +15 -0
- package/dist/ui/legend.d.ts.map +1 -0
- package/dist/ui/legend.js +68 -0
- package/dist/ui/legend.js.map +1 -0
- package/dist/ui/popovers.d.ts +3 -1
- package/dist/ui/popovers.d.ts.map +1 -1
- package/dist/ui/popovers.js +62 -23
- package/dist/ui/popovers.js.map +1 -1
- package/dist/ui/search.d.ts +13 -0
- package/dist/ui/search.d.ts.map +1 -0
- package/dist/ui/search.js +192 -0
- package/dist/ui/search.js.map +1 -0
- package/dist/ui/sidebar.d.ts +32 -17
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +327 -519
- package/dist/ui/sidebar.js.map +1 -1
- package/dist/ui/stage.d.ts +56 -1
- package/dist/ui/stage.d.ts.map +1 -1
- package/dist/ui/stage.js +244 -256
- package/dist/ui/stage.js.map +1 -1
- package/dist/ui/swatch.js +1 -1
- package/dist/ui/swatch.js.map +1 -1
- package/dist/ui/transport.d.ts +12 -0
- package/dist/ui/transport.d.ts.map +1 -0
- package/dist/ui/transport.js +198 -0
- package/dist/ui/transport.js.map +1 -0
- package/dist/ui/zoom.d.ts +15 -0
- package/dist/ui/zoom.d.ts.map +1 -0
- package/dist/ui/zoom.js +30 -0
- package/dist/ui/zoom.js.map +1 -0
- package/dist/view.d.ts +94 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/view.js +441 -0
- package/dist/view.js.map +1 -0
- package/package.json +95 -12
- package/src/bind.ts +80 -0
- package/src/context.ts +106 -0
- package/src/controller.ts +52 -1630
- package/src/dom.ts +30 -0
- package/src/element.ts +386 -619
- package/src/elements/actions.ts +94 -0
- package/src/elements/bound.ts +247 -0
- package/src/elements/change-detail.ts +34 -0
- package/src/elements/change-list.ts +51 -0
- package/src/elements/filters.ts +34 -0
- package/src/elements/legend.ts +103 -0
- package/src/elements/minimap.ts +75 -0
- package/src/elements/root.ts +86 -0
- package/src/elements/search.ts +38 -0
- package/src/elements/stage-context.ts +34 -0
- package/src/elements/stage.ts +285 -0
- package/src/elements/title.ts +34 -0
- package/src/elements/transport.ts +39 -0
- package/src/elements/view-bound.ts +118 -0
- package/src/elements/view-dock.ts +43 -0
- package/src/elements/zoom.ts +38 -0
- package/src/i18n.ts +20 -0
- package/src/icons.ts +1 -0
- package/src/index.ts +350 -35
- package/src/keyboard.ts +188 -61
- package/src/layout-store.ts +71 -0
- package/src/panel-levels.ts +53 -0
- package/src/panels.ts +81 -0
- package/src/pipeline.ts +4 -1
- package/src/prefs.ts +12 -4
- package/src/react.ts +459 -0
- package/src/root-element.ts +624 -0
- package/src/root.ts +335 -0
- package/src/schedule.ts +43 -0
- package/src/session.ts +1375 -0
- package/src/styles.ts +396 -263
- package/src/theme.ts +170 -0
- package/src/topics.ts +37 -0
- package/src/ui/change-list.ts +405 -0
- package/src/ui/context.ts +36 -7
- package/src/ui/filters.ts +79 -0
- package/src/ui/header.ts +129 -58
- package/src/ui/legend.ts +112 -0
- package/src/ui/popovers.ts +68 -25
- package/src/ui/search.ts +225 -0
- package/src/ui/sidebar.ts +362 -639
- package/src/ui/stage.ts +267 -331
- package/src/ui/swatch.ts +1 -1
- package/src/ui/transport.ts +240 -0
- package/src/ui/zoom.ts +45 -0
- package/src/view.ts +516 -0
- package/vscode.html-custom-data.json +726 -0
package/dist/ui/stage.js
CHANGED
|
@@ -1,37 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The canvas area and its floating chrome: the view dock (with replay's caption and
|
|
3
|
-
* stacked above it), swipe divider, legend (a card, or one line of dots
|
|
4
|
-
* zoom controls + minimap (shown only while part of the
|
|
5
|
-
* and the loading / identical / error / empty states.
|
|
6
|
-
* fits and flights frame it in the free part of the
|
|
2
|
+
* The canvas area and its floating chrome: the view dock (`dock.ts`, with replay's caption and
|
|
3
|
+
* transport stacked above it), swipe divider, legend (`legend.ts`: a card, or one line of dots
|
|
4
|
+
* for chrome-less embeds), zoom controls (`zoom.ts`) + minimap (shown only while part of the
|
|
5
|
+
* content is out of view), hover tooltip and the loading / identical / error / empty states.
|
|
6
|
+
* Overlays never cover the current hunk: fits and flights frame it in the free part of the
|
|
7
|
+
* canvas. The view (`view` once attached) draws the canvas; the chrome reads the session.
|
|
8
|
+
*
|
|
9
|
+
* With slots (`<kgdiff-stage>`, and `<kgdiff-viewer>` for the page's content), a chrome element
|
|
10
|
+
* slotted in (`<kgdiff-transport slot="bottom-center">`) replaces the built-in one.
|
|
7
11
|
*/
|
|
8
12
|
import { KgdiffError } from '@kgdiff/core';
|
|
9
|
-
import { attr, h, icon,
|
|
10
|
-
import { FAMILIES } from '../styles.js';
|
|
13
|
+
import { attr, h, icon, moveFocusOut, replace, setText } from '../dom.js';
|
|
11
14
|
import { sceneHunkGlyph } from './context.js';
|
|
12
15
|
import { Dock } from './dock.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
16
|
+
import { Legend } from './legend.js';
|
|
17
|
+
import { Transport } from './transport.js';
|
|
18
|
+
import { ZoomBar } from './zoom.js';
|
|
19
|
+
/** The named slots of `<kgdiff-stage>`, where the page puts its own content over the canvas. */
|
|
20
|
+
export const STAGE_SLOTS = [
|
|
21
|
+
'top-start',
|
|
22
|
+
'top-end',
|
|
23
|
+
'bottom-start',
|
|
24
|
+
'bottom-center',
|
|
25
|
+
'bottom-end',
|
|
26
|
+
];
|
|
27
|
+
/** The chrome elements, by the region a slotted one replaces. */
|
|
28
|
+
const CHROME_TAGS = {
|
|
29
|
+
'kgdiff-view-dock': 'dock',
|
|
30
|
+
'kgdiff-transport': 'transport',
|
|
31
|
+
'kgdiff-legend': 'legend',
|
|
32
|
+
'kgdiff-zoom': 'zoom',
|
|
33
|
+
'kgdiff-minimap': 'minimap',
|
|
34
|
+
'kgdiff-title': 'title',
|
|
35
|
+
'kgdiff-actions': 'actions',
|
|
36
|
+
};
|
|
15
37
|
export class Stage {
|
|
16
38
|
el;
|
|
17
39
|
canvasHost;
|
|
18
40
|
minimapHost;
|
|
41
|
+
#view = null;
|
|
42
|
+
/** Removes the stage's own minimap from the view; null while it has none. */
|
|
43
|
+
#ownMinimap = null;
|
|
44
|
+
/** The view drawing this stage's canvas (its camera, minimap, framing), once attached. */
|
|
45
|
+
get view() {
|
|
46
|
+
return this.#view;
|
|
47
|
+
}
|
|
48
|
+
set view(view) {
|
|
49
|
+
if (view === this.#view)
|
|
50
|
+
return;
|
|
51
|
+
this.#ownMinimap?.();
|
|
52
|
+
this.#ownMinimap = null;
|
|
53
|
+
this.#view = view;
|
|
54
|
+
this.#syncMinimap();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The view draws the stage's own minimap unless a slotted `<kgdiff-minimap>` replaces it:
|
|
58
|
+
* none is created, or drawn, while one is, and it comes back when that one leaves.
|
|
59
|
+
*/
|
|
60
|
+
#syncMinimap() {
|
|
61
|
+
const view = this.#view;
|
|
62
|
+
const own = view !== null && !this.#replaced.has('minimap');
|
|
63
|
+
if (own && !this.#ownMinimap)
|
|
64
|
+
this.#ownMinimap = view.addMinimap(this.minimapHost);
|
|
65
|
+
else if (!own && this.#ownMinimap) {
|
|
66
|
+
this.#ownMinimap();
|
|
67
|
+
this.#ownMinimap = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
19
70
|
#chrome;
|
|
20
71
|
#liveCanvasLabel;
|
|
21
72
|
// Replay
|
|
22
73
|
#caption;
|
|
23
|
-
|
|
24
|
-
#playButton;
|
|
25
|
-
#prevButton;
|
|
26
|
-
#nextButton;
|
|
27
|
-
#scrubber;
|
|
28
|
-
#fill;
|
|
29
|
-
#knob;
|
|
30
|
-
#ticks;
|
|
31
|
-
#scrubTip;
|
|
32
|
-
#pos;
|
|
33
|
-
#speed;
|
|
34
|
-
#finished;
|
|
74
|
+
transport;
|
|
35
75
|
// Swipe
|
|
36
76
|
#swipe;
|
|
37
77
|
#divider;
|
|
@@ -41,24 +81,37 @@ export class Stage {
|
|
|
41
81
|
dock;
|
|
42
82
|
#dockWrap;
|
|
43
83
|
// Chrome
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#legendBody;
|
|
47
|
-
#miniLegend;
|
|
48
|
-
#zoomIn;
|
|
49
|
-
#zoomOut;
|
|
50
|
-
#fitButton;
|
|
84
|
+
legend;
|
|
85
|
+
zoom;
|
|
51
86
|
#mapCluster;
|
|
52
|
-
#zoomBar;
|
|
53
87
|
tooltip;
|
|
54
88
|
#state;
|
|
55
89
|
#pointer = null;
|
|
56
|
-
#scrubbing = null;
|
|
57
90
|
#dragging = null;
|
|
58
|
-
#legendOpen = false;
|
|
59
91
|
#stateKey = '';
|
|
92
|
+
#slots = [];
|
|
93
|
+
/** Elements over the canvas that registered themselves (`registerObstacle`). */
|
|
94
|
+
#obstacles = new Set();
|
|
95
|
+
#version = null;
|
|
96
|
+
#versionChip = h('div', { class: 'version-chip chip card', hidden: true });
|
|
97
|
+
/** The chrome's panel states, and the regions a slotted chrome element replaces. */
|
|
98
|
+
#states = null;
|
|
99
|
+
#replaced = new Set();
|
|
100
|
+
/**
|
|
101
|
+
* Called when the regions slotted elements replace change: the layout that draws the title
|
|
102
|
+
* and the actions (`<kgdiff-viewer>`) applies its panels again.
|
|
103
|
+
*/
|
|
104
|
+
onReplaced = null;
|
|
105
|
+
/** Whether the stage is a named region (`asRegion`) rather than the main landmark. */
|
|
106
|
+
#region = false;
|
|
107
|
+
/** The legend's state applied last: the user's toggle stands until it changes. */
|
|
108
|
+
#legendState = null;
|
|
60
109
|
#ctx;
|
|
61
|
-
|
|
110
|
+
/**
|
|
111
|
+
* `slots`: the stage has named slots for the page's content (stacked with the built-in chrome
|
|
112
|
+
* in its corner).
|
|
113
|
+
*/
|
|
114
|
+
constructor(ctx, slots = false) {
|
|
62
115
|
this.#ctx = ctx;
|
|
63
116
|
const c = ctx.controller;
|
|
64
117
|
this.canvasHost = h('div', { class: 'stage' });
|
|
@@ -69,32 +122,7 @@ export class Stage {
|
|
|
69
122
|
'aria-hidden': 'true',
|
|
70
123
|
'data-empty': 'true',
|
|
71
124
|
});
|
|
72
|
-
this
|
|
73
|
-
this.#playButton = iconButton('play', '', () => c.togglePlay(), { class: 'icon-btn play' });
|
|
74
|
-
this.#nextButton = iconButton('skip-forward', '', () => c.stepReplay(1), { class: 'icon-btn' });
|
|
75
|
-
this.#fill = h('div', { class: 'fill' });
|
|
76
|
-
this.#knob = h('div', { class: 'knob' });
|
|
77
|
-
this.#ticks = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
78
|
-
this.#ticks.setAttribute('class', 'ticks');
|
|
79
|
-
this.#ticks.setAttribute('viewBox', `0 0 ${TICK_W} 14`);
|
|
80
|
-
this.#ticks.setAttribute('preserveAspectRatio', 'none');
|
|
81
|
-
this.#ticks.setAttribute('aria-hidden', 'true');
|
|
82
|
-
this.#scrubTip = h('div', { class: 'scrub-tip card', hidden: true });
|
|
83
|
-
this.#scrubber = h('div', {
|
|
84
|
-
class: 'scrubber',
|
|
85
|
-
role: 'slider',
|
|
86
|
-
tabindex: '0',
|
|
87
|
-
'aria-valuemin': '0',
|
|
88
|
-
}, h('div', { class: 'track' }, this.#fill), this.#ticks, this.#knob, this.#scrubTip);
|
|
89
|
-
this.#bindScrubber();
|
|
90
|
-
this.#pos = h('span', { class: 'pos', 'aria-hidden': 'true' });
|
|
91
|
-
this.#speed = h('button', {
|
|
92
|
-
type: 'button',
|
|
93
|
-
class: 'speed',
|
|
94
|
-
onclick: () => c.cycleSpeed(),
|
|
95
|
-
});
|
|
96
|
-
this.#transport = h('div', { class: 'transport card', role: 'toolbar' }, this.#prevButton, this.#playButton, this.#nextButton, this.#scrubber, this.#pos, this.#speed);
|
|
97
|
-
this.#finished = h('div', { class: 'finished card', role: 'status', hidden: true });
|
|
125
|
+
this.transport = new Transport(ctx);
|
|
98
126
|
// Swipe
|
|
99
127
|
this.#chipBefore = h('div', { class: 'side-chip before chip card' });
|
|
100
128
|
this.#chipAfter = h('div', { class: 'side-chip after chip card' });
|
|
@@ -110,28 +138,34 @@ export class Stage {
|
|
|
110
138
|
this.#swipe = h('div', { class: 'swipe', hidden: true }, this.#chipBefore, this.#chipAfter, this.#divider);
|
|
111
139
|
// View dock, with replay's finished pill, caption and transport stacked above it.
|
|
112
140
|
this.dock = new Dock(ctx);
|
|
113
|
-
this.#dockWrap = h('div', { class: 'dock' }, this
|
|
141
|
+
this.#dockWrap = h('div', { class: 'dock' }, this.transport.finished, this.#caption, this.transport.el, this.dock.el);
|
|
114
142
|
// Legend
|
|
115
|
-
this
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
'aria-expanded': 'false',
|
|
119
|
-
onclick: () => this.#setLegend(!this.#legendOpen),
|
|
120
|
-
}, h('span', { class: 'lbl' }), icon('chevron-down'));
|
|
121
|
-
this.#legendBody = h('div', { class: 'legend-body', id: 'kg-legend' });
|
|
122
|
-
attr(this.#legendHead, 'aria-controls', 'kg-legend');
|
|
123
|
-
this.#legend = h('div', { class: 'legend card float bottom-left', 'data-open': 'false' }, this.#legendHead, this.#legendBody);
|
|
124
|
-
this.#miniLegend = h('div', { class: 'mini-legend float bottom-left', role: 'list' });
|
|
125
|
-
// Zoom + minimap
|
|
126
|
-
this.#zoomIn = iconButton('plus', '', () => c.zoom(1));
|
|
127
|
-
this.#zoomOut = iconButton('minus', '', () => c.zoom(-1));
|
|
128
|
-
this.#fitButton = iconButton('fit', '', () => void c.fit());
|
|
143
|
+
this.legend = new Legend(ctx, (open) => ctx.setPanel?.('legend', open ? 'open' : 'collapsed'));
|
|
144
|
+
// Zoom + minimap: the buttons act on this stage's view.
|
|
145
|
+
this.zoom = new ZoomBar(ctx, () => this.view);
|
|
129
146
|
this.minimapHost = h('div', { class: 'minimap-host' });
|
|
130
|
-
this.#
|
|
131
|
-
this.#mapCluster = h('div', { class: 'map-cluster float bottom-right' }, this.#zoomBar, this.minimapHost);
|
|
147
|
+
this.#mapCluster = h('div', { class: 'map-cluster float bottom-right' }, this.zoom.el, this.minimapHost);
|
|
132
148
|
this.tooltip = h('div', { class: 'tooltip card', role: 'tooltip', 'data-open': 'false' });
|
|
133
149
|
this.#state = h('div', { class: 'state', hidden: true });
|
|
134
|
-
|
|
150
|
+
let bottomStart = this.legend.card;
|
|
151
|
+
let bottomEnd = this.#mapCluster;
|
|
152
|
+
const corners = [];
|
|
153
|
+
if (slots) {
|
|
154
|
+
const slot = (name) => {
|
|
155
|
+
const el = document.createElement('slot');
|
|
156
|
+
el.name = name;
|
|
157
|
+
el.addEventListener('slotchange', () => this.slotted());
|
|
158
|
+
this.#slots.push(el);
|
|
159
|
+
return el;
|
|
160
|
+
};
|
|
161
|
+
// Slotted content sits further from the corner than the built-in chrome.
|
|
162
|
+
corners.push(h('div', { class: 'corner top-start' }, this.#versionChip, slot('top-start')), h('div', { class: 'corner top-end' }, slot('top-end')));
|
|
163
|
+
bottomStart = h('div', { class: 'corner bottom-start' }, slot('bottom-start'), this.legend.card);
|
|
164
|
+
bottomEnd = h('div', { class: 'corner bottom-end' }, slot('bottom-end'), this.#mapCluster);
|
|
165
|
+
this.#dockWrap.prepend(slot('bottom-center'));
|
|
166
|
+
}
|
|
167
|
+
// The top corners first: the chrome reads and tabs from the top down.
|
|
168
|
+
this.#chrome = h('div', { class: 'chrome' }, ...corners, this.#swipe, bottomStart, this.legend.mini, bottomEnd, this.#dockWrap, this.tooltip);
|
|
135
169
|
this.el = h('main', { class: 'stage-wrap' }, this.canvasHost, this.#liveCanvasLabel, this.#chrome, this.#state);
|
|
136
170
|
this.el.addEventListener('pointermove', (e) => {
|
|
137
171
|
const r = this.el.getBoundingClientRect();
|
|
@@ -152,22 +186,27 @@ export class Stage {
|
|
|
152
186
|
const r = this.el.getBoundingClientRect();
|
|
153
187
|
const obstacles = [];
|
|
154
188
|
const parts = [
|
|
155
|
-
this
|
|
156
|
-
this
|
|
157
|
-
this
|
|
189
|
+
this.legend.card,
|
|
190
|
+
this.legend.mini,
|
|
191
|
+
this.zoom.el,
|
|
158
192
|
this.dock.el,
|
|
159
|
-
this
|
|
160
|
-
this
|
|
193
|
+
this.transport.el,
|
|
194
|
+
this.transport.finished,
|
|
161
195
|
this.#chipBefore,
|
|
162
196
|
this.#chipAfter,
|
|
163
197
|
];
|
|
164
198
|
if (this.#caption.dataset.empty !== 'true')
|
|
165
199
|
parts.push(this.#caption);
|
|
200
|
+
parts.push(this.#versionChip);
|
|
166
201
|
if (this.minimapHost.dataset.hidden !== 'true')
|
|
167
202
|
parts.push(this.minimapHost);
|
|
168
203
|
// The HUD (title, actions) floats over the canvas too.
|
|
169
204
|
for (const el of this.el.querySelectorAll('.hud .card'))
|
|
170
205
|
parts.push(el);
|
|
206
|
+
// The page's content in the slots, and what registered itself: one rule for all.
|
|
207
|
+
for (const slot of this.#slots)
|
|
208
|
+
parts.push(...slot.assignedElements({ flatten: true }));
|
|
209
|
+
parts.push(...this.#obstacles);
|
|
171
210
|
for (const el of parts) {
|
|
172
211
|
if (el.hidden || el.closest('[hidden]'))
|
|
173
212
|
continue;
|
|
@@ -178,14 +217,118 @@ export class Stage {
|
|
|
178
217
|
}
|
|
179
218
|
return { width: r.width, height: r.height, obstacles };
|
|
180
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Keep `el` clear of fits and flights, as the chrome is (content over the canvas the stage
|
|
222
|
+
* does not place itself); returns the function that stops it.
|
|
223
|
+
*/
|
|
224
|
+
registerObstacle(el) {
|
|
225
|
+
this.#obstacles.add(el);
|
|
226
|
+
return () => {
|
|
227
|
+
this.#obstacles.delete(el);
|
|
228
|
+
};
|
|
229
|
+
}
|
|
181
230
|
/** Re-measure what depends on rendered sizes (fonts, resizes). */
|
|
182
231
|
layout() {
|
|
183
232
|
this.dock.layout();
|
|
184
233
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
234
|
+
/**
|
|
235
|
+
* The page's slotted content changed (and once the stage is in its shadow root): a chrome
|
|
236
|
+
* element replaces the built-in one.
|
|
237
|
+
*/
|
|
238
|
+
slotted() {
|
|
239
|
+
const replaced = new Set();
|
|
240
|
+
for (const slot of this.#slots) {
|
|
241
|
+
for (const el of slot.assignedElements({ flatten: true })) {
|
|
242
|
+
const region = CHROME_TAGS[el.localName];
|
|
243
|
+
if (region)
|
|
244
|
+
replaced.add(region);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const same = replaced.size === this.#replaced.size && [...replaced].every((r) => this.#replaced.has(r));
|
|
248
|
+
if (!same) {
|
|
249
|
+
this.#replaced.clear();
|
|
250
|
+
for (const r of replaced)
|
|
251
|
+
this.#replaced.add(r);
|
|
252
|
+
this.#applyPanels();
|
|
253
|
+
this.#syncMinimap();
|
|
254
|
+
this.onReplaced?.();
|
|
255
|
+
}
|
|
256
|
+
this.view?.layoutChanged();
|
|
257
|
+
}
|
|
258
|
+
/** Whether a chrome element slotted into the stage replaces `region`. */
|
|
259
|
+
replaces(region) {
|
|
260
|
+
return this.#replaced.has(region);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Follow the panel states of the chrome over the canvas: the dock, the replay transport, the
|
|
264
|
+
* legend (collapsed: folded to its head), the zoom buttons and the minimap. A region that is
|
|
265
|
+
* not open is not shown (and inert); focus inside it moves to the canvas first. A region a
|
|
266
|
+
* slotted chrome element replaces is not shown either, whatever its state.
|
|
267
|
+
*/
|
|
268
|
+
setPanels(states) {
|
|
269
|
+
this.#states = states;
|
|
270
|
+
this.#applyPanels();
|
|
271
|
+
}
|
|
272
|
+
#applyPanels() {
|
|
273
|
+
const given = this.#states;
|
|
274
|
+
const state = (r) => this.#replaced.has(r) ? 'hidden' : (given?.[r] ?? (r === 'legend' ? 'collapsed' : 'open'));
|
|
275
|
+
// Nothing declared or replaced, ever: the chrome stays as built.
|
|
276
|
+
if (!given && this.#replaced.size === 0 && this.#legendState === null)
|
|
277
|
+
return;
|
|
278
|
+
const canvas = () => this.canvasHost.querySelector('canvas');
|
|
279
|
+
const set = (s, ...els) => {
|
|
280
|
+
for (const el of els) {
|
|
281
|
+
if (s !== 'open')
|
|
282
|
+
moveFocusOut(this.#ctx.root, el, canvas);
|
|
283
|
+
attr(el, 'data-panel', s);
|
|
284
|
+
el.inert = s !== 'open';
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
set(state('dock'), this.dock.el);
|
|
288
|
+
set(state('transport'), this.transport.el, this.transport.finished);
|
|
289
|
+
set(state('zoom'), this.zoom.el);
|
|
290
|
+
set(state('minimap'), this.minimapHost);
|
|
291
|
+
const legend = state('legend');
|
|
292
|
+
if (legend === 'hidden')
|
|
293
|
+
moveFocusOut(this.#ctx.root, this.legend.card, canvas);
|
|
294
|
+
attr(this.legend.card, 'data-panel', legend);
|
|
295
|
+
attr(this.legend.mini, 'data-panel', legend);
|
|
296
|
+
this.legend.card.inert = legend === 'hidden';
|
|
297
|
+
if (legend !== 'hidden' && legend !== this.#legendState)
|
|
298
|
+
this.legend.setOpen(legend === 'open');
|
|
299
|
+
this.#legendState = legend;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The version a stage pinned to it draws (`<kgdiff-stage version>`): no swipe divider or
|
|
303
|
+
* replay caption then, and a chip naming the version; null follows the mode.
|
|
304
|
+
*/
|
|
305
|
+
setVersion(version) {
|
|
306
|
+
this.#version = version;
|
|
307
|
+
if (version)
|
|
308
|
+
this.el.dataset.version = version;
|
|
309
|
+
else
|
|
310
|
+
delete this.el.dataset.version;
|
|
311
|
+
this.#renderMode();
|
|
312
|
+
this.#renderVersion(this.#ctx.tr());
|
|
313
|
+
}
|
|
314
|
+
#renderVersion(tr) {
|
|
315
|
+
const v = this.#version;
|
|
316
|
+
const version = v ? tr.t(v === 'before' ? 'detail.before' : 'detail.after') : '';
|
|
317
|
+
this.#versionChip.hidden = v === null;
|
|
318
|
+
if (v)
|
|
319
|
+
setText(this.#versionChip, version);
|
|
320
|
+
if (this.#region) {
|
|
321
|
+
attr(this.el, 'aria-label', v ? `${tr.t('stage.label')} · ${version}` : tr.t('stage.label'));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* A region named after the graph (and its version), not the page's main landmark: for a
|
|
326
|
+
* `<kgdiff-stage>`, one of several on a page that has its own.
|
|
327
|
+
*/
|
|
328
|
+
asRegion() {
|
|
329
|
+
this.#region = true;
|
|
330
|
+
this.el.setAttribute('role', 'region');
|
|
331
|
+
this.#renderVersion(this.#ctx.tr());
|
|
189
332
|
}
|
|
190
333
|
/** Legend style, and whether clicks select (the tooltip only hints at clicking then). */
|
|
191
334
|
setChrome(legend, selectable) {
|
|
@@ -197,63 +340,36 @@ export class Stage {
|
|
|
197
340
|
const tr = this.#ctx.tr();
|
|
198
341
|
const all = topics.has('status') || topics.has('data') || topics.has('locale');
|
|
199
342
|
if (all) {
|
|
343
|
+
this.#renderVersion(tr);
|
|
200
344
|
this.#renderLabels(tr);
|
|
201
|
-
this.#renderMiniLegend(tr);
|
|
202
345
|
this.#renderState(tr);
|
|
203
|
-
this.#renderTicks();
|
|
204
346
|
this.#renderCanvasLabel(tr);
|
|
205
347
|
}
|
|
348
|
+
this.legend.update(topics);
|
|
349
|
+
this.zoom.update(topics);
|
|
350
|
+
this.transport.update(topics);
|
|
206
351
|
this.dock.update(topics);
|
|
207
352
|
if (all || topics.has('mode'))
|
|
208
353
|
this.#renderMode();
|
|
209
354
|
if (all || topics.has('overview')) {
|
|
210
|
-
attr(this.minimapHost, 'data-hidden', String(
|
|
355
|
+
attr(this.minimapHost, 'data-hidden', String(this.view?.overview ?? true));
|
|
211
356
|
}
|
|
212
357
|
if (all || topics.has('mode') || topics.has('replay'))
|
|
213
|
-
this.#
|
|
358
|
+
this.#renderCaption(tr);
|
|
214
359
|
if (all || topics.has('mode') || topics.has('swipe'))
|
|
215
360
|
this.#renderSwipe();
|
|
216
361
|
if (all || topics.has('hover') || topics.has('selection') || topics.has('focus')) {
|
|
217
362
|
this.#renderTooltip(tr);
|
|
218
363
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const ready = c.status === 'ready';
|
|
222
|
-
this.#chrome.hidden = !ready;
|
|
364
|
+
// Until a diff is ready the built-in chrome waits; the page's slotted content stays.
|
|
365
|
+
attr(this.#chrome, 'data-ready', String(c.status === 'ready'));
|
|
223
366
|
}
|
|
224
367
|
#renderLabels(tr) {
|
|
225
|
-
relabel(this.#prevButton, `${tr.t('detail.prev')} (←)`);
|
|
226
|
-
relabel(this.#nextButton, `${tr.t('detail.next')} (→)`);
|
|
227
|
-
attr(this.#transport, 'aria-label', tr.t('replay.label'));
|
|
228
|
-
attr(this.#scrubber, 'aria-label', tr.t('replay.scrubber'));
|
|
229
368
|
setText(this.#chipBefore, tr.t('detail.before'));
|
|
230
369
|
setText(this.#chipAfter, tr.t('detail.after'));
|
|
231
370
|
attr(this.#divider, 'aria-label', tr.t('swipe.divider'));
|
|
232
|
-
setText(this.#legendHead.querySelector('.lbl'), tr.t('legend.title'));
|
|
233
|
-
attr(this.#legendHead, 'aria-label', tr.t('legend.toggle'));
|
|
234
|
-
relabel(this.#zoomIn, `${tr.t('action.zoomIn')} (+)`);
|
|
235
|
-
relabel(this.#zoomOut, `${tr.t('action.zoomOut')} (−)`);
|
|
236
|
-
relabel(this.#fitButton, `${tr.t('action.fit')} (F)`);
|
|
237
371
|
attr(this.minimapHost, 'aria-label', tr.t('minimap.label'));
|
|
238
372
|
attr(this.minimapHost, 'title', tr.t('minimap.label'));
|
|
239
|
-
const fams = [...FAMILIES, 'neutral'];
|
|
240
|
-
replace(this.#legendBody, fams.map((f) => h('div', { class: 'legend-row', 'data-family': f, title: tr.t(`familyHint.${f}`) }, swatch(f), h('span', { class: 'name', text: tr.t(`family.${f}`) }))));
|
|
241
|
-
}
|
|
242
|
-
/** One line: the families this diff has, then the unchanged context. */
|
|
243
|
-
#renderMiniLegend(tr) {
|
|
244
|
-
const scene = this.#ctx.controller.scene;
|
|
245
|
-
const present = new Set(scene?.hunks.map((x) => x.family));
|
|
246
|
-
if (scene?.nodes.some((n) => n.status === 'unchanged'))
|
|
247
|
-
present.add('neutral');
|
|
248
|
-
attr(this.#miniLegend, 'aria-label', tr.t('legend.title'));
|
|
249
|
-
replace(this.#miniLegend, [...FAMILIES, 'neutral']
|
|
250
|
-
.filter((f) => present.has(f))
|
|
251
|
-
.map((f) => h('span', {
|
|
252
|
-
class: 'mini-item',
|
|
253
|
-
role: 'listitem',
|
|
254
|
-
'data-family': f,
|
|
255
|
-
title: tr.t(`familyHint.${f}`),
|
|
256
|
-
}, h('span', { class: 'dot', 'aria-hidden': 'true' }), tr.t(`family.${f}`))));
|
|
257
373
|
}
|
|
258
374
|
#renderCanvasLabel(tr) {
|
|
259
375
|
const stats = this.#ctx.controller.diff?.stats;
|
|
@@ -273,39 +389,19 @@ export class Stage {
|
|
|
273
389
|
}
|
|
274
390
|
#renderMode() {
|
|
275
391
|
const mode = this.#ctx.controller.mode;
|
|
276
|
-
|
|
277
|
-
this.#caption.hidden = mode !== 'replay';
|
|
278
|
-
this.#swipe.hidden = mode !== 'swipe';
|
|
392
|
+
const pinned = this.#version !== null;
|
|
393
|
+
this.#caption.hidden = mode !== 'replay' || pinned;
|
|
394
|
+
this.#swipe.hidden = mode !== 'swipe' || pinned;
|
|
279
395
|
}
|
|
280
|
-
|
|
396
|
+
/** The replay caption: the playing hunk (or chapter), else the last one played. */
|
|
397
|
+
#renderCaption(tr) {
|
|
281
398
|
const c = this.#ctx.controller;
|
|
282
399
|
const clock = c.clock;
|
|
283
400
|
const scene = c.scene;
|
|
284
|
-
if (c.mode !== 'replay' || !clock || !scene)
|
|
285
|
-
this.#finished.hidden = true;
|
|
401
|
+
if (c.mode !== 'replay' || !clock || !scene)
|
|
286
402
|
return;
|
|
287
|
-
}
|
|
288
|
-
const p = clock.progress;
|
|
289
|
-
this.#fill.style.transform = `scaleX(${p})`;
|
|
290
|
-
this.#knob.style.left = `${p * 100}%`;
|
|
291
403
|
const position = clock.position();
|
|
292
404
|
const total = clock.total;
|
|
293
|
-
setText(this.#pos, tr.t('replay.position', { i: position, n: total }));
|
|
294
|
-
attr(this.#scrubber, 'aria-valuemax', String(Math.round(clock.duration)));
|
|
295
|
-
attr(this.#scrubber, 'aria-valuenow', String(Math.round(clock.t)));
|
|
296
|
-
attr(this.#scrubber, 'aria-valuetext', tr.t('replay.position', { i: position, n: total }));
|
|
297
|
-
const playing = clock.playing;
|
|
298
|
-
const playLabel = `${tr.t(playing ? 'replay.pause' : 'replay.play')} (Space)`;
|
|
299
|
-
if (this.#playButton.dataset.state !== String(playing)) {
|
|
300
|
-
this.#playButton.dataset.state = String(playing);
|
|
301
|
-
replace(this.#playButton, icon(playing ? 'pause' : 'play'));
|
|
302
|
-
}
|
|
303
|
-
relabel(this.#playButton, playLabel);
|
|
304
|
-
setText(this.#speed, `${clock.speed}×`);
|
|
305
|
-
relabel(this.#speed, tr.t('replay.speed', { s: `${clock.speed}×` }));
|
|
306
|
-
this.#prevButton.disabled = clock.t <= 0;
|
|
307
|
-
this.#nextButton.disabled = clock.t >= clock.duration;
|
|
308
|
-
// Caption: the playing hunk (or chapter), else the last one played.
|
|
309
405
|
let key = '';
|
|
310
406
|
let content = [];
|
|
311
407
|
if (clock.t <= 0) {
|
|
@@ -347,117 +443,9 @@ export class Stage {
|
|
|
347
443
|
attr(this.#caption, 'data-empty', String(key === ''));
|
|
348
444
|
replace(this.#caption, ...content);
|
|
349
445
|
}
|
|
350
|
-
const finished = clock.finished;
|
|
351
|
-
this.#finished.hidden = !finished;
|
|
352
446
|
// The finished pill takes the caption's place above the transport.
|
|
353
|
-
this.#
|
|
354
|
-
|
|
355
|
-
replace(this.#finished, icon('check-circle'), h('span', { text: tr.t('replay.finished') }), h('button', { type: 'button', class: 'btn', onclick: () => c.restart() }, icon('restart'), tr.t('replay.restart')));
|
|
356
|
-
}
|
|
357
|
-
else if (!finished && this.#finished.childElementCount > 0) {
|
|
358
|
-
// Restart was pressed from the pill: keep focus in the transport.
|
|
359
|
-
replaceKeepingFocus(this.#ctx.root, this.#finished, this.#playButton);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
#renderTicks() {
|
|
363
|
-
const c = this.#ctx.controller;
|
|
364
|
-
const clock = c.clock;
|
|
365
|
-
const scene = c.scene;
|
|
366
|
-
while (this.#ticks.firstChild)
|
|
367
|
-
this.#ticks.removeChild(this.#ticks.firstChild);
|
|
368
|
-
if (!clock || !scene || clock.duration <= 0)
|
|
369
|
-
return;
|
|
370
|
-
const paths = new Map();
|
|
371
|
-
for (const w of clock.timeline.windows) {
|
|
372
|
-
const fam = scene.hunks[w.hunk]?.family;
|
|
373
|
-
if (!fam)
|
|
374
|
-
continue;
|
|
375
|
-
const x = ((w.start / clock.duration) * TICK_W).toFixed(1);
|
|
376
|
-
let list = paths.get(fam);
|
|
377
|
-
if (!list) {
|
|
378
|
-
list = [];
|
|
379
|
-
paths.set(fam, list);
|
|
380
|
-
}
|
|
381
|
-
list.push(`M${x} 2V12`);
|
|
382
|
-
}
|
|
383
|
-
for (const [fam, d] of paths) {
|
|
384
|
-
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
385
|
-
p.setAttribute('d', d.join(''));
|
|
386
|
-
p.setAttribute('stroke', `var(--kg-${fam})`);
|
|
387
|
-
p.setAttribute('stroke-width', '2');
|
|
388
|
-
p.setAttribute('stroke-linecap', 'round');
|
|
389
|
-
p.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
390
|
-
this.#ticks.appendChild(p);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
#bindScrubber() {
|
|
394
|
-
const timeAt = (clientX) => {
|
|
395
|
-
const clock = this.#ctx.controller.clock;
|
|
396
|
-
const r = this.#scrubber.getBoundingClientRect();
|
|
397
|
-
if (!clock || r.width <= 0)
|
|
398
|
-
return 0;
|
|
399
|
-
return Math.max(0, Math.min(1, (clientX - r.left) / r.width)) * clock.duration;
|
|
400
|
-
};
|
|
401
|
-
this.#scrubber.addEventListener('pointerdown', (e) => {
|
|
402
|
-
this.#scrubbing = e.pointerId;
|
|
403
|
-
this.#scrubber.setPointerCapture?.(e.pointerId);
|
|
404
|
-
this.#ctx.controller.pause();
|
|
405
|
-
this.#ctx.controller.seek(timeAt(e.clientX));
|
|
406
|
-
});
|
|
407
|
-
this.#scrubber.addEventListener('pointermove', (e) => {
|
|
408
|
-
if (this.#scrubbing === e.pointerId)
|
|
409
|
-
this.#ctx.controller.seek(timeAt(e.clientX));
|
|
410
|
-
this.#showScrubTip(e.clientX, timeAt(e.clientX));
|
|
411
|
-
});
|
|
412
|
-
const end = (e) => {
|
|
413
|
-
if (this.#scrubbing === e.pointerId)
|
|
414
|
-
this.#scrubbing = null;
|
|
415
|
-
};
|
|
416
|
-
this.#scrubber.addEventListener('pointerup', end);
|
|
417
|
-
this.#scrubber.addEventListener('pointercancel', end);
|
|
418
|
-
this.#scrubber.addEventListener('pointerleave', () => {
|
|
419
|
-
this.#scrubTip.hidden = true;
|
|
420
|
-
});
|
|
421
|
-
this.#scrubber.addEventListener('keydown', (e) => {
|
|
422
|
-
const c = this.#ctx.controller;
|
|
423
|
-
if (e.key === 'Home')
|
|
424
|
-
c.seek(0);
|
|
425
|
-
else if (e.key === 'End')
|
|
426
|
-
c.seek(c.clock?.duration ?? 0);
|
|
427
|
-
else
|
|
428
|
-
return;
|
|
429
|
-
e.preventDefault();
|
|
430
|
-
e.stopPropagation();
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
#showScrubTip(clientX, t) {
|
|
434
|
-
const c = this.#ctx.controller;
|
|
435
|
-
const clock = c.clock;
|
|
436
|
-
const scene = c.scene;
|
|
437
|
-
if (!clock || !scene)
|
|
438
|
-
return;
|
|
439
|
-
// Nearest window start: the tick under the pointer.
|
|
440
|
-
let best = -1;
|
|
441
|
-
let bestD = Number.POSITIVE_INFINITY;
|
|
442
|
-
for (const w of clock.timeline.windows) {
|
|
443
|
-
const d = Math.abs(w.start - t);
|
|
444
|
-
if (d < bestD) {
|
|
445
|
-
bestD = d;
|
|
446
|
-
best = w.hunk;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
const hunk = scene.hunks[best];
|
|
450
|
-
if (!hunk) {
|
|
451
|
-
this.#scrubTip.hidden = true;
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
const r = this.#scrubber.getBoundingClientRect();
|
|
455
|
-
this.#scrubTip.hidden = false;
|
|
456
|
-
this.#scrubTip.style.left = `${clientX - r.left}px`;
|
|
457
|
-
if (this.#scrubTip.dataset.hunk !== hunk.id) {
|
|
458
|
-
this.#scrubTip.dataset.hunk = hunk.id;
|
|
459
|
-
replace(this.#scrubTip, h('span', { class: 'glyph-box', 'data-family': hunk.family, text: sceneHunkGlyph(hunk) }), h('span', { text: hunk.title }));
|
|
460
|
-
}
|
|
447
|
+
if (this.#version === null)
|
|
448
|
+
this.#caption.hidden = clock.finished;
|
|
461
449
|
}
|
|
462
450
|
#renderSwipe() {
|
|
463
451
|
const c = this.#ctx.controller;
|