@kgdiff/viewer 0.5.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/controller.d.ts +22 -74
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +35 -376
- 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 +52 -46
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +362 -403
- 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 +328 -34
- 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/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 +30 -30
- package/dist/root.d.ts.map +1 -1
- package/dist/root.js +65 -42
- package/dist/root.js.map +1 -1
- package/dist/session.d.ts +19 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +15 -17
- package/dist/session.js.map +1 -1
- package/dist/standalone/kgdiff-viewer.js +4 -4
- package/dist/styles.d.ts +4 -0
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +224 -84
- package/dist/styles.js.map +1 -1
- package/dist/theme.d.ts +7 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +10 -7
- package/dist/theme.js.map +1 -1
- 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.map +1 -1
- package/dist/ui/change-list.js +2 -2
- package/dist/ui/change-list.js.map +1 -1
- package/dist/ui/context.d.ts +20 -7
- package/dist/ui/context.d.ts.map +1 -1
- package/dist/ui/context.js.map +1 -1
- package/dist/ui/header.d.ts +41 -3
- package/dist/ui/header.d.ts.map +1 -1
- package/dist/ui/header.js +99 -34
- 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.map +1 -1
- package/dist/ui/search.js +3 -2
- package/dist/ui/search.js.map +1 -1
- package/dist/ui/sidebar.d.ts +26 -3
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +344 -12
- 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/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/controller.ts +51 -442
- package/src/dom.ts +30 -0
- package/src/element.ts +364 -406
- 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 +347 -34
- 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/prefs.ts +12 -4
- package/src/react.ts +459 -0
- package/src/root-element.ts +624 -0
- package/src/root.ts +92 -65
- package/src/session.ts +22 -33
- package/src/styles.ts +245 -84
- package/src/theme.ts +10 -7
- package/src/topics.ts +37 -0
- package/src/ui/change-list.ts +6 -2
- package/src/ui/context.ts +20 -7
- package/src/ui/header.ts +127 -56
- package/src/ui/legend.ts +112 -0
- package/src/ui/popovers.ts +68 -25
- package/src/ui/search.ts +10 -5
- package/src/ui/sidebar.ts +386 -23
- package/src/ui/stage.ts +267 -331
- 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,133 +1,116 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<kgdiff-viewer>`, the default layout (RFC 0001 §4): a root (the session,
|
|
3
|
-
* region, events
|
|
4
|
-
* chrome, the change list, the search, the family filters and
|
|
5
|
-
* always were (a 360 px sidebar; a bottom sheet below 760 px).
|
|
6
|
-
*
|
|
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.
|
|
7
13
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { provideContext } from './context.js';
|
|
15
|
+
import { ViewerController } from './controller.js';
|
|
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';
|
|
22
|
+
import { defaultStorage } from './review-store.js';
|
|
23
|
+
import { bindRegion, uiContext } from './root.js';
|
|
24
|
+
import { ROOT_ATTRIBUTES, RootElement } from './root-element.js';
|
|
25
|
+
import { stylesheet } from './styles.js';
|
|
16
26
|
import { ChangeList } from './ui/change-list.js';
|
|
17
27
|
import { Detail } from './ui/detail.js';
|
|
18
28
|
import { Filters } from './ui/filters.js';
|
|
19
29
|
import { Header } from './ui/header.js';
|
|
20
30
|
import { HelpDialog, Menu } from './ui/popovers.js';
|
|
21
31
|
import { Search } from './ui/search.js';
|
|
22
|
-
import { Sidebar } from './ui/sidebar.js';
|
|
32
|
+
import { SIDEBAR_WIDTH, Sidebar } from './ui/sidebar.js';
|
|
23
33
|
import { Stage } from './ui/stage.js';
|
|
24
34
|
/** Below this width the sidebar becomes a bottom sheet. */
|
|
25
35
|
export const COMPACT_WIDTH = 760;
|
|
26
36
|
/** Below this width the title drops the version tags. */
|
|
27
37
|
const NARROW_WIDTH = 1100;
|
|
28
38
|
const OBSERVED = [
|
|
29
|
-
|
|
30
|
-
'scope',
|
|
31
|
-
'theme',
|
|
32
|
-
'palette',
|
|
33
|
-
'locale',
|
|
39
|
+
...ROOT_ATTRIBUTES,
|
|
34
40
|
'sidebar',
|
|
35
|
-
'
|
|
41
|
+
'header',
|
|
36
42
|
'chrome',
|
|
37
43
|
'legend',
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
44
|
+
'panels',
|
|
45
|
+
'persist',
|
|
46
|
+
'persist-key',
|
|
41
47
|
];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'theme',
|
|
46
|
-
'palette',
|
|
47
|
-
'locale',
|
|
48
|
-
'chrome',
|
|
49
|
-
'legend',
|
|
50
|
-
'loop',
|
|
51
|
-
'loopDelay',
|
|
52
|
-
'pauseOnHover',
|
|
53
|
-
'payload',
|
|
54
|
-
];
|
|
55
|
-
/** Keys the page itself scrolls with: never taken at document level. */
|
|
56
|
-
const PAGE_KEYS = new Set([
|
|
57
|
-
'ArrowUp',
|
|
58
|
-
'ArrowDown',
|
|
59
|
-
'ArrowLeft',
|
|
60
|
-
'ArrowRight',
|
|
61
|
-
'Home',
|
|
62
|
-
'End',
|
|
63
|
-
'PageUp',
|
|
64
|
-
'PageDown',
|
|
65
|
-
' ',
|
|
66
|
-
'Spacebar',
|
|
67
|
-
]);
|
|
68
|
-
/** Connected viewers, in connection order (`global-shortcuts` goes to the first opted-in one). */
|
|
69
|
-
const connected = [];
|
|
70
|
-
function deepFreeze(value) {
|
|
71
|
-
if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {
|
|
72
|
-
for (const v of Object.values(value))
|
|
73
|
-
deepFreeze(v);
|
|
74
|
-
Object.freeze(value);
|
|
75
|
-
}
|
|
76
|
-
return value;
|
|
48
|
+
function warnPanels(invalid) {
|
|
49
|
+
if (invalid.length > 0)
|
|
50
|
+
console.warn(`[kgdiff] ignoring invalid panels: ${invalid.join(' ')}`);
|
|
77
51
|
}
|
|
78
|
-
|
|
52
|
+
/**
|
|
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.
|
|
64
|
+
*/
|
|
65
|
+
export class KgdiffViewer extends RootElement {
|
|
79
66
|
static get observedAttributes() {
|
|
80
67
|
return OBSERVED;
|
|
81
68
|
}
|
|
82
|
-
/** @internal */
|
|
83
|
-
controller;
|
|
84
|
-
#shadow;
|
|
85
|
-
#root;
|
|
86
69
|
#ui = null;
|
|
87
|
-
#currentPayload = null;
|
|
88
|
-
#reflecting = false;
|
|
89
70
|
#compact = false;
|
|
90
71
|
#cleanup = [];
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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();
|
|
95
94
|
constructor() {
|
|
96
95
|
super();
|
|
97
|
-
this.#shadow = this.attachShadow({ mode: 'open' });
|
|
98
|
-
this.#root = new ViewerRoot(this, this.#shadow, this.#actions(), (type) => {
|
|
99
|
-
if (type === 'kgdiff-select' || type === 'kgdiff-mode')
|
|
100
|
-
this.#writeHash();
|
|
101
|
-
});
|
|
102
|
-
this.controller = this.#root.controller;
|
|
103
|
-
this.controller.locale = toLocale(this.getAttribute('locale')) ?? detectLocale();
|
|
104
96
|
// The layout renders first, then the regions in the order they bound to the root.
|
|
105
|
-
this
|
|
97
|
+
this.root.attach({ update: (topics) => this.#update(topics) });
|
|
106
98
|
}
|
|
107
99
|
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
if (!this.#upgraded) {
|
|
110
|
-
this.#upgraded = true;
|
|
111
|
-
// Properties set before the element was defined shadow the accessors: re-apply them.
|
|
112
|
-
for (const prop of UPGRADED) {
|
|
113
|
-
if (Object.hasOwn(this, prop)) {
|
|
114
|
-
const value = this[prop];
|
|
115
|
-
delete this[prop];
|
|
116
|
-
this[prop] = value;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// Provide the session (the regions bind to it) and follow the system's preferences.
|
|
121
|
-
this.#cleanup.push(this.#root.connect());
|
|
100
|
+
connected() {
|
|
122
101
|
if (!this.#ui)
|
|
123
102
|
this.#build();
|
|
124
|
-
connected.push(this);
|
|
125
103
|
const ui = this.#ui;
|
|
126
104
|
if (!ui)
|
|
127
105
|
return;
|
|
128
|
-
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);
|
|
129
112
|
// The graph takes keyboard focus (shortcuts apply where focus is), also on click.
|
|
130
|
-
const canvas =
|
|
113
|
+
const canvas = this.#canvas();
|
|
131
114
|
if (canvas) {
|
|
132
115
|
canvas.tabIndex = this.chrome === 'none' ? -1 : 0;
|
|
133
116
|
const focusCanvas = () => {
|
|
@@ -138,159 +121,91 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
138
121
|
this.#cleanup.push(() => canvas.removeEventListener('pointerdown', focusCanvas));
|
|
139
122
|
}
|
|
140
123
|
this.#watchSize();
|
|
141
|
-
const onKey = (e) => this.#onKeyDown(e);
|
|
142
|
-
document.addEventListener('keydown', onKey);
|
|
143
|
-
this.#cleanup.push(() => document.removeEventListener('keydown', onKey));
|
|
144
|
-
const onHash = () => this.#readHash();
|
|
145
|
-
window.addEventListener('hashchange', onHash);
|
|
146
|
-
this.#cleanup.push(() => window.removeEventListener('hashchange', onHash));
|
|
147
124
|
this.controller.setChrome(() => ui.stage.chromeLayout());
|
|
148
125
|
document.fonts?.ready?.then(() => this.#ui?.stage.layout()).catch(() => undefined);
|
|
149
|
-
this.#readHash();
|
|
150
|
-
this.#root.render(ALL_TOPICS);
|
|
151
126
|
}
|
|
152
|
-
|
|
153
|
-
const i = connected.indexOf(this);
|
|
154
|
-
if (i >= 0)
|
|
155
|
-
connected.splice(i, 1);
|
|
127
|
+
disconnected() {
|
|
156
128
|
for (const fn of this.#cleanup.splice(0))
|
|
157
129
|
fn();
|
|
158
130
|
this.controller.detach();
|
|
159
|
-
// Moved while hovered: no pointerleave follows.
|
|
160
|
-
this.#pointerOver = false;
|
|
161
|
-
this.#applyHold();
|
|
162
131
|
}
|
|
163
|
-
|
|
164
|
-
if (this.#reflecting)
|
|
165
|
-
return;
|
|
132
|
+
attributeChanged(name, value, old) {
|
|
166
133
|
switch (name) {
|
|
167
|
-
case 'mode':
|
|
168
|
-
case 'scope':
|
|
169
|
-
case 'theme':
|
|
170
|
-
case 'palette':
|
|
171
|
-
case 'locale':
|
|
172
|
-
this.#applyPreference(name, value, false);
|
|
173
|
-
break;
|
|
174
134
|
case 'sidebar':
|
|
175
|
-
|
|
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);
|
|
176
139
|
break;
|
|
177
|
-
case '
|
|
178
|
-
|
|
179
|
-
this.#readHash();
|
|
140
|
+
case 'header':
|
|
141
|
+
this.#preset(name, value);
|
|
180
142
|
break;
|
|
181
143
|
case 'chrome':
|
|
182
|
-
|
|
144
|
+
this.#preset(name, value);
|
|
183
145
|
this.#applyChrome();
|
|
184
146
|
break;
|
|
185
|
-
case '
|
|
186
|
-
|
|
187
|
-
this.controller.setLoop(this.loop ? this.loopDelay : null);
|
|
188
|
-
this.#applyHold();
|
|
189
|
-
break;
|
|
190
|
-
case 'pause-on-hover':
|
|
191
|
-
this.#applyHold();
|
|
192
|
-
break;
|
|
193
|
-
default:
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Apply a preference from an attribute or a property. Valid values are explicit (a report's
|
|
199
|
-
* `view` does not override them); invalid ones fall back to the default (`overlay`, `all`,
|
|
200
|
-
* `auto`, `default`, the browser's language). Properties reflect the normalized value; the
|
|
201
|
-
* legacy mode `changes` reflects as `overlay` + scope `changes`.
|
|
202
|
-
*/
|
|
203
|
-
#applyPreference(name, raw, reflect) {
|
|
204
|
-
const c = this.controller;
|
|
205
|
-
// `mode="changes"` is rewritten to the attribute pair it stands for.
|
|
206
|
-
const legacy = name === 'mode' && raw === LEGACY_CHANGES_MODE;
|
|
207
|
-
let value;
|
|
208
|
-
switch (name) {
|
|
209
|
-
case 'mode': {
|
|
210
|
-
if (legacy) {
|
|
211
|
-
c.explicit.add('scope');
|
|
212
|
-
c.setView('overlay', 'changes');
|
|
213
|
-
value = 'overlay';
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
const v = toMode(raw);
|
|
217
|
-
c.setMode(v ?? 'overlay');
|
|
218
|
-
value = v;
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
case 'scope': {
|
|
222
|
-
const v = toScope(raw);
|
|
223
|
-
c.setScope(v ?? 'all');
|
|
224
|
-
// A locked scope stays `changes`: the attribute says what is shown.
|
|
225
|
-
value = v === null ? null : c.scope;
|
|
226
|
-
break;
|
|
227
|
-
}
|
|
228
|
-
case 'theme': {
|
|
229
|
-
const v = toTheme(raw);
|
|
230
|
-
c.setThemePreference(v ?? 'auto');
|
|
231
|
-
value = v;
|
|
147
|
+
case 'legend':
|
|
148
|
+
this.#applyChrome();
|
|
232
149
|
break;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
value = v;
|
|
150
|
+
case 'panels': {
|
|
151
|
+
const { panels, invalid } = parsePanels(value ?? '');
|
|
152
|
+
warnPanels(invalid);
|
|
153
|
+
this.#setHostPanels(panels);
|
|
238
154
|
break;
|
|
239
155
|
}
|
|
240
|
-
case '
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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');
|
|
244
163
|
break;
|
|
245
|
-
}
|
|
246
164
|
default:
|
|
247
|
-
|
|
165
|
+
break;
|
|
248
166
|
}
|
|
249
|
-
if (value === null)
|
|
250
|
-
c.explicit.delete(name);
|
|
251
|
-
else
|
|
252
|
-
c.explicit.add(name);
|
|
253
|
-
if (reflect || legacy)
|
|
254
|
-
this.#reflect(name, value);
|
|
255
|
-
if (legacy)
|
|
256
|
-
this.#reflect('scope', c.scope);
|
|
257
|
-
}
|
|
258
|
-
// ─── Properties ───────────────────────────────────────────────────────────
|
|
259
|
-
get mode() {
|
|
260
|
-
return this.controller.mode;
|
|
261
|
-
}
|
|
262
|
-
set mode(value) {
|
|
263
|
-
this.#applyPreference('mode', value, true);
|
|
264
167
|
}
|
|
265
|
-
|
|
266
|
-
return
|
|
168
|
+
upgradedProperties() {
|
|
169
|
+
return ['chrome', 'legend', 'panels', 'persist', 'persistKey'];
|
|
267
170
|
}
|
|
268
|
-
|
|
269
|
-
|
|
171
|
+
sheets(doc) {
|
|
172
|
+
return [stylesheet(doc)];
|
|
270
173
|
}
|
|
271
|
-
|
|
272
|
-
return
|
|
174
|
+
createSession(host, deps) {
|
|
175
|
+
return new ViewerController(host, deps);
|
|
273
176
|
}
|
|
274
|
-
|
|
275
|
-
this.#
|
|
177
|
+
keysEnabled() {
|
|
178
|
+
return this.#ui !== null && this.chrome !== 'none';
|
|
276
179
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|
|
282
190
|
}
|
|
283
|
-
|
|
284
|
-
|
|
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');
|
|
285
195
|
}
|
|
286
|
-
|
|
287
|
-
this.#
|
|
196
|
+
load(options) {
|
|
197
|
+
this.#report = {};
|
|
198
|
+
this.#resolve('host');
|
|
199
|
+
return super.load(options);
|
|
288
200
|
}
|
|
201
|
+
// ─── Properties ───────────────────────────────────────────────────────────
|
|
289
202
|
get chrome() {
|
|
290
203
|
return toChrome(this.getAttribute('chrome')) ?? 'full';
|
|
291
204
|
}
|
|
292
205
|
set chrome(value) {
|
|
293
|
-
|
|
206
|
+
const v = toChrome(value) === 'none' ? 'none' : null;
|
|
207
|
+
this.reflect('chrome', v);
|
|
208
|
+
this.#preset('chrome', v);
|
|
294
209
|
this.#applyChrome();
|
|
295
210
|
}
|
|
296
211
|
get legend() {
|
|
@@ -298,121 +213,186 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
298
213
|
}
|
|
299
214
|
set legend(value) {
|
|
300
215
|
const v = toLegend(value);
|
|
301
|
-
this
|
|
216
|
+
this.reflect('legend', v === 'auto' ? null : v);
|
|
302
217
|
this.#applyChrome();
|
|
303
218
|
}
|
|
304
|
-
|
|
305
|
-
|
|
219
|
+
/** The host's declaration (the `panels` attribute as an object); `layout` has the states. */
|
|
220
|
+
get panels() {
|
|
221
|
+
return { ...this.#hostPanels };
|
|
306
222
|
}
|
|
307
|
-
set
|
|
308
|
-
|
|
223
|
+
set panels(value) {
|
|
224
|
+
const { panels, invalid } = toPanels(value);
|
|
225
|
+
warnPanels(invalid);
|
|
226
|
+
this.reflect('panels', formatPanels(panels) || null);
|
|
227
|
+
this.#setHostPanels(panels);
|
|
309
228
|
}
|
|
310
|
-
get
|
|
311
|
-
return
|
|
229
|
+
get persist() {
|
|
230
|
+
return this.getAttribute('persist') === 'none' ? 'none' : 'local';
|
|
312
231
|
}
|
|
313
|
-
set
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
this.removeAttribute('loop-delay');
|
|
232
|
+
set persist(value) {
|
|
233
|
+
if (value === 'none')
|
|
234
|
+
this.setAttribute('persist', 'none');
|
|
317
235
|
else
|
|
318
|
-
this.
|
|
236
|
+
this.removeAttribute('persist');
|
|
319
237
|
}
|
|
320
|
-
|
|
321
|
-
|
|
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;
|
|
322
241
|
}
|
|
323
|
-
set
|
|
324
|
-
if (value)
|
|
325
|
-
this.removeAttribute('
|
|
242
|
+
set persistKey(value) {
|
|
243
|
+
if (!value || value === LAYOUT_KEY)
|
|
244
|
+
this.removeAttribute('persist-key');
|
|
326
245
|
else
|
|
327
|
-
this.setAttribute('
|
|
246
|
+
this.setAttribute('persist-key', String(value));
|
|
328
247
|
}
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
};
|
|
331
256
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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))
|
|
335
269
|
return;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
// ─── Methods ──────────────────────────────────────────────────────────────
|
|
349
|
-
load(options) {
|
|
350
|
-
this.#currentPayload = null;
|
|
351
|
-
return this.controller.load(options);
|
|
352
|
-
}
|
|
353
|
-
selectHunk(id) {
|
|
354
|
-
this.controller.selectHunkById(id, { fly: true });
|
|
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
|
+
}));
|
|
355
281
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
this
|
|
364
|
-
|
|
365
|
-
pause() {
|
|
366
|
-
this.controller.pause();
|
|
367
|
-
}
|
|
368
|
-
fit(options) {
|
|
369
|
-
return this.controller.fit(options);
|
|
370
|
-
}
|
|
371
|
-
exportDelta() {
|
|
372
|
-
const diff = this.controller.diff;
|
|
373
|
-
if (!diff)
|
|
374
|
-
throw new KgdiffError('invalid-input', translate('en', 'error.notLoaded'));
|
|
375
|
-
// A copy: callers can never mutate the diff the viewer shows.
|
|
376
|
-
return deepFreeze(structuredClone(diff.delta));
|
|
377
|
-
}
|
|
378
|
-
reviewState() {
|
|
379
|
-
return this.controller.reviewState();
|
|
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');
|
|
380
291
|
}
|
|
381
|
-
|
|
382
|
-
|
|
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');
|
|
331
|
+
}
|
|
332
|
+
if (r.palette && !this.hasAttribute('palette') && !c.explicit.has('palette')) {
|
|
333
|
+
c.setPalette(r.palette);
|
|
334
|
+
c.explicit.add('palette');
|
|
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;
|
|
383
342
|
}
|
|
384
|
-
/**
|
|
385
|
-
|
|
386
|
-
this.
|
|
343
|
+
/** The first connection: the remembered states are the first ones (not a change, no event). */
|
|
344
|
+
#loadRemembered() {
|
|
345
|
+
if (this.persist !== 'local')
|
|
346
|
+
return;
|
|
347
|
+
this.#remembered = this.#recall();
|
|
348
|
+
this.#resolve('host');
|
|
387
349
|
}
|
|
388
|
-
|
|
389
|
-
#
|
|
390
|
-
if (this.
|
|
350
|
+
/** Remember the user's choice (merged into what other viewers of this origin wrote). */
|
|
351
|
+
#remember(patch) {
|
|
352
|
+
if (this.persist !== 'local')
|
|
391
353
|
return;
|
|
392
|
-
this.#
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
this.#reflecting = false;
|
|
401
|
-
}
|
|
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);
|
|
402
362
|
}
|
|
403
|
-
|
|
404
|
-
|
|
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');
|
|
405
367
|
}
|
|
406
|
-
#
|
|
368
|
+
#applyPanels() {
|
|
407
369
|
const ui = this.#ui;
|
|
408
370
|
if (!ui)
|
|
409
371
|
return;
|
|
410
|
-
|
|
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());
|
|
411
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');
|
|
412
392
|
}
|
|
413
393
|
/**
|
|
414
394
|
* `chrome` and `legend`: what the stage shows, whether clicks select, whether the view stays
|
|
415
|
-
* fitted.
|
|
395
|
+
* fitted. Which regions show is the `chrome="none"` preset's (see `panels.ts`).
|
|
416
396
|
*/
|
|
417
397
|
#applyChrome() {
|
|
418
398
|
const bare = this.chrome === 'none';
|
|
@@ -426,7 +406,7 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
426
406
|
ui.root.dataset.chrome = this.chrome;
|
|
427
407
|
const legend = this.legend;
|
|
428
408
|
ui.stage.setChrome(legend === 'auto' ? (bare ? 'mini' : 'full') : legend, !bare);
|
|
429
|
-
const canvas =
|
|
409
|
+
const canvas = this.#canvas();
|
|
430
410
|
if (canvas)
|
|
431
411
|
canvas.tabIndex = bare ? -1 : 0;
|
|
432
412
|
if (bare) {
|
|
@@ -434,50 +414,59 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
434
414
|
this.#toggleMenu(false);
|
|
435
415
|
}
|
|
436
416
|
}
|
|
437
|
-
/** Hold a looping replay while the pointer is over the graph (`pause-on-hover`). */
|
|
438
|
-
#applyHold() {
|
|
439
|
-
this.controller.hold(this.#pointerOver && this.loop && this.pauseOnHover);
|
|
440
|
-
}
|
|
441
417
|
/** What the layout does for the regions: preferences reflect as attributes, panels open. */
|
|
442
|
-
|
|
418
|
+
actions() {
|
|
443
419
|
return {
|
|
444
|
-
|
|
445
|
-
setMode: (mode) => this.controller.setMode(mode),
|
|
446
|
-
toggleScope: () => this.controller.toggleScope(),
|
|
420
|
+
...this.baseActions(),
|
|
447
421
|
setThemePreference: (pref) => {
|
|
448
422
|
this.theme = pref;
|
|
423
|
+
this.#remember({ theme: this.theme });
|
|
449
424
|
},
|
|
450
425
|
setPalette: (p) => {
|
|
451
426
|
this.palette = p;
|
|
427
|
+
this.#remember({ palette: this.palette });
|
|
452
428
|
},
|
|
453
429
|
setLocale: (l) => {
|
|
454
430
|
this.locale = l;
|
|
431
|
+
this.#remember({ locale: this.locale });
|
|
455
432
|
},
|
|
456
433
|
toggleSidebar: (open) => this.#toggleSidebar(open),
|
|
457
434
|
sidebarOpen: () => this.#sidebarOpen,
|
|
435
|
+
hasSidebar: () => SIDEBAR_REGIONS.some((r) => this.#states[r] !== 'hidden'),
|
|
458
436
|
toggleMenu: (open) => this.#toggleMenu(open),
|
|
459
437
|
openHelp: () => this.#ui?.help.show(),
|
|
460
438
|
focusSearch: () => this.#focusSearch(),
|
|
439
|
+
setPanel: (region, state) => this.#userPanels({ [region]: state }),
|
|
461
440
|
};
|
|
462
441
|
}
|
|
463
442
|
#build() {
|
|
464
443
|
const root = h('div', { class: 'root' });
|
|
465
|
-
this
|
|
444
|
+
this.shadow.append(root);
|
|
466
445
|
// Each region binds to the root on its own (context protocol); the layout only places them.
|
|
467
446
|
const header = bindRegion(root, (ui) => new Header(ui));
|
|
468
447
|
const list = bindRegion(root, (ui) => new ChangeList(ui));
|
|
469
448
|
const search = bindRegion(root, (ui) => new Search(ui));
|
|
470
449
|
const filters = bindRegion(root, (ui) => new Filters(ui));
|
|
471
450
|
const detail = bindRegion(root, (ui) => new Detail(ui));
|
|
472
|
-
|
|
473
|
-
const
|
|
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);
|
|
474
457
|
const menu = new Menu(ui);
|
|
475
458
|
const help = new HelpDialog(ui);
|
|
476
|
-
const sidebar = new Sidebar(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);
|
|
477
464
|
// The title and the actions float over the canvas; the menu drops from the actions.
|
|
478
465
|
header.attachMenu(menu.el);
|
|
479
466
|
stage.el.appendChild(header.el);
|
|
480
|
-
root.append(h('div', { class: 'body' }, stage.el, sidebar.el), help.el, this
|
|
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();
|
|
481
470
|
this.#ui = { root, header, stage, list, sidebar, menu, help };
|
|
482
471
|
// Close the menu on outside clicks.
|
|
483
472
|
root.addEventListener('pointerdown', (e) => {
|
|
@@ -490,25 +479,37 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
490
479
|
const hover = (over) => (e) => {
|
|
491
480
|
if (e.pointerType === 'touch')
|
|
492
481
|
return;
|
|
493
|
-
this
|
|
494
|
-
this.#applyHold();
|
|
482
|
+
this.pointerOver(over);
|
|
495
483
|
};
|
|
496
484
|
stage.el.addEventListener('pointerenter', hover(true));
|
|
497
485
|
stage.el.addEventListener('pointerleave', hover(false));
|
|
498
|
-
|
|
486
|
+
sidebar.setSizes(SIDEBAR_WIDTH, null, true);
|
|
487
|
+
this.#applyPanels();
|
|
499
488
|
this.#applyChrome();
|
|
500
489
|
}
|
|
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);
|
|
507
|
+
}
|
|
501
508
|
/** The layout's part of a render (the root renders the regions after it). */
|
|
502
509
|
#update(topics) {
|
|
503
510
|
const ui = this.#ui;
|
|
504
511
|
const c = this.controller;
|
|
505
|
-
|
|
506
|
-
this.#reflect('mode', c.mode);
|
|
507
|
-
this.#reflect('scope', c.scope);
|
|
508
|
-
}
|
|
509
|
-
if (topics.has('theme')) {
|
|
510
|
-
this.#reflect('theme', c.themePreference === 'auto' && !this.hasAttribute('theme') ? null : c.themePreference);
|
|
511
|
-
}
|
|
512
|
+
this.reflectSession(topics);
|
|
512
513
|
if (!ui)
|
|
513
514
|
return;
|
|
514
515
|
if (topics.has('theme') || topics.has('status'))
|
|
@@ -528,10 +529,32 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
528
529
|
ui.root.dataset.scope = c.scope;
|
|
529
530
|
}
|
|
530
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
|
+
*/
|
|
531
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;
|
|
532
541
|
const next = open ?? !this.#sidebarOpen;
|
|
533
|
-
this
|
|
534
|
-
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);
|
|
535
558
|
}
|
|
536
559
|
#toggleMenu(open) {
|
|
537
560
|
const ui = this.#ui;
|
|
@@ -544,10 +567,17 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
544
567
|
ui.menu.hide(false);
|
|
545
568
|
ui.header.setMenuState(next);
|
|
546
569
|
}
|
|
570
|
+
/** `/`: an open search takes focus; else this layout's search opens, unless it is hidden. */
|
|
547
571
|
#focusSearch() {
|
|
572
|
+
if (this.root.focus('search'))
|
|
573
|
+
return;
|
|
574
|
+
if (this.#states.search === 'hidden')
|
|
575
|
+
return;
|
|
548
576
|
if (!this.#sidebarOpen)
|
|
549
577
|
this.#toggleSidebar(true);
|
|
550
|
-
this.#
|
|
578
|
+
if (this.#states.search === 'collapsed')
|
|
579
|
+
this.#userPanels({ search: 'open' });
|
|
580
|
+
this.root.focus('search');
|
|
551
581
|
}
|
|
552
582
|
#watchSize() {
|
|
553
583
|
if (typeof ResizeObserver !== 'function')
|
|
@@ -565,8 +595,7 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
565
595
|
}
|
|
566
596
|
ui.stage.layout();
|
|
567
597
|
ui.list.renderWindow();
|
|
568
|
-
|
|
569
|
-
ui.sidebar.syncSheet();
|
|
598
|
+
ui.sidebar.layout(compact);
|
|
570
599
|
this.controller.layoutChanged();
|
|
571
600
|
});
|
|
572
601
|
ro.observe(this);
|
|
@@ -575,75 +604,5 @@ export class KgdiffViewer extends HTMLElement {
|
|
|
575
604
|
ro.observe(this.#ui.stage.el);
|
|
576
605
|
this.#cleanup.push(() => ro.disconnect());
|
|
577
606
|
}
|
|
578
|
-
#onKeyDown(e) {
|
|
579
|
-
if (e.defaultPrevented || e.isComposing)
|
|
580
|
-
return;
|
|
581
|
-
const ui = this.#ui;
|
|
582
|
-
if (!ui || this.chrome === 'none')
|
|
583
|
-
return;
|
|
584
|
-
const path = e.composedPath();
|
|
585
|
-
if (!path.includes(this)) {
|
|
586
|
-
// Outside the component only with `global-shortcuts`, when nothing else has focus, for
|
|
587
|
-
// the first such viewer, and never the keys pages scroll with.
|
|
588
|
-
if (!this.hasAttribute('global-shortcuts') || PAGE_KEYS.has(e.key))
|
|
589
|
-
return;
|
|
590
|
-
const active = document.activeElement;
|
|
591
|
-
if (active && active !== document.body && active !== document.documentElement)
|
|
592
|
-
return;
|
|
593
|
-
if (connected.find((v) => v.hasAttribute('global-shortcuts')) !== this)
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
if (ui.help.open || ui.menu.open) {
|
|
597
|
-
if (e.key === 'Escape') {
|
|
598
|
-
e.preventDefault();
|
|
599
|
-
ui.help.hide();
|
|
600
|
-
this.#toggleMenu(false);
|
|
601
|
-
}
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
this.#root.handleKey(e);
|
|
605
|
-
}
|
|
606
|
-
// ─── Deep links ───────────────────────────────────────────────────────────
|
|
607
|
-
#readHash() {
|
|
608
|
-
if (!this.hasAttribute('sync-hash') || typeof location === 'undefined')
|
|
609
|
-
return;
|
|
610
|
-
if (location.hash === this.#lastHash)
|
|
611
|
-
return;
|
|
612
|
-
const state = parseHash(location.hash);
|
|
613
|
-
this.#lastHash = location.hash;
|
|
614
|
-
if (!state)
|
|
615
|
-
return;
|
|
616
|
-
if (state.mode || state.scope) {
|
|
617
|
-
// A deep link outranks the large-graph default and a report's preferred view.
|
|
618
|
-
const c = this.controller;
|
|
619
|
-
if (state.mode)
|
|
620
|
-
c.explicit.add('mode');
|
|
621
|
-
if (state.scope)
|
|
622
|
-
c.explicit.add('scope');
|
|
623
|
-
c.setView(state.mode ?? c.mode, state.scope ?? c.scope);
|
|
624
|
-
}
|
|
625
|
-
if (state.hunk)
|
|
626
|
-
this.controller.selectHunkById(state.hunk, { fly: true });
|
|
627
|
-
}
|
|
628
|
-
#writeHash() {
|
|
629
|
-
if (!this.hasAttribute('sync-hash') || typeof location === 'undefined')
|
|
630
|
-
return;
|
|
631
|
-
const c = this.controller;
|
|
632
|
-
const hunk = c.selectedHunk >= 0 ? c.scene?.hunks[c.selectedHunk]?.id : undefined;
|
|
633
|
-
const hash = formatHash({
|
|
634
|
-
...(hunk !== undefined ? { hunk } : {}),
|
|
635
|
-
mode: c.mode,
|
|
636
|
-
scope: c.scope,
|
|
637
|
-
});
|
|
638
|
-
if (hash === location.hash)
|
|
639
|
-
return;
|
|
640
|
-
this.#lastHash = hash;
|
|
641
|
-
try {
|
|
642
|
-
history.replaceState(history.state, '', hash);
|
|
643
|
-
}
|
|
644
|
-
catch {
|
|
645
|
-
location.hash = hash;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
607
|
}
|
|
649
608
|
//# sourceMappingURL=element.js.map
|