@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/element.js
CHANGED
|
@@ -1,169 +1,116 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<kgdiff-viewer
|
|
3
|
-
*
|
|
2
|
+
* `<kgdiff-viewer>`, the default layout (RFC 0001 §4): a root (`root-element.ts`: the session,
|
|
3
|
+
* keyboard map, live region, events, theme tokens, preferences and methods) and the regions
|
|
4
|
+
* bound to it, the stage with its chrome, the change list, the search, the family filters and
|
|
5
|
+
* the detail panel, placed as they always were (a 360 px sidebar; a bottom sheet below 760 px).
|
|
6
|
+
*
|
|
7
|
+
* Every region has a panel state (`panels.ts`): the host's `panels` attribute or property and
|
|
8
|
+
* the presets `sidebar`, `header` and `chrome` set them, the user changes them (folding the
|
|
9
|
+
* sidebar, the list or the detail panel, opening the legend, dragging the sidebar's width or
|
|
10
|
+
* the list / detail split), and the layout remembers the user's choices, theme, palette and
|
|
11
|
+
* language per origin (`layout-store.ts`; `persist="none"` turns that off). `kgdiff-layout`
|
|
12
|
+
* reports every change.
|
|
4
13
|
*/
|
|
5
|
-
import {
|
|
14
|
+
import { provideContext } from './context.js';
|
|
6
15
|
import { ViewerController } from './controller.js';
|
|
7
|
-
import { h } from './dom.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
16
|
+
import { h, isOpen } from './dom.js';
|
|
17
|
+
import { stageContext } from './elements/stage-context.js';
|
|
18
|
+
import { LAYOUT_KEY, readRemembered, writeRemembered } from './layout-store.js';
|
|
19
|
+
import { presetPanels, resolvePanels, SIDEBAR_REGIONS, samePanels } from './panel-levels.js';
|
|
20
|
+
import { DEFAULT_PANELS, formatPanels, PANEL_REGIONS, parsePanels, toPanels } from './panels.js';
|
|
21
|
+
import { sanitizeView, toChrome, toLegend } from './prefs.js';
|
|
13
22
|
import { defaultStorage } from './review-store.js';
|
|
14
|
-
import {
|
|
23
|
+
import { bindRegion, uiContext } from './root.js';
|
|
24
|
+
import { ROOT_ATTRIBUTES, RootElement } from './root-element.js';
|
|
25
|
+
import { stylesheet } from './styles.js';
|
|
26
|
+
import { ChangeList } from './ui/change-list.js';
|
|
27
|
+
import { Detail } from './ui/detail.js';
|
|
28
|
+
import { Filters } from './ui/filters.js';
|
|
15
29
|
import { Header } from './ui/header.js';
|
|
16
30
|
import { HelpDialog, Menu } from './ui/popovers.js';
|
|
17
|
-
import {
|
|
31
|
+
import { Search } from './ui/search.js';
|
|
32
|
+
import { SIDEBAR_WIDTH, Sidebar } from './ui/sidebar.js';
|
|
18
33
|
import { Stage } from './ui/stage.js';
|
|
19
34
|
/** Below this width the sidebar becomes a bottom sheet. */
|
|
20
35
|
export const COMPACT_WIDTH = 760;
|
|
21
36
|
/** Below this width the title drops the version tags. */
|
|
22
37
|
const NARROW_WIDTH = 1100;
|
|
23
38
|
const OBSERVED = [
|
|
24
|
-
|
|
25
|
-
'scope',
|
|
26
|
-
'theme',
|
|
27
|
-
'palette',
|
|
28
|
-
'locale',
|
|
39
|
+
...ROOT_ATTRIBUTES,
|
|
29
40
|
'sidebar',
|
|
30
|
-
'
|
|
41
|
+
'header',
|
|
31
42
|
'chrome',
|
|
32
43
|
'legend',
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
44
|
+
'panels',
|
|
45
|
+
'persist',
|
|
46
|
+
'persist-key',
|
|
36
47
|
];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
'theme',
|
|
41
|
-
'palette',
|
|
42
|
-
'locale',
|
|
43
|
-
'chrome',
|
|
44
|
-
'legend',
|
|
45
|
-
'loop',
|
|
46
|
-
'loopDelay',
|
|
47
|
-
'pauseOnHover',
|
|
48
|
-
'payload',
|
|
49
|
-
];
|
|
50
|
-
/** Keys the page itself scrolls with: never taken at document level. */
|
|
51
|
-
const PAGE_KEYS = new Set([
|
|
52
|
-
'ArrowUp',
|
|
53
|
-
'ArrowDown',
|
|
54
|
-
'ArrowLeft',
|
|
55
|
-
'ArrowRight',
|
|
56
|
-
'Home',
|
|
57
|
-
'End',
|
|
58
|
-
'PageUp',
|
|
59
|
-
'PageDown',
|
|
60
|
-
' ',
|
|
61
|
-
'Spacebar',
|
|
62
|
-
]);
|
|
63
|
-
/** Connected viewers, in connection order (`global-shortcuts` goes to the first opted-in one). */
|
|
64
|
-
const connected = [];
|
|
65
|
-
function deepFreeze(value) {
|
|
66
|
-
if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {
|
|
67
|
-
for (const v of Object.values(value))
|
|
68
|
-
deepFreeze(v);
|
|
69
|
-
Object.freeze(value);
|
|
70
|
-
}
|
|
71
|
-
return value;
|
|
72
|
-
}
|
|
73
|
-
function media(query) {
|
|
74
|
-
return typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
75
|
-
? window.matchMedia(query)
|
|
76
|
-
: null;
|
|
48
|
+
function warnPanels(invalid) {
|
|
49
|
+
if (invalid.length > 0)
|
|
50
|
+
console.warn(`[kgdiff] ignoring invalid panels: ${invalid.join(' ')}`);
|
|
77
51
|
}
|
|
78
52
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
53
|
+
* @tagname kgdiff-viewer
|
|
54
|
+
* @attr {'open' | 'closed'} sidebar - A preset: `closed` starts with the sidebar folded (`\` and the
|
|
55
|
+
* sidebar button open it), `open` with its four regions open. Reflects the sidebar's state
|
|
56
|
+
* once the user toggles it.
|
|
57
|
+
* @attr {'compact'} header - A preset: `compact` leaves the brand and the compared names out of
|
|
58
|
+
* the canvas, for hosts that show their own (`panels="title:hidden"`).
|
|
59
|
+
* @slot top-start - The page's content over the canvas, top left (under the title while it shows).
|
|
60
|
+
* @slot top-end - The page's content over the canvas, top right (under the actions while they show).
|
|
61
|
+
* @slot bottom-start - The page's content over the canvas, bottom left, above the legend.
|
|
62
|
+
* @slot bottom-center - The page's content above the view dock and the transport.
|
|
63
|
+
* @slot bottom-end - The page's content over the canvas, bottom right, above the zoom buttons and the minimap.
|
|
81
64
|
*/
|
|
82
|
-
|
|
83
|
-
return new Promise((resolve) => {
|
|
84
|
-
let done = false;
|
|
85
|
-
const finish = () => {
|
|
86
|
-
if (done)
|
|
87
|
-
return;
|
|
88
|
-
done = true;
|
|
89
|
-
resolve();
|
|
90
|
-
};
|
|
91
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
92
|
-
requestAnimationFrame(() => setTimeout(finish, 0));
|
|
93
|
-
}
|
|
94
|
-
setTimeout(finish, 50);
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
export class KgdiffViewer extends HTMLElement {
|
|
65
|
+
export class KgdiffViewer extends RootElement {
|
|
98
66
|
static get observedAttributes() {
|
|
99
67
|
return OBSERVED;
|
|
100
68
|
}
|
|
101
|
-
/** @internal */
|
|
102
|
-
controller;
|
|
103
|
-
#shadow;
|
|
104
69
|
#ui = null;
|
|
105
|
-
#currentPayload = null;
|
|
106
|
-
#renderFailed = false;
|
|
107
|
-
#reflecting = false;
|
|
108
70
|
#compact = false;
|
|
109
71
|
#cleanup = [];
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
72
|
+
// Panel states, from the first level that names a region (see `panels.ts`).
|
|
73
|
+
/** What the user changed in this page. */
|
|
74
|
+
#runtime = {};
|
|
75
|
+
/** The host's `panels`. */
|
|
76
|
+
#hostPanels = {};
|
|
77
|
+
/** The presets, from the attributes as the host set them (not as the layout reflects them). */
|
|
78
|
+
#presetAttrs = {
|
|
79
|
+
sidebar: null,
|
|
80
|
+
header: null,
|
|
81
|
+
chrome: null,
|
|
82
|
+
};
|
|
83
|
+
#presets = {};
|
|
84
|
+
/** What the user chose before (`persist`), read at the first connection. */
|
|
85
|
+
#remembered = { panels: {} };
|
|
86
|
+
/** The report's `view.panels`. */
|
|
87
|
+
#report = {};
|
|
88
|
+
#states = DEFAULT_PANELS;
|
|
89
|
+
/** The sidebar's regions that were open when the user folded it. */
|
|
90
|
+
#folded = null;
|
|
91
|
+
/** `kgdiff-layout` reports changes from the first connection on. */
|
|
92
|
+
#layoutReady = false;
|
|
93
|
+
#storage = defaultStorage();
|
|
114
94
|
constructor() {
|
|
115
95
|
super();
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
ready: (stats) => this.#emit('kgdiff-ready', { stats }),
|
|
119
|
-
select: (hunk, node) => {
|
|
120
|
-
this.#emit('kgdiff-select', { hunk, node });
|
|
121
|
-
this.#writeHash();
|
|
122
|
-
},
|
|
123
|
-
mode: (mode, scope) => {
|
|
124
|
-
this.#emit('kgdiff-mode', { mode, scope });
|
|
125
|
-
this.#writeHash();
|
|
126
|
-
},
|
|
127
|
-
review: (hunk, reviewed, progress) => this.#emit('kgdiff-review', { hunk, reviewed, progress }),
|
|
128
|
-
error: (error) => this.#emit('kgdiff-error', { error }),
|
|
129
|
-
announce: (text) => this.#announce(text),
|
|
130
|
-
replayEnd: (loop) => this.#emit('kgdiff-replay-end', { loop }),
|
|
131
|
-
};
|
|
132
|
-
this.controller = new ViewerController(host, {
|
|
133
|
-
now: () => (typeof performance !== 'undefined' ? performance.now() : Date.now()),
|
|
134
|
-
requestFrame: (cb) => typeof requestAnimationFrame === 'function' ? requestAnimationFrame(cb) : 0,
|
|
135
|
-
cancelFrame: (id) => {
|
|
136
|
-
if (typeof cancelAnimationFrame === 'function')
|
|
137
|
-
cancelAnimationFrame(id);
|
|
138
|
-
},
|
|
139
|
-
yieldToPaint: nextPaint,
|
|
140
|
-
storage: defaultStorage(),
|
|
141
|
-
});
|
|
142
|
-
this.controller.locale = toLocale(this.getAttribute('locale')) ?? detectLocale();
|
|
143
|
-
this.controller.subscribe((topics) => this.#render(topics));
|
|
96
|
+
// The layout renders first, then the regions in the order they bound to the root.
|
|
97
|
+
this.root.attach({ update: (topics) => this.#update(topics) });
|
|
144
98
|
}
|
|
145
99
|
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
if (!this.#upgraded) {
|
|
148
|
-
this.#upgraded = true;
|
|
149
|
-
// Properties set before the element was defined shadow the accessors: re-apply them.
|
|
150
|
-
for (const prop of UPGRADED) {
|
|
151
|
-
if (Object.hasOwn(this, prop)) {
|
|
152
|
-
const value = this[prop];
|
|
153
|
-
delete this[prop];
|
|
154
|
-
this[prop] = value;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
100
|
+
connected() {
|
|
158
101
|
if (!this.#ui)
|
|
159
102
|
this.#build();
|
|
160
|
-
connected.push(this);
|
|
161
103
|
const ui = this.#ui;
|
|
162
104
|
if (!ui)
|
|
163
105
|
return;
|
|
164
|
-
this
|
|
106
|
+
if (!this.#layoutReady) {
|
|
107
|
+
this.#loadRemembered();
|
|
108
|
+
this.#layoutReady = true;
|
|
109
|
+
}
|
|
110
|
+
// The stage adds its own minimap to the view (unless a slotted one replaces it).
|
|
111
|
+
this.controller.attach(ui.stage.canvasHost, null);
|
|
165
112
|
// The graph takes keyboard focus (shortcuts apply where focus is), also on click.
|
|
166
|
-
const canvas =
|
|
113
|
+
const canvas = this.#canvas();
|
|
167
114
|
if (canvas) {
|
|
168
115
|
canvas.tabIndex = this.chrome === 'none' ? -1 : 0;
|
|
169
116
|
const focusCanvas = () => {
|
|
@@ -173,177 +120,92 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
173
120
|
canvas.addEventListener('pointerdown', focusCanvas);
|
|
174
121
|
this.#cleanup.push(() => canvas.removeEventListener('pointerdown', focusCanvas));
|
|
175
122
|
}
|
|
176
|
-
this.#watchMedia();
|
|
177
123
|
this.#watchSize();
|
|
178
|
-
const onKey = (e) => this.#onKeyDown(e);
|
|
179
|
-
document.addEventListener('keydown', onKey);
|
|
180
|
-
this.#cleanup.push(() => document.removeEventListener('keydown', onKey));
|
|
181
|
-
const onStorage = (e) => this.controller.syncReview(e.key);
|
|
182
|
-
window.addEventListener('storage', onStorage);
|
|
183
|
-
this.#cleanup.push(() => window.removeEventListener('storage', onStorage));
|
|
184
|
-
const onHash = () => this.#readHash();
|
|
185
|
-
window.addEventListener('hashchange', onHash);
|
|
186
|
-
this.#cleanup.push(() => window.removeEventListener('hashchange', onHash));
|
|
187
124
|
this.controller.setChrome(() => ui.stage.chromeLayout());
|
|
188
125
|
document.fonts?.ready?.then(() => this.#ui?.stage.layout()).catch(() => undefined);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
'status',
|
|
192
|
-
'data',
|
|
193
|
-
'mode',
|
|
194
|
-
'theme',
|
|
195
|
-
'locale',
|
|
196
|
-
'filters',
|
|
197
|
-
'review',
|
|
198
|
-
'selection',
|
|
199
|
-
'focus',
|
|
200
|
-
'swipe',
|
|
201
|
-
'replay',
|
|
202
|
-
'overview',
|
|
203
|
-
]));
|
|
204
|
-
}
|
|
205
|
-
disconnectedCallback() {
|
|
206
|
-
const i = connected.indexOf(this);
|
|
207
|
-
if (i >= 0)
|
|
208
|
-
connected.splice(i, 1);
|
|
126
|
+
}
|
|
127
|
+
disconnected() {
|
|
209
128
|
for (const fn of this.#cleanup.splice(0))
|
|
210
129
|
fn();
|
|
211
130
|
this.controller.detach();
|
|
212
|
-
// Moved while hovered: no pointerleave follows.
|
|
213
|
-
this.#pointerOver = false;
|
|
214
|
-
this.#applyHold();
|
|
215
131
|
}
|
|
216
|
-
|
|
217
|
-
if (this.#reflecting)
|
|
218
|
-
return;
|
|
132
|
+
attributeChanged(name, value, old) {
|
|
219
133
|
switch (name) {
|
|
220
|
-
case 'mode':
|
|
221
|
-
case 'scope':
|
|
222
|
-
case 'theme':
|
|
223
|
-
case 'palette':
|
|
224
|
-
case 'locale':
|
|
225
|
-
this.#applyPreference(name, value, false);
|
|
226
|
-
break;
|
|
227
134
|
case 'sidebar':
|
|
228
|
-
|
|
135
|
+
// The layout reflects the sidebar's state here (after `\`): the page changing the
|
|
136
|
+
// attribute from what it says now is a command, even back to what the page said
|
|
137
|
+
// before; setting what it already says is not.
|
|
138
|
+
this.#preset(name, value, value !== old);
|
|
229
139
|
break;
|
|
230
|
-
case '
|
|
231
|
-
|
|
232
|
-
this.#readHash();
|
|
140
|
+
case 'header':
|
|
141
|
+
this.#preset(name, value);
|
|
233
142
|
break;
|
|
234
143
|
case 'chrome':
|
|
235
|
-
|
|
144
|
+
this.#preset(name, value);
|
|
236
145
|
this.#applyChrome();
|
|
237
146
|
break;
|
|
238
|
-
case '
|
|
239
|
-
|
|
240
|
-
this.controller.setLoop(this.loop ? this.loopDelay : null);
|
|
241
|
-
this.#applyHold();
|
|
242
|
-
break;
|
|
243
|
-
case 'pause-on-hover':
|
|
244
|
-
this.#applyHold();
|
|
245
|
-
break;
|
|
246
|
-
default:
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Apply a preference from an attribute or a property. Valid values are explicit (a report's
|
|
252
|
-
* `view` does not override them); invalid ones fall back to the default (`overlay`, `all`,
|
|
253
|
-
* `auto`, `default`, the browser's language). Properties reflect the normalized value; the
|
|
254
|
-
* legacy mode `changes` reflects as `overlay` + scope `changes`.
|
|
255
|
-
*/
|
|
256
|
-
#applyPreference(name, raw, reflect) {
|
|
257
|
-
const c = this.controller;
|
|
258
|
-
// `mode="changes"` is rewritten to the attribute pair it stands for.
|
|
259
|
-
const legacy = name === 'mode' && raw === LEGACY_CHANGES_MODE;
|
|
260
|
-
let value;
|
|
261
|
-
switch (name) {
|
|
262
|
-
case 'mode': {
|
|
263
|
-
if (legacy) {
|
|
264
|
-
c.explicit.add('scope');
|
|
265
|
-
c.setView('overlay', 'changes');
|
|
266
|
-
value = 'overlay';
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
const v = toMode(raw);
|
|
270
|
-
c.setMode(v ?? 'overlay');
|
|
271
|
-
value = v;
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
case 'scope': {
|
|
275
|
-
const v = toScope(raw);
|
|
276
|
-
c.setScope(v ?? 'all');
|
|
277
|
-
// A locked scope stays `changes`: the attribute says what is shown.
|
|
278
|
-
value = v === null ? null : c.scope;
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
case 'theme': {
|
|
282
|
-
const v = toTheme(raw);
|
|
283
|
-
c.setThemePreference(v ?? 'auto');
|
|
284
|
-
value = v;
|
|
147
|
+
case 'legend':
|
|
148
|
+
this.#applyChrome();
|
|
285
149
|
break;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
value = v;
|
|
150
|
+
case 'panels': {
|
|
151
|
+
const { panels, invalid } = parsePanels(value ?? '');
|
|
152
|
+
warnPanels(invalid);
|
|
153
|
+
this.#setHostPanels(panels);
|
|
291
154
|
break;
|
|
292
155
|
}
|
|
293
|
-
case '
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
156
|
+
case 'persist':
|
|
157
|
+
case 'persist-key':
|
|
158
|
+
// Turned on, or another store, in the page: what the user chose before there applies
|
|
159
|
+
// now, as a host change.
|
|
160
|
+
if (this.#layoutReady && this.persist === 'local')
|
|
161
|
+
this.#remembered = this.#recall();
|
|
162
|
+
this.#resolve('host');
|
|
297
163
|
break;
|
|
298
|
-
}
|
|
299
164
|
default:
|
|
300
|
-
|
|
165
|
+
break;
|
|
301
166
|
}
|
|
302
|
-
if (value === null)
|
|
303
|
-
c.explicit.delete(name);
|
|
304
|
-
else
|
|
305
|
-
c.explicit.add(name);
|
|
306
|
-
if (reflect || legacy)
|
|
307
|
-
this.#reflect(name, value);
|
|
308
|
-
if (legacy)
|
|
309
|
-
this.#reflect('scope', c.scope);
|
|
310
|
-
}
|
|
311
|
-
// ─── Properties ───────────────────────────────────────────────────────────
|
|
312
|
-
get mode() {
|
|
313
|
-
return this.controller.mode;
|
|
314
|
-
}
|
|
315
|
-
set mode(value) {
|
|
316
|
-
this.#applyPreference('mode', value, true);
|
|
317
|
-
}
|
|
318
|
-
get scope() {
|
|
319
|
-
return this.controller.scope;
|
|
320
167
|
}
|
|
321
|
-
|
|
322
|
-
|
|
168
|
+
upgradedProperties() {
|
|
169
|
+
return ['chrome', 'legend', 'panels', 'persist', 'persistKey'];
|
|
323
170
|
}
|
|
324
|
-
|
|
325
|
-
return
|
|
171
|
+
sheets(doc) {
|
|
172
|
+
return [stylesheet(doc)];
|
|
326
173
|
}
|
|
327
|
-
|
|
328
|
-
|
|
174
|
+
createSession(host, deps) {
|
|
175
|
+
return new ViewerController(host, deps);
|
|
329
176
|
}
|
|
330
|
-
|
|
331
|
-
return this.
|
|
177
|
+
keysEnabled() {
|
|
178
|
+
return this.#ui !== null && this.chrome !== 'none';
|
|
332
179
|
}
|
|
333
|
-
|
|
334
|
-
this.#
|
|
180
|
+
interceptKey(e) {
|
|
181
|
+
const ui = this.#ui;
|
|
182
|
+
if (!ui || !(ui.help.open || ui.menu.open))
|
|
183
|
+
return false;
|
|
184
|
+
if (e.key === 'Escape') {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
ui.help.hide();
|
|
187
|
+
this.#toggleMenu(false);
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
335
190
|
}
|
|
336
|
-
|
|
337
|
-
|
|
191
|
+
payloadSet(payload) {
|
|
192
|
+
// Validated (and warned about) once more when it loads.
|
|
193
|
+
this.#report = sanitizeView(payload.view, true)?.panels ?? {};
|
|
194
|
+
this.#resolve('host');
|
|
338
195
|
}
|
|
339
|
-
|
|
340
|
-
this.#
|
|
196
|
+
load(options) {
|
|
197
|
+
this.#report = {};
|
|
198
|
+
this.#resolve('host');
|
|
199
|
+
return super.load(options);
|
|
341
200
|
}
|
|
201
|
+
// ─── Properties ───────────────────────────────────────────────────────────
|
|
342
202
|
get chrome() {
|
|
343
203
|
return toChrome(this.getAttribute('chrome')) ?? 'full';
|
|
344
204
|
}
|
|
345
205
|
set chrome(value) {
|
|
346
|
-
|
|
206
|
+
const v = toChrome(value) === 'none' ? 'none' : null;
|
|
207
|
+
this.reflect('chrome', v);
|
|
208
|
+
this.#preset('chrome', v);
|
|
347
209
|
this.#applyChrome();
|
|
348
210
|
}
|
|
349
211
|
get legend() {
|
|
@@ -351,134 +213,186 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
351
213
|
}
|
|
352
214
|
set legend(value) {
|
|
353
215
|
const v = toLegend(value);
|
|
354
|
-
this
|
|
216
|
+
this.reflect('legend', v === 'auto' ? null : v);
|
|
355
217
|
this.#applyChrome();
|
|
356
218
|
}
|
|
357
|
-
|
|
358
|
-
|
|
219
|
+
/** The host's declaration (the `panels` attribute as an object); `layout` has the states. */
|
|
220
|
+
get panels() {
|
|
221
|
+
return { ...this.#hostPanels };
|
|
359
222
|
}
|
|
360
|
-
set
|
|
361
|
-
|
|
223
|
+
set panels(value) {
|
|
224
|
+
const { panels, invalid } = toPanels(value);
|
|
225
|
+
warnPanels(invalid);
|
|
226
|
+
this.reflect('panels', formatPanels(panels) || null);
|
|
227
|
+
this.#setHostPanels(panels);
|
|
362
228
|
}
|
|
363
|
-
get
|
|
364
|
-
return
|
|
229
|
+
get persist() {
|
|
230
|
+
return this.getAttribute('persist') === 'none' ? 'none' : 'local';
|
|
365
231
|
}
|
|
366
|
-
set
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this.removeAttribute('loop-delay');
|
|
232
|
+
set persist(value) {
|
|
233
|
+
if (value === 'none')
|
|
234
|
+
this.setAttribute('persist', 'none');
|
|
370
235
|
else
|
|
371
|
-
this.
|
|
236
|
+
this.removeAttribute('persist');
|
|
372
237
|
}
|
|
373
|
-
|
|
374
|
-
|
|
238
|
+
/** Where the layout remembers (`persist-key`): a `localStorage` key, `kgdiff:viewer` by default. */
|
|
239
|
+
get persistKey() {
|
|
240
|
+
return this.getAttribute('persist-key') || LAYOUT_KEY;
|
|
375
241
|
}
|
|
376
|
-
set
|
|
377
|
-
if (value)
|
|
378
|
-
this.removeAttribute('
|
|
242
|
+
set persistKey(value) {
|
|
243
|
+
if (!value || value === LAYOUT_KEY)
|
|
244
|
+
this.removeAttribute('persist-key');
|
|
379
245
|
else
|
|
380
|
-
this.setAttribute('
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
246
|
+
this.setAttribute('persist-key', String(value));
|
|
247
|
+
}
|
|
248
|
+
/** Every region's state and the sizes now, as `kgdiff-layout` reports them. */
|
|
249
|
+
get layout() {
|
|
250
|
+
const sidebar = this.#ui?.sidebar;
|
|
251
|
+
return {
|
|
252
|
+
panels: { ...this.#states },
|
|
253
|
+
sidebarWidth: sidebar?.width ?? SIDEBAR_WIDTH,
|
|
254
|
+
detailShare: sidebar?.detailShare ?? null,
|
|
255
|
+
};
|
|
384
256
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
257
|
+
// ─── Panels ───────────────────────────────────────────────────────────────
|
|
258
|
+
/** Some region's state may have changed: apply the new states, and report them. */
|
|
259
|
+
#resolve(source) {
|
|
260
|
+
const remembered = this.persist === 'local' ? this.#remembered.panels : {};
|
|
261
|
+
const next = resolvePanels([
|
|
262
|
+
this.#runtime,
|
|
263
|
+
this.#hostPanels,
|
|
264
|
+
this.#presets,
|
|
265
|
+
remembered,
|
|
266
|
+
this.#report,
|
|
267
|
+
]);
|
|
268
|
+
if (samePanels(next, this.#states))
|
|
388
269
|
return;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
// ─── Methods ──────────────────────────────────────────────────────────────
|
|
402
|
-
load(options) {
|
|
403
|
-
this.#currentPayload = null;
|
|
404
|
-
return this.controller.load(options);
|
|
405
|
-
}
|
|
406
|
-
selectHunk(id) {
|
|
407
|
-
this.controller.selectHunkById(id, { fly: true });
|
|
408
|
-
}
|
|
409
|
-
next() {
|
|
410
|
-
this.controller.next();
|
|
411
|
-
}
|
|
412
|
-
prev() {
|
|
413
|
-
this.controller.prev();
|
|
414
|
-
}
|
|
415
|
-
play() {
|
|
416
|
-
this.controller.play();
|
|
270
|
+
this.#states = next;
|
|
271
|
+
this.#applyPanels();
|
|
272
|
+
if (this.#layoutReady)
|
|
273
|
+
this.#emitLayout(source);
|
|
274
|
+
}
|
|
275
|
+
#emitLayout(source) {
|
|
276
|
+
this.dispatchEvent(new CustomEvent('kgdiff-layout', {
|
|
277
|
+
detail: { ...this.layout, source },
|
|
278
|
+
bubbles: true,
|
|
279
|
+
composed: true,
|
|
280
|
+
}));
|
|
417
281
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
throw new KgdiffError('invalid-input', translate('en', 'error.notLoaded'));
|
|
428
|
-
// A copy: callers can never mutate the diff the viewer shows.
|
|
429
|
-
return deepFreeze(structuredClone(diff.delta));
|
|
430
|
-
}
|
|
431
|
-
reviewState() {
|
|
432
|
-
return this.controller.reviewState();
|
|
433
|
-
}
|
|
434
|
-
setReviewed(hunkId, reviewed) {
|
|
435
|
-
this.controller.setReviewed(hunkId, reviewed);
|
|
436
|
-
}
|
|
437
|
-
/** Show an error state (used for undecodable embedded payloads). */
|
|
438
|
-
showError(error) {
|
|
439
|
-
this.controller.fail(error);
|
|
440
|
-
}
|
|
441
|
-
// ─── Internals ────────────────────────────────────────────────────────────
|
|
442
|
-
#emit(type, detail) {
|
|
443
|
-
this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
|
|
282
|
+
/**
|
|
283
|
+
* The host declared `panels`. Where its value changed, what the user changed there in this
|
|
284
|
+
* page gives way; declaring the same again (a framework re-rendering, an attribute set to
|
|
285
|
+
* the value it has) leaves the user's change alone.
|
|
286
|
+
*/
|
|
287
|
+
#setHostPanels(panels) {
|
|
288
|
+
this.#yield(this.#hostPanels, panels);
|
|
289
|
+
this.#hostPanels = panels;
|
|
290
|
+
this.#resolve('host');
|
|
444
291
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
292
|
+
/**
|
|
293
|
+
* A preset attribute changed. `commanded` (`sidebar`, which the layout reflects): the
|
|
294
|
+
* sidebar's regions yield even when the preset the page set last is the same.
|
|
295
|
+
*/
|
|
296
|
+
#preset(name, value, commanded = false) {
|
|
297
|
+
this.#presetAttrs[name] = value;
|
|
298
|
+
const { sidebar, header, chrome } = this.#presetAttrs;
|
|
299
|
+
const next = presetPanels(sidebar, header, chrome);
|
|
300
|
+
const yielded = this.#yield(this.#presets, next);
|
|
301
|
+
if (commanded) {
|
|
302
|
+
for (const r of SIDEBAR_REGIONS)
|
|
303
|
+
delete this.#runtime[r];
|
|
304
|
+
}
|
|
305
|
+
if (commanded || yielded.some((r) => SIDEBAR_REGIONS.includes(r)))
|
|
306
|
+
this.#folded = null;
|
|
307
|
+
this.#presets = next;
|
|
308
|
+
this.#resolve('host');
|
|
309
|
+
}
|
|
310
|
+
/** Drop the user's in-page change of every region `before` and `after` disagree on. */
|
|
311
|
+
#yield(before, after) {
|
|
312
|
+
const changed = PANEL_REGIONS.filter((r) => before[r] !== after[r]);
|
|
313
|
+
for (const r of changed)
|
|
314
|
+
delete this.#runtime[r];
|
|
315
|
+
return changed;
|
|
316
|
+
}
|
|
317
|
+
/** The user changed some regions: they hold in this page, and are remembered. */
|
|
318
|
+
#userPanels(changes) {
|
|
319
|
+
Object.assign(this.#runtime, changes);
|
|
320
|
+
this.#remember({ panels: { ...this.#remembered.panels, ...changes } });
|
|
321
|
+
this.#resolve('user');
|
|
322
|
+
}
|
|
323
|
+
/** What the user chose before; theme, palette and language apply unless the host set them. */
|
|
324
|
+
#recall() {
|
|
325
|
+
const r = readRemembered(this.#storage, this.persistKey);
|
|
326
|
+
// A preference the host set wins; one the user chose before wins over a report's view.
|
|
327
|
+
const c = this.controller;
|
|
328
|
+
if (r.theme && !this.hasAttribute('theme') && !c.explicit.has('theme')) {
|
|
329
|
+
c.setThemePreference(r.theme);
|
|
330
|
+
c.explicit.add('theme');
|
|
454
331
|
}
|
|
455
|
-
|
|
456
|
-
|
|
332
|
+
if (r.palette && !this.hasAttribute('palette') && !c.explicit.has('palette')) {
|
|
333
|
+
c.setPalette(r.palette);
|
|
334
|
+
c.explicit.add('palette');
|
|
457
335
|
}
|
|
336
|
+
if (r.locale && !this.hasAttribute('locale') && !c.explicit.has('locale')) {
|
|
337
|
+
c.setLocale(r.locale);
|
|
338
|
+
c.explicit.add('locale');
|
|
339
|
+
}
|
|
340
|
+
this.#ui?.sidebar.setSizes(r.sidebarWidth ?? SIDEBAR_WIDTH, r.detailShare ?? null, true);
|
|
341
|
+
return r;
|
|
458
342
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (
|
|
343
|
+
/** The first connection: the remembered states are the first ones (not a change, no event). */
|
|
344
|
+
#loadRemembered() {
|
|
345
|
+
if (this.persist !== 'local')
|
|
462
346
|
return;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
setTimeout(() => {
|
|
466
|
-
live.textContent = text;
|
|
467
|
-
}, 30);
|
|
347
|
+
this.#remembered = this.#recall();
|
|
348
|
+
this.#resolve('host');
|
|
468
349
|
}
|
|
469
|
-
|
|
470
|
-
|
|
350
|
+
/** Remember the user's choice (merged into what other viewers of this origin wrote). */
|
|
351
|
+
#remember(patch) {
|
|
352
|
+
if (this.persist !== 'local')
|
|
353
|
+
return;
|
|
354
|
+
const stored = readRemembered(this.#storage, this.persistKey);
|
|
355
|
+
const next = {
|
|
356
|
+
...stored,
|
|
357
|
+
...patch,
|
|
358
|
+
panels: { ...stored.panels, ...(patch.panels ?? {}) },
|
|
359
|
+
};
|
|
360
|
+
this.#remembered = { ...this.#remembered, ...patch, panels: next.panels };
|
|
361
|
+
writeRemembered(this.#storage, next, this.persistKey);
|
|
362
|
+
}
|
|
363
|
+
/** The user resized the sidebar or moved the split (once the drag or key press ended). */
|
|
364
|
+
#resized(width, detailShare) {
|
|
365
|
+
this.#remember({ sidebarWidth: width, detailShare });
|
|
366
|
+
this.#emitLayout('user');
|
|
471
367
|
}
|
|
472
|
-
#
|
|
368
|
+
#applyPanels() {
|
|
473
369
|
const ui = this.#ui;
|
|
474
370
|
if (!ui)
|
|
475
371
|
return;
|
|
476
|
-
|
|
372
|
+
const s = this.#states;
|
|
373
|
+
ui.sidebar.setPanels(s);
|
|
374
|
+
ui.stage.setPanels(s);
|
|
375
|
+
const sidebar = SIDEBAR_REGIONS.some((r) => s[r] !== 'hidden');
|
|
376
|
+
// A `<kgdiff-title>` or `<kgdiff-actions>` slotted into the viewer replaces the HUD's.
|
|
377
|
+
const title = ui.stage.replaces('title') ? 'hidden' : s.title;
|
|
378
|
+
const actions = ui.stage.replaces('actions') ? 'hidden' : s.actions;
|
|
379
|
+
ui.header.setPanels(title, actions, sidebar, () => this.#canvas());
|
|
477
380
|
ui.header.setSidebarState(this.#sidebarOpen);
|
|
381
|
+
// The top corners sit under what the HUD shows there.
|
|
382
|
+
const hud = [title === 'open' ? 'title' : '', actions === 'open' ? 'actions' : ''];
|
|
383
|
+
ui.stage.el.dataset.hud = hud.filter(Boolean).join(' ');
|
|
384
|
+
}
|
|
385
|
+
#canvas() {
|
|
386
|
+
return this.#ui?.stage.canvasHost.querySelector('canvas') ?? null;
|
|
387
|
+
}
|
|
388
|
+
// ─── Internals ────────────────────────────────────────────────────────────
|
|
389
|
+
/** The sidebar is open while one of its regions is. */
|
|
390
|
+
get #sidebarOpen() {
|
|
391
|
+
return SIDEBAR_REGIONS.some((r) => this.#states[r] === 'open');
|
|
478
392
|
}
|
|
479
393
|
/**
|
|
480
394
|
* `chrome` and `legend`: what the stage shows, whether clicks select, whether the view stays
|
|
481
|
-
* fitted.
|
|
395
|
+
* fitted. Which regions show is the `chrome="none"` preset's (see `panels.ts`).
|
|
482
396
|
*/
|
|
483
397
|
#applyChrome() {
|
|
484
398
|
const bare = this.chrome === 'none';
|
|
@@ -492,7 +406,7 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
492
406
|
ui.root.dataset.chrome = this.chrome;
|
|
493
407
|
const legend = this.legend;
|
|
494
408
|
ui.stage.setChrome(legend === 'auto' ? (bare ? 'mini' : 'full') : legend, !bare);
|
|
495
|
-
const canvas =
|
|
409
|
+
const canvas = this.#canvas();
|
|
496
410
|
if (canvas)
|
|
497
411
|
canvas.tabIndex = bare ? -1 : 0;
|
|
498
412
|
if (bare) {
|
|
@@ -500,50 +414,60 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
500
414
|
this.#toggleMenu(false);
|
|
501
415
|
}
|
|
502
416
|
}
|
|
503
|
-
/**
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
#build() {
|
|
508
|
-
const style = h('style', { text: STYLES });
|
|
509
|
-
const vars = h('style');
|
|
510
|
-
const ctx = {
|
|
511
|
-
controller: this.controller,
|
|
512
|
-
tr: () => translator(this.controller.locale),
|
|
513
|
-
root: this.#shadow,
|
|
514
|
-
setMode: (mode) => {
|
|
515
|
-
this.controller.setMode(mode);
|
|
516
|
-
},
|
|
517
|
-
toggleScope: () => {
|
|
518
|
-
this.controller.toggleScope();
|
|
519
|
-
},
|
|
417
|
+
/** What the layout does for the regions: preferences reflect as attributes, panels open. */
|
|
418
|
+
actions() {
|
|
419
|
+
return {
|
|
420
|
+
...this.baseActions(),
|
|
520
421
|
setThemePreference: (pref) => {
|
|
521
422
|
this.theme = pref;
|
|
423
|
+
this.#remember({ theme: this.theme });
|
|
522
424
|
},
|
|
523
425
|
setPalette: (p) => {
|
|
524
426
|
this.palette = p;
|
|
427
|
+
this.#remember({ palette: this.palette });
|
|
525
428
|
},
|
|
526
429
|
setLocale: (l) => {
|
|
527
430
|
this.locale = l;
|
|
431
|
+
this.#remember({ locale: this.locale });
|
|
528
432
|
},
|
|
529
433
|
toggleSidebar: (open) => this.#toggleSidebar(open),
|
|
530
434
|
sidebarOpen: () => this.#sidebarOpen,
|
|
435
|
+
hasSidebar: () => SIDEBAR_REGIONS.some((r) => this.#states[r] !== 'hidden'),
|
|
531
436
|
toggleMenu: (open) => this.#toggleMenu(open),
|
|
532
437
|
openHelp: () => this.#ui?.help.show(),
|
|
533
438
|
focusSearch: () => this.#focusSearch(),
|
|
439
|
+
setPanel: (region, state) => this.#userPanels({ [region]: state }),
|
|
534
440
|
};
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
441
|
+
}
|
|
442
|
+
#build() {
|
|
443
|
+
const root = h('div', { class: 'root' });
|
|
444
|
+
this.shadow.append(root);
|
|
445
|
+
// Each region binds to the root on its own (context protocol); the layout only places them.
|
|
446
|
+
const header = bindRegion(root, (ui) => new Header(ui));
|
|
447
|
+
const list = bindRegion(root, (ui) => new ChangeList(ui));
|
|
448
|
+
const search = bindRegion(root, (ui) => new Search(ui));
|
|
449
|
+
const filters = bindRegion(root, (ui) => new Filters(ui));
|
|
450
|
+
const detail = bindRegion(root, (ui) => new Detail(ui));
|
|
451
|
+
// The stage's slots are the viewer's: the page's content goes over the canvas.
|
|
452
|
+
const stage = bindRegion(root, (ui) => new Stage(ui, true));
|
|
453
|
+
stage.view = this.controller.view;
|
|
454
|
+
stage.onReplaced = () => this.#applyPanels();
|
|
455
|
+
this.#provideStage(stage);
|
|
456
|
+
const ui = uiContext(this.root, this.shadow);
|
|
457
|
+
const menu = new Menu(ui);
|
|
458
|
+
const help = new HelpDialog(ui);
|
|
459
|
+
const sidebar = new Sidebar(ui, {
|
|
460
|
+
setPanel: (region, state) => this.#userPanels({ [region]: state }),
|
|
461
|
+
resized: (width, share) => this.#resized(width, share),
|
|
462
|
+
focusOutside: () => (isOpen(header.sidebarButton) ? header.sidebarButton : this.#canvas()),
|
|
463
|
+
}, list, search, filters, detail);
|
|
541
464
|
// The title and the actions float over the canvas; the menu drops from the actions.
|
|
542
465
|
header.attachMenu(menu.el);
|
|
543
466
|
stage.el.appendChild(header.el);
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
467
|
+
root.append(h('div', { class: 'body' }, stage.el, sidebar.el), help.el, this.root.live);
|
|
468
|
+
// Content slotted before the layout was built.
|
|
469
|
+
stage.slotted();
|
|
470
|
+
this.#ui = { root, header, stage, list, sidebar, menu, help };
|
|
547
471
|
// Close the menu on outside clicks.
|
|
548
472
|
root.addEventListener('pointerdown', (e) => {
|
|
549
473
|
if (!menu.open)
|
|
@@ -555,48 +479,41 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
555
479
|
const hover = (over) => (e) => {
|
|
556
480
|
if (e.pointerType === 'touch')
|
|
557
481
|
return;
|
|
558
|
-
this
|
|
559
|
-
this.#applyHold();
|
|
482
|
+
this.pointerOver(over);
|
|
560
483
|
};
|
|
561
484
|
stage.el.addEventListener('pointerenter', hover(true));
|
|
562
485
|
stage.el.addEventListener('pointerleave', hover(false));
|
|
563
|
-
|
|
486
|
+
sidebar.setSizes(SIDEBAR_WIDTH, null, true);
|
|
487
|
+
this.#applyPanels();
|
|
564
488
|
this.#applyChrome();
|
|
565
489
|
}
|
|
566
|
-
/**
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
490
|
+
/**
|
|
491
|
+
* The viewer is the stage of the chrome that acts on a view (`<kgdiff-zoom>`,
|
|
492
|
+
* `<kgdiff-minimap>`) inside it or naming it with `stage`: its built-in stage's view.
|
|
493
|
+
*/
|
|
494
|
+
#provideStage(stage) {
|
|
495
|
+
const view = this.controller.view;
|
|
496
|
+
const ctx = {
|
|
497
|
+
stage: this,
|
|
498
|
+
root: this.root,
|
|
499
|
+
view,
|
|
500
|
+
registerObstacle: (el) => stage.registerObstacle(el),
|
|
501
|
+
watchView: (listener) => {
|
|
502
|
+
listener(view);
|
|
503
|
+
return () => undefined;
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
provideContext(this, stageContext, () => ctx);
|
|
583
507
|
}
|
|
584
|
-
|
|
508
|
+
/** The layout's part of a render (the root renders the regions after it). */
|
|
509
|
+
#update(topics) {
|
|
585
510
|
const ui = this.#ui;
|
|
586
511
|
const c = this.controller;
|
|
587
|
-
|
|
588
|
-
this.#reflect('mode', c.mode);
|
|
589
|
-
this.#reflect('scope', c.scope);
|
|
590
|
-
}
|
|
591
|
-
if (topics.has('theme')) {
|
|
592
|
-
this.#reflect('theme', c.themePreference === 'auto' && !this.hasAttribute('theme') ? null : c.themePreference);
|
|
593
|
-
}
|
|
512
|
+
this.reflectSession(topics);
|
|
594
513
|
if (!ui)
|
|
595
514
|
return;
|
|
596
|
-
if (topics.has('theme') || topics.has('status'))
|
|
597
|
-
ui.vars.textContent = `:host { ${themeVariables(c.theme)} }`;
|
|
515
|
+
if (topics.has('theme') || topics.has('status'))
|
|
598
516
|
ui.root.dataset.theme = c.themeName;
|
|
599
|
-
}
|
|
600
517
|
if (topics.has('locale') || topics.has('status')) {
|
|
601
518
|
ui.root.setAttribute('lang', c.locale);
|
|
602
519
|
if (ui.menu.open)
|
|
@@ -606,18 +523,38 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
606
523
|
}
|
|
607
524
|
if (topics.has('theme') && ui.menu.open)
|
|
608
525
|
ui.menu.render();
|
|
609
|
-
ui.header.update(topics);
|
|
610
526
|
ui.sidebar.update(topics);
|
|
611
|
-
ui.stage.update(topics);
|
|
612
527
|
if (topics.has('mode') || topics.has('status')) {
|
|
613
528
|
ui.root.dataset.mode = c.mode;
|
|
614
529
|
ui.root.dataset.scope = c.scope;
|
|
615
530
|
}
|
|
616
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* Fold the sidebar (its open regions collapse) or open it again (the regions that were open,
|
|
534
|
+
* or all four). Reflected to the `sidebar` attribute.
|
|
535
|
+
*/
|
|
617
536
|
#toggleSidebar(open) {
|
|
537
|
+
const s = this.#states;
|
|
538
|
+
const shown = SIDEBAR_REGIONS.filter((r) => s[r] !== 'hidden');
|
|
539
|
+
if (shown.length === 0)
|
|
540
|
+
return;
|
|
618
541
|
const next = open ?? !this.#sidebarOpen;
|
|
619
|
-
this
|
|
620
|
-
this.#
|
|
542
|
+
this.reflect('sidebar', next ? 'open' : 'closed');
|
|
543
|
+
if (next === this.#sidebarOpen)
|
|
544
|
+
return;
|
|
545
|
+
const changes = {};
|
|
546
|
+
if (next) {
|
|
547
|
+
const restore = this.#folded?.filter((r) => shown.includes(r)) ?? [];
|
|
548
|
+
for (const r of restore.length > 0 ? restore : shown)
|
|
549
|
+
changes[r] = 'open';
|
|
550
|
+
this.#folded = null;
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
this.#folded = shown.filter((r) => s[r] === 'open');
|
|
554
|
+
for (const r of shown)
|
|
555
|
+
changes[r] = 'collapsed';
|
|
556
|
+
}
|
|
557
|
+
this.#userPanels(changes);
|
|
621
558
|
}
|
|
622
559
|
#toggleMenu(open) {
|
|
623
560
|
const ui = this.#ui;
|
|
@@ -630,25 +567,17 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
630
567
|
ui.menu.hide(false);
|
|
631
568
|
ui.header.setMenuState(next);
|
|
632
569
|
}
|
|
570
|
+
/** `/`: an open search takes focus; else this layout's search opens, unless it is hidden. */
|
|
633
571
|
#focusSearch() {
|
|
572
|
+
if (this.root.focus('search'))
|
|
573
|
+
return;
|
|
574
|
+
if (this.#states.search === 'hidden')
|
|
575
|
+
return;
|
|
634
576
|
if (!this.#sidebarOpen)
|
|
635
577
|
this.#toggleSidebar(true);
|
|
636
|
-
this.#
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const dark = media('(prefers-color-scheme: light)');
|
|
640
|
-
const motion = media('(prefers-reduced-motion: reduce)');
|
|
641
|
-
const apply = () => {
|
|
642
|
-
this.controller.setSystemDark(!(dark?.matches ?? false));
|
|
643
|
-
this.controller.setReducedMotion(motion?.matches ?? false);
|
|
644
|
-
};
|
|
645
|
-
apply();
|
|
646
|
-
for (const q of [dark, motion]) {
|
|
647
|
-
if (!q)
|
|
648
|
-
continue;
|
|
649
|
-
q.addEventListener?.('change', apply);
|
|
650
|
-
this.#cleanup.push(() => q.removeEventListener?.('change', apply));
|
|
651
|
-
}
|
|
578
|
+
if (this.#states.search === 'collapsed')
|
|
579
|
+
this.#userPanels({ search: 'open' });
|
|
580
|
+
this.root.focus('search');
|
|
652
581
|
}
|
|
653
582
|
#watchSize() {
|
|
654
583
|
if (typeof ResizeObserver !== 'function')
|
|
@@ -665,9 +594,8 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
665
594
|
ui.root.dataset.compact = String(compact);
|
|
666
595
|
}
|
|
667
596
|
ui.stage.layout();
|
|
668
|
-
ui.
|
|
669
|
-
|
|
670
|
-
ui.sidebar.syncSheet();
|
|
597
|
+
ui.list.renderWindow();
|
|
598
|
+
ui.sidebar.layout(compact);
|
|
671
599
|
this.controller.layoutChanged();
|
|
672
600
|
});
|
|
673
601
|
ro.observe(this);
|
|
@@ -676,154 +604,5 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
676
604
|
ro.observe(this.#ui.stage.el);
|
|
677
605
|
this.#cleanup.push(() => ro.disconnect());
|
|
678
606
|
}
|
|
679
|
-
#onKeyDown(e) {
|
|
680
|
-
if (e.defaultPrevented || e.isComposing)
|
|
681
|
-
return;
|
|
682
|
-
const ui = this.#ui;
|
|
683
|
-
if (!ui || this.chrome === 'none')
|
|
684
|
-
return;
|
|
685
|
-
const path = e.composedPath();
|
|
686
|
-
if (!path.includes(this)) {
|
|
687
|
-
// Outside the component only with `global-shortcuts`, when nothing else has focus, for
|
|
688
|
-
// the first such viewer, and never the keys pages scroll with.
|
|
689
|
-
if (!this.hasAttribute('global-shortcuts') || PAGE_KEYS.has(e.key))
|
|
690
|
-
return;
|
|
691
|
-
const active = document.activeElement;
|
|
692
|
-
if (active && active !== document.body && active !== document.documentElement)
|
|
693
|
-
return;
|
|
694
|
-
if (connected.find((v) => v.hasAttribute('global-shortcuts')) !== this)
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
if (ui.help.open || ui.menu.open) {
|
|
698
|
-
if (e.key === 'Escape') {
|
|
699
|
-
e.preventDefault();
|
|
700
|
-
ui.help.hide();
|
|
701
|
-
this.#toggleMenu(false);
|
|
702
|
-
}
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
const target = path[0] ?? null;
|
|
706
|
-
if (isTypingTarget(target))
|
|
707
|
-
return;
|
|
708
|
-
const c = this.controller;
|
|
709
|
-
const action = keyAction(e, c.mode, c.scope);
|
|
710
|
-
if (!action)
|
|
711
|
-
return;
|
|
712
|
-
// Enter / Space on a focused control activate that control.
|
|
713
|
-
const el = target;
|
|
714
|
-
if ((e.key === 'Enter' || e.key === ' ') &&
|
|
715
|
-
el?.closest?.('button, a, summary, [role="radio"], [role="menuitemradio"]')) {
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
if (c.status !== 'ready' && !['help', 'theme', 'sidebar', 'escape'].includes(action.type))
|
|
719
|
-
return;
|
|
720
|
-
e.preventDefault();
|
|
721
|
-
switch (action.type) {
|
|
722
|
-
case 'mode':
|
|
723
|
-
c.setMode(action.mode);
|
|
724
|
-
break;
|
|
725
|
-
case 'scope':
|
|
726
|
-
c.toggleScope();
|
|
727
|
-
break;
|
|
728
|
-
case 'next':
|
|
729
|
-
c.next();
|
|
730
|
-
break;
|
|
731
|
-
case 'prev':
|
|
732
|
-
c.prev();
|
|
733
|
-
break;
|
|
734
|
-
case 'first':
|
|
735
|
-
c.first();
|
|
736
|
-
break;
|
|
737
|
-
case 'last':
|
|
738
|
-
c.last();
|
|
739
|
-
break;
|
|
740
|
-
case 'fly':
|
|
741
|
-
c.flyToSelection();
|
|
742
|
-
break;
|
|
743
|
-
case 'escape':
|
|
744
|
-
if (!ui.sidebar.escape())
|
|
745
|
-
c.clearSelection();
|
|
746
|
-
break;
|
|
747
|
-
case 'play-pause':
|
|
748
|
-
c.togglePlay();
|
|
749
|
-
break;
|
|
750
|
-
case 'step':
|
|
751
|
-
c.stepReplay(action.direction);
|
|
752
|
-
break;
|
|
753
|
-
case 'divider':
|
|
754
|
-
c.setSplit(c.split + action.delta);
|
|
755
|
-
break;
|
|
756
|
-
case 'fit':
|
|
757
|
-
void c.fit();
|
|
758
|
-
break;
|
|
759
|
-
case 'zoom':
|
|
760
|
-
c.zoom(action.direction);
|
|
761
|
-
break;
|
|
762
|
-
case 'search':
|
|
763
|
-
this.#focusSearch();
|
|
764
|
-
break;
|
|
765
|
-
case 'review':
|
|
766
|
-
c.toggleReviewed();
|
|
767
|
-
break;
|
|
768
|
-
case 'sidebar':
|
|
769
|
-
this.#toggleSidebar();
|
|
770
|
-
break;
|
|
771
|
-
case 'theme': {
|
|
772
|
-
const i = THEME_PREFERENCES.indexOf(c.themePreference);
|
|
773
|
-
this.theme = THEME_PREFERENCES[(i + 1) % THEME_PREFERENCES.length];
|
|
774
|
-
break;
|
|
775
|
-
}
|
|
776
|
-
case 'help':
|
|
777
|
-
ui.help.show();
|
|
778
|
-
break;
|
|
779
|
-
case 'expand':
|
|
780
|
-
c.expandNearest();
|
|
781
|
-
break;
|
|
782
|
-
default:
|
|
783
|
-
action;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
// ─── Deep links ───────────────────────────────────────────────────────────
|
|
787
|
-
#readHash() {
|
|
788
|
-
if (!this.hasAttribute('sync-hash') || typeof location === 'undefined')
|
|
789
|
-
return;
|
|
790
|
-
if (location.hash === this.#lastHash)
|
|
791
|
-
return;
|
|
792
|
-
const state = parseHash(location.hash);
|
|
793
|
-
this.#lastHash = location.hash;
|
|
794
|
-
if (!state)
|
|
795
|
-
return;
|
|
796
|
-
if (state.mode || state.scope) {
|
|
797
|
-
// A deep link outranks the large-graph default and a report's preferred view.
|
|
798
|
-
const c = this.controller;
|
|
799
|
-
if (state.mode)
|
|
800
|
-
c.explicit.add('mode');
|
|
801
|
-
if (state.scope)
|
|
802
|
-
c.explicit.add('scope');
|
|
803
|
-
c.setView(state.mode ?? c.mode, state.scope ?? c.scope);
|
|
804
|
-
}
|
|
805
|
-
if (state.hunk)
|
|
806
|
-
this.controller.selectHunkById(state.hunk, { fly: true });
|
|
807
|
-
}
|
|
808
|
-
#writeHash() {
|
|
809
|
-
if (!this.hasAttribute('sync-hash') || typeof location === 'undefined')
|
|
810
|
-
return;
|
|
811
|
-
const c = this.controller;
|
|
812
|
-
const hunk = c.selectedHunk >= 0 ? c.scene?.hunks[c.selectedHunk]?.id : undefined;
|
|
813
|
-
const hash = formatHash({
|
|
814
|
-
...(hunk !== undefined ? { hunk } : {}),
|
|
815
|
-
mode: c.mode,
|
|
816
|
-
scope: c.scope,
|
|
817
|
-
});
|
|
818
|
-
if (hash === location.hash)
|
|
819
|
-
return;
|
|
820
|
-
this.#lastHash = hash;
|
|
821
|
-
try {
|
|
822
|
-
history.replaceState(history.state, '', hash);
|
|
823
|
-
}
|
|
824
|
-
catch {
|
|
825
|
-
location.hash = hash;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
607
|
}
|
|
829
608
|
//# sourceMappingURL=element.js.map
|