@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/src/element.ts
CHANGED
|
@@ -1,43 +1,41 @@
|
|
|
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
|
-
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import { h } from './dom.js';
|
|
12
|
-
import {
|
|
13
|
-
import { detectLocale, translate, translator } from './i18n.js';
|
|
14
|
+
|
|
15
|
+
import { provideContext } from './context.js';
|
|
16
|
+
import { ViewerController } from './controller.js';
|
|
17
|
+
import { h, isOpen } from './dom.js';
|
|
18
|
+
import { type StageViewContext, stageContext } from './elements/stage-context.js';
|
|
14
19
|
import type {
|
|
15
|
-
FitOptions,
|
|
16
20
|
KgdiffViewerElement,
|
|
17
21
|
LoadOptions,
|
|
22
|
+
PanelRegion,
|
|
23
|
+
PanelStates,
|
|
18
24
|
ReportPayload,
|
|
19
|
-
ReviewState,
|
|
20
|
-
ThemePreference,
|
|
21
25
|
ViewerChrome,
|
|
26
|
+
ViewerLayout,
|
|
22
27
|
ViewerLegend,
|
|
23
|
-
|
|
24
|
-
ViewerScope,
|
|
28
|
+
ViewerPanels,
|
|
25
29
|
} from './index.js';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
toPalette,
|
|
36
|
-
toScope,
|
|
37
|
-
toTheme,
|
|
38
|
-
} from './prefs.js';
|
|
39
|
-
import { bindRegion, uiContext, ViewerRoot } from './root.js';
|
|
40
|
-
import { ALL_TOPICS, type Topic } from './session.js';
|
|
30
|
+
import { LAYOUT_KEY, type Remembered, readRemembered, writeRemembered } from './layout-store.js';
|
|
31
|
+
import { presetPanels, resolvePanels, SIDEBAR_REGIONS, samePanels } from './panel-levels.js';
|
|
32
|
+
import { DEFAULT_PANELS, formatPanels, PANEL_REGIONS, parsePanels, toPanels } from './panels.js';
|
|
33
|
+
import { sanitizeView, toChrome, toLegend } from './prefs.js';
|
|
34
|
+
import { defaultStorage } from './review-store.js';
|
|
35
|
+
import { bindRegion, uiContext } from './root.js';
|
|
36
|
+
import { ROOT_ATTRIBUTES, RootElement } from './root-element.js';
|
|
37
|
+
import type { SessionDeps, SessionHost, Topic } from './session.js';
|
|
38
|
+
import { stylesheet } from './styles.js';
|
|
41
39
|
import { ChangeList } from './ui/change-list.js';
|
|
42
40
|
import type { UiActions } from './ui/context.js';
|
|
43
41
|
import { Detail } from './ui/detail.js';
|
|
@@ -45,7 +43,7 @@ import { Filters } from './ui/filters.js';
|
|
|
45
43
|
import { Header } from './ui/header.js';
|
|
46
44
|
import { HelpDialog, Menu } from './ui/popovers.js';
|
|
47
45
|
import { Search } from './ui/search.js';
|
|
48
|
-
import { Sidebar } from './ui/sidebar.js';
|
|
46
|
+
import { SIDEBAR_WIDTH, Sidebar } from './ui/sidebar.js';
|
|
49
47
|
import { Stage } from './ui/stage.js';
|
|
50
48
|
|
|
51
49
|
/** Below this width the sidebar becomes a bottom sheet. */
|
|
@@ -54,67 +52,41 @@ export const COMPACT_WIDTH = 760;
|
|
|
54
52
|
const NARROW_WIDTH = 1100;
|
|
55
53
|
|
|
56
54
|
const OBSERVED = [
|
|
57
|
-
|
|
58
|
-
'scope',
|
|
59
|
-
'theme',
|
|
60
|
-
'palette',
|
|
61
|
-
'locale',
|
|
55
|
+
...ROOT_ATTRIBUTES,
|
|
62
56
|
'sidebar',
|
|
63
|
-
'
|
|
64
|
-
'chrome',
|
|
65
|
-
'legend',
|
|
66
|
-
'loop',
|
|
67
|
-
'loop-delay',
|
|
68
|
-
'pause-on-hover',
|
|
69
|
-
] as const;
|
|
70
|
-
const UPGRADED = [
|
|
71
|
-
'mode',
|
|
72
|
-
'scope',
|
|
73
|
-
'theme',
|
|
74
|
-
'palette',
|
|
75
|
-
'locale',
|
|
57
|
+
'header',
|
|
76
58
|
'chrome',
|
|
77
59
|
'legend',
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'payload',
|
|
60
|
+
'panels',
|
|
61
|
+
'persist',
|
|
62
|
+
'persist-key',
|
|
82
63
|
] as const;
|
|
83
64
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'
|
|
89
|
-
'ArrowRight',
|
|
90
|
-
'Home',
|
|
91
|
-
'End',
|
|
92
|
-
'PageUp',
|
|
93
|
-
'PageDown',
|
|
94
|
-
' ',
|
|
95
|
-
'Spacebar',
|
|
96
|
-
]);
|
|
97
|
-
|
|
98
|
-
/** Connected viewers, in connection order (`global-shortcuts` goes to the first opted-in one). */
|
|
99
|
-
const connected: KgdiffViewer[] = [];
|
|
100
|
-
|
|
101
|
-
function deepFreeze<T>(value: T): T {
|
|
102
|
-
if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {
|
|
103
|
-
for (const v of Object.values(value)) deepFreeze(v);
|
|
104
|
-
Object.freeze(value);
|
|
105
|
-
}
|
|
106
|
-
return value;
|
|
65
|
+
type Preset = 'sidebar' | 'header' | 'chrome';
|
|
66
|
+
type MutablePanels = { -readonly [R in PanelRegion]?: PanelStates[R] };
|
|
67
|
+
|
|
68
|
+
function warnPanels(invalid: readonly string[]): void {
|
|
69
|
+
if (invalid.length > 0) console.warn(`[kgdiff] ignoring invalid panels: ${invalid.join(' ')}`);
|
|
107
70
|
}
|
|
108
71
|
|
|
109
|
-
|
|
72
|
+
/**
|
|
73
|
+
* @tagname kgdiff-viewer
|
|
74
|
+
* @attr {'open' | 'closed'} sidebar - A preset: `closed` starts with the sidebar folded (`\` and the
|
|
75
|
+
* sidebar button open it), `open` with its four regions open. Reflects the sidebar's state
|
|
76
|
+
* once the user toggles it.
|
|
77
|
+
* @attr {'compact'} header - A preset: `compact` leaves the brand and the compared names out of
|
|
78
|
+
* the canvas, for hosts that show their own (`panels="title:hidden"`).
|
|
79
|
+
* @slot top-start - The page's content over the canvas, top left (under the title while it shows).
|
|
80
|
+
* @slot top-end - The page's content over the canvas, top right (under the actions while they show).
|
|
81
|
+
* @slot bottom-start - The page's content over the canvas, bottom left, above the legend.
|
|
82
|
+
* @slot bottom-center - The page's content above the view dock and the transport.
|
|
83
|
+
* @slot bottom-end - The page's content over the canvas, bottom right, above the zoom buttons and the minimap.
|
|
84
|
+
*/
|
|
85
|
+
export class KgdiffViewer extends RootElement<ViewerController> implements KgdiffViewerElement {
|
|
110
86
|
static get observedAttributes(): readonly string[] {
|
|
111
87
|
return OBSERVED;
|
|
112
88
|
}
|
|
113
89
|
|
|
114
|
-
/** @internal */
|
|
115
|
-
readonly controller: ViewerController;
|
|
116
|
-
readonly #shadow: ShadowRoot;
|
|
117
|
-
readonly #root: ViewerRoot;
|
|
118
90
|
#ui: {
|
|
119
91
|
root: HTMLElement;
|
|
120
92
|
header: Header;
|
|
@@ -124,50 +96,52 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
124
96
|
menu: Menu;
|
|
125
97
|
help: HelpDialog;
|
|
126
98
|
} | null = null;
|
|
127
|
-
#currentPayload: ReportPayload | null = null;
|
|
128
|
-
#reflecting = false;
|
|
129
99
|
#compact = false;
|
|
130
100
|
readonly #cleanup: (() => void)[] = [];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
#
|
|
101
|
+
|
|
102
|
+
// Panel states, from the first level that names a region (see `panels.ts`).
|
|
103
|
+
/** What the user changed in this page. */
|
|
104
|
+
readonly #runtime: MutablePanels = {};
|
|
105
|
+
/** The host's `panels`. */
|
|
106
|
+
#hostPanels: ViewerPanels = {};
|
|
107
|
+
/** The presets, from the attributes as the host set them (not as the layout reflects them). */
|
|
108
|
+
readonly #presetAttrs: Record<Preset, string | null> = {
|
|
109
|
+
sidebar: null,
|
|
110
|
+
header: null,
|
|
111
|
+
chrome: null,
|
|
112
|
+
};
|
|
113
|
+
#presets: ViewerPanels = {};
|
|
114
|
+
/** What the user chose before (`persist`), read at the first connection. */
|
|
115
|
+
#remembered: Remembered = { panels: {} };
|
|
116
|
+
/** The report's `view.panels`. */
|
|
117
|
+
#report: ViewerPanels = {};
|
|
118
|
+
#states: PanelStates = DEFAULT_PANELS;
|
|
119
|
+
/** The sidebar's regions that were open when the user folded it. */
|
|
120
|
+
#folded: readonly PanelRegion[] | null = null;
|
|
121
|
+
/** `kgdiff-layout` reports changes from the first connection on. */
|
|
122
|
+
#layoutReady = false;
|
|
123
|
+
readonly #storage = defaultStorage();
|
|
135
124
|
|
|
136
125
|
constructor() {
|
|
137
126
|
super();
|
|
138
|
-
this.#shadow = this.attachShadow({ mode: 'open' });
|
|
139
|
-
this.#root = new ViewerRoot(this, this.#shadow, this.#actions(), (type) => {
|
|
140
|
-
if (type === 'kgdiff-select' || type === 'kgdiff-mode') this.#writeHash();
|
|
141
|
-
});
|
|
142
|
-
this.controller = this.#root.controller;
|
|
143
|
-
this.controller.locale = toLocale(this.getAttribute('locale')) ?? detectLocale();
|
|
144
127
|
// The layout renders first, then the regions in the order they bound to the root.
|
|
145
|
-
this
|
|
128
|
+
this.root.attach({ update: (topics) => this.#update(topics) });
|
|
146
129
|
}
|
|
147
130
|
|
|
148
131
|
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
149
132
|
|
|
150
|
-
|
|
151
|
-
if (!this.#upgraded) {
|
|
152
|
-
this.#upgraded = true;
|
|
153
|
-
// Properties set before the element was defined shadow the accessors: re-apply them.
|
|
154
|
-
for (const prop of UPGRADED) {
|
|
155
|
-
if (Object.hasOwn(this, prop)) {
|
|
156
|
-
const value = (this as unknown as Record<string, unknown>)[prop];
|
|
157
|
-
delete (this as unknown as Record<string, unknown>)[prop];
|
|
158
|
-
(this as unknown as Record<string, unknown>)[prop] = value;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// Provide the session (the regions bind to it) and follow the system's preferences.
|
|
163
|
-
this.#cleanup.push(this.#root.connect());
|
|
133
|
+
protected connected(): void {
|
|
164
134
|
if (!this.#ui) this.#build();
|
|
165
|
-
connected.push(this);
|
|
166
135
|
const ui = this.#ui;
|
|
167
136
|
if (!ui) return;
|
|
168
|
-
this
|
|
137
|
+
if (!this.#layoutReady) {
|
|
138
|
+
this.#loadRemembered();
|
|
139
|
+
this.#layoutReady = true;
|
|
140
|
+
}
|
|
141
|
+
// The stage adds its own minimap to the view (unless a slotted one replaces it).
|
|
142
|
+
this.controller.attach(ui.stage.canvasHost, null);
|
|
169
143
|
// The graph takes keyboard focus (shortcuts apply where focus is), also on click.
|
|
170
|
-
const canvas =
|
|
144
|
+
const canvas = this.#canvas();
|
|
171
145
|
if (canvas) {
|
|
172
146
|
canvas.tabIndex = this.chrome === 'none' ? -1 : 0;
|
|
173
147
|
const focusCanvas = (): void => {
|
|
@@ -177,171 +151,104 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
177
151
|
this.#cleanup.push(() => canvas.removeEventListener('pointerdown', focusCanvas));
|
|
178
152
|
}
|
|
179
153
|
this.#watchSize();
|
|
180
|
-
const onKey = (e: KeyboardEvent): void => this.#onKeyDown(e);
|
|
181
|
-
document.addEventListener('keydown', onKey);
|
|
182
|
-
this.#cleanup.push(() => document.removeEventListener('keydown', onKey));
|
|
183
|
-
const onHash = (): void => this.#readHash();
|
|
184
|
-
window.addEventListener('hashchange', onHash);
|
|
185
|
-
this.#cleanup.push(() => window.removeEventListener('hashchange', onHash));
|
|
186
154
|
this.controller.setChrome(() => ui.stage.chromeLayout());
|
|
187
155
|
document.fonts?.ready?.then(() => this.#ui?.stage.layout()).catch(() => undefined);
|
|
188
|
-
this.#readHash();
|
|
189
|
-
this.#root.render(ALL_TOPICS);
|
|
190
156
|
}
|
|
191
157
|
|
|
192
|
-
|
|
193
|
-
const i = connected.indexOf(this);
|
|
194
|
-
if (i >= 0) connected.splice(i, 1);
|
|
158
|
+
protected disconnected(): void {
|
|
195
159
|
for (const fn of this.#cleanup.splice(0)) fn();
|
|
196
160
|
this.controller.detach();
|
|
197
|
-
// Moved while hovered: no pointerleave follows.
|
|
198
|
-
this.#pointerOver = false;
|
|
199
|
-
this.#applyHold();
|
|
200
161
|
}
|
|
201
162
|
|
|
202
|
-
|
|
203
|
-
|
|
163
|
+
protected override attributeChanged(
|
|
164
|
+
name: string,
|
|
165
|
+
value: string | null,
|
|
166
|
+
old: string | null,
|
|
167
|
+
): void {
|
|
204
168
|
switch (name) {
|
|
205
|
-
case 'mode':
|
|
206
|
-
case 'scope':
|
|
207
|
-
case 'theme':
|
|
208
|
-
case 'palette':
|
|
209
|
-
case 'locale':
|
|
210
|
-
this.#applyPreference(name, value, false);
|
|
211
|
-
break;
|
|
212
169
|
case 'sidebar':
|
|
213
|
-
|
|
170
|
+
// The layout reflects the sidebar's state here (after `\`): the page changing the
|
|
171
|
+
// attribute from what it says now is a command, even back to what the page said
|
|
172
|
+
// before; setting what it already says is not.
|
|
173
|
+
this.#preset(name, value, value !== old);
|
|
214
174
|
break;
|
|
215
|
-
case '
|
|
216
|
-
|
|
175
|
+
case 'header':
|
|
176
|
+
this.#preset(name, value);
|
|
217
177
|
break;
|
|
218
178
|
case 'chrome':
|
|
219
|
-
|
|
179
|
+
this.#preset(name, value);
|
|
220
180
|
this.#applyChrome();
|
|
221
181
|
break;
|
|
222
|
-
case '
|
|
223
|
-
|
|
224
|
-
this.controller.setLoop(this.loop ? this.loopDelay : null);
|
|
225
|
-
this.#applyHold();
|
|
226
|
-
break;
|
|
227
|
-
case 'pause-on-hover':
|
|
228
|
-
this.#applyHold();
|
|
229
|
-
break;
|
|
230
|
-
default:
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Apply a preference from an attribute or a property. Valid values are explicit (a report's
|
|
237
|
-
* `view` does not override them); invalid ones fall back to the default (`overlay`, `all`,
|
|
238
|
-
* `auto`, `default`, the browser's language). Properties reflect the normalized value; the
|
|
239
|
-
* legacy mode `changes` reflects as `overlay` + scope `changes`.
|
|
240
|
-
*/
|
|
241
|
-
#applyPreference(
|
|
242
|
-
name: 'mode' | 'scope' | 'theme' | 'palette' | 'locale',
|
|
243
|
-
raw: unknown,
|
|
244
|
-
reflect: boolean,
|
|
245
|
-
): void {
|
|
246
|
-
const c = this.controller;
|
|
247
|
-
// `mode="changes"` is rewritten to the attribute pair it stands for.
|
|
248
|
-
const legacy = name === 'mode' && raw === LEGACY_CHANGES_MODE;
|
|
249
|
-
let value: string | null;
|
|
250
|
-
switch (name) {
|
|
251
|
-
case 'mode': {
|
|
252
|
-
if (legacy) {
|
|
253
|
-
c.explicit.add('scope');
|
|
254
|
-
c.setView('overlay', 'changes');
|
|
255
|
-
value = 'overlay';
|
|
256
|
-
break;
|
|
257
|
-
}
|
|
258
|
-
const v = toMode(raw);
|
|
259
|
-
c.setMode(v ?? 'overlay');
|
|
260
|
-
value = v;
|
|
261
|
-
break;
|
|
262
|
-
}
|
|
263
|
-
case 'scope': {
|
|
264
|
-
const v = toScope(raw);
|
|
265
|
-
c.setScope(v ?? 'all');
|
|
266
|
-
// A locked scope stays `changes`: the attribute says what is shown.
|
|
267
|
-
value = v === null ? null : c.scope;
|
|
182
|
+
case 'legend':
|
|
183
|
+
this.#applyChrome();
|
|
268
184
|
break;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
value = v;
|
|
185
|
+
case 'panels': {
|
|
186
|
+
const { panels, invalid } = parsePanels(value ?? '');
|
|
187
|
+
warnPanels(invalid);
|
|
188
|
+
this.#setHostPanels(panels);
|
|
274
189
|
break;
|
|
275
190
|
}
|
|
276
|
-
case '
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
191
|
+
case 'persist':
|
|
192
|
+
case 'persist-key':
|
|
193
|
+
// Turned on, or another store, in the page: what the user chose before there applies
|
|
194
|
+
// now, as a host change.
|
|
195
|
+
if (this.#layoutReady && this.persist === 'local') this.#remembered = this.#recall();
|
|
196
|
+
this.#resolve('host');
|
|
280
197
|
break;
|
|
281
|
-
}
|
|
282
|
-
case 'locale': {
|
|
283
|
-
const v = toLocale(raw);
|
|
284
|
-
c.setLocale(v ?? detectLocale());
|
|
285
|
-
value = v;
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
198
|
default:
|
|
289
|
-
|
|
199
|
+
break;
|
|
290
200
|
}
|
|
291
|
-
if (value === null) c.explicit.delete(name);
|
|
292
|
-
else c.explicit.add(name);
|
|
293
|
-
if (reflect || legacy) this.#reflect(name, value);
|
|
294
|
-
if (legacy) this.#reflect('scope', c.scope);
|
|
295
201
|
}
|
|
296
202
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
get mode(): ViewerMode {
|
|
300
|
-
return this.controller.mode;
|
|
203
|
+
protected override upgradedProperties(): readonly string[] {
|
|
204
|
+
return ['chrome', 'legend', 'panels', 'persist', 'persistKey'];
|
|
301
205
|
}
|
|
302
206
|
|
|
303
|
-
|
|
304
|
-
|
|
207
|
+
protected sheets(doc: Document): readonly CSSStyleSheet[] {
|
|
208
|
+
return [stylesheet(doc)];
|
|
305
209
|
}
|
|
306
210
|
|
|
307
|
-
|
|
308
|
-
return
|
|
211
|
+
protected createSession(host: SessionHost, deps: SessionDeps): ViewerController {
|
|
212
|
+
return new ViewerController(host, deps);
|
|
309
213
|
}
|
|
310
214
|
|
|
311
|
-
|
|
312
|
-
this.#
|
|
215
|
+
protected override keysEnabled(): boolean {
|
|
216
|
+
return this.#ui !== null && this.chrome !== 'none';
|
|
313
217
|
}
|
|
314
218
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return this.controller.palette;
|
|
219
|
+
protected override interceptKey(e: KeyboardEvent): boolean {
|
|
220
|
+
const ui = this.#ui;
|
|
221
|
+
if (!ui || !(ui.help.open || ui.menu.open)) return false;
|
|
222
|
+
if (e.key === 'Escape') {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
ui.help.hide();
|
|
225
|
+
this.#toggleMenu(false);
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
325
228
|
}
|
|
326
229
|
|
|
327
|
-
|
|
328
|
-
|
|
230
|
+
protected override payloadSet(payload: ReportPayload): void {
|
|
231
|
+
// Validated (and warned about) once more when it loads.
|
|
232
|
+
this.#report = sanitizeView(payload.view, true)?.panels ?? {};
|
|
233
|
+
this.#resolve('host');
|
|
329
234
|
}
|
|
330
235
|
|
|
331
|
-
|
|
332
|
-
|
|
236
|
+
override load(options: LoadOptions): Promise<void> {
|
|
237
|
+
this.#report = {};
|
|
238
|
+
this.#resolve('host');
|
|
239
|
+
return super.load(options);
|
|
333
240
|
}
|
|
334
241
|
|
|
335
|
-
|
|
336
|
-
this.#applyPreference('locale', value, true);
|
|
337
|
-
}
|
|
242
|
+
// ─── Properties ───────────────────────────────────────────────────────────
|
|
338
243
|
|
|
339
244
|
get chrome(): ViewerChrome {
|
|
340
245
|
return toChrome(this.getAttribute('chrome')) ?? 'full';
|
|
341
246
|
}
|
|
342
247
|
|
|
343
248
|
set chrome(value: ViewerChrome) {
|
|
344
|
-
|
|
249
|
+
const v = toChrome(value) === 'none' ? 'none' : null;
|
|
250
|
+
this.reflect('chrome', v);
|
|
251
|
+
this.#preset('chrome', v);
|
|
345
252
|
this.#applyChrome();
|
|
346
253
|
}
|
|
347
254
|
|
|
@@ -351,134 +258,199 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
351
258
|
|
|
352
259
|
set legend(value: ViewerLegend) {
|
|
353
260
|
const v = toLegend(value);
|
|
354
|
-
this
|
|
261
|
+
this.reflect('legend', v === 'auto' ? null : v);
|
|
355
262
|
this.#applyChrome();
|
|
356
263
|
}
|
|
357
264
|
|
|
358
|
-
|
|
359
|
-
|
|
265
|
+
/** The host's declaration (the `panels` attribute as an object); `layout` has the states. */
|
|
266
|
+
get panels(): ViewerPanels {
|
|
267
|
+
return { ...this.#hostPanels };
|
|
360
268
|
}
|
|
361
269
|
|
|
362
|
-
set
|
|
363
|
-
|
|
270
|
+
set panels(value: ViewerPanels | string | null) {
|
|
271
|
+
const { panels, invalid } = toPanels(value);
|
|
272
|
+
warnPanels(invalid);
|
|
273
|
+
this.reflect('panels', formatPanels(panels) || null);
|
|
274
|
+
this.#setHostPanels(panels);
|
|
364
275
|
}
|
|
365
276
|
|
|
366
|
-
get
|
|
367
|
-
return
|
|
277
|
+
get persist(): 'local' | 'none' {
|
|
278
|
+
return this.getAttribute('persist') === 'none' ? 'none' : 'local';
|
|
368
279
|
}
|
|
369
280
|
|
|
370
|
-
set
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
else this.setAttribute('loop-delay', String(v));
|
|
281
|
+
set persist(value: 'local' | 'none') {
|
|
282
|
+
if (value === 'none') this.setAttribute('persist', 'none');
|
|
283
|
+
else this.removeAttribute('persist');
|
|
374
284
|
}
|
|
375
285
|
|
|
376
|
-
|
|
377
|
-
|
|
286
|
+
/** Where the layout remembers (`persist-key`): a `localStorage` key, `kgdiff:viewer` by default. */
|
|
287
|
+
get persistKey(): string {
|
|
288
|
+
return this.getAttribute('persist-key') || LAYOUT_KEY;
|
|
378
289
|
}
|
|
379
290
|
|
|
380
|
-
set
|
|
381
|
-
if (value) this.removeAttribute('
|
|
382
|
-
else this.setAttribute('
|
|
291
|
+
set persistKey(value: string) {
|
|
292
|
+
if (!value || value === LAYOUT_KEY) this.removeAttribute('persist-key');
|
|
293
|
+
else this.setAttribute('persist-key', String(value));
|
|
383
294
|
}
|
|
384
295
|
|
|
385
|
-
|
|
386
|
-
|
|
296
|
+
/** Every region's state and the sizes now, as `kgdiff-layout` reports them. */
|
|
297
|
+
get layout(): ViewerLayout {
|
|
298
|
+
const sidebar = this.#ui?.sidebar;
|
|
299
|
+
return {
|
|
300
|
+
panels: { ...this.#states },
|
|
301
|
+
sidebarWidth: sidebar?.width ?? SIDEBAR_WIDTH,
|
|
302
|
+
detailShare: sidebar?.detailShare ?? null,
|
|
303
|
+
};
|
|
387
304
|
}
|
|
388
305
|
|
|
389
|
-
|
|
390
|
-
this.#currentPayload = value;
|
|
391
|
-
if (value === null || value === undefined) return;
|
|
392
|
-
let payload: ReportPayload;
|
|
393
|
-
try {
|
|
394
|
-
payload = assertPayload(value);
|
|
395
|
-
} catch (error) {
|
|
396
|
-
this.controller.fail(error as Error);
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
this.controller.loadPayload(payload).catch(() => {
|
|
400
|
-
// Already surfaced: error state + `kgdiff-error`.
|
|
401
|
-
});
|
|
402
|
-
}
|
|
306
|
+
// ─── Panels ───────────────────────────────────────────────────────────────
|
|
403
307
|
|
|
404
|
-
|
|
308
|
+
/** Some region's state may have changed: apply the new states, and report them. */
|
|
309
|
+
#resolve(source: 'user' | 'host'): void {
|
|
310
|
+
const remembered = this.persist === 'local' ? this.#remembered.panels : {};
|
|
311
|
+
const next = resolvePanels([
|
|
312
|
+
this.#runtime,
|
|
313
|
+
this.#hostPanels,
|
|
314
|
+
this.#presets,
|
|
315
|
+
remembered,
|
|
316
|
+
this.#report,
|
|
317
|
+
]);
|
|
318
|
+
if (samePanels(next, this.#states)) return;
|
|
319
|
+
this.#states = next;
|
|
320
|
+
this.#applyPanels();
|
|
321
|
+
if (this.#layoutReady) this.#emitLayout(source);
|
|
322
|
+
}
|
|
405
323
|
|
|
406
|
-
|
|
407
|
-
this
|
|
408
|
-
|
|
324
|
+
#emitLayout(source: 'user' | 'host'): void {
|
|
325
|
+
this.dispatchEvent(
|
|
326
|
+
new CustomEvent('kgdiff-layout', {
|
|
327
|
+
detail: { ...this.layout, source },
|
|
328
|
+
bubbles: true,
|
|
329
|
+
composed: true,
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
409
332
|
}
|
|
410
333
|
|
|
411
|
-
|
|
412
|
-
|
|
334
|
+
/**
|
|
335
|
+
* The host declared `panels`. Where its value changed, what the user changed there in this
|
|
336
|
+
* page gives way; declaring the same again (a framework re-rendering, an attribute set to
|
|
337
|
+
* the value it has) leaves the user's change alone.
|
|
338
|
+
*/
|
|
339
|
+
#setHostPanels(panels: ViewerPanels): void {
|
|
340
|
+
this.#yield(this.#hostPanels, panels);
|
|
341
|
+
this.#hostPanels = panels;
|
|
342
|
+
this.#resolve('host');
|
|
413
343
|
}
|
|
414
344
|
|
|
415
|
-
|
|
416
|
-
|
|
345
|
+
/**
|
|
346
|
+
* A preset attribute changed. `commanded` (`sidebar`, which the layout reflects): the
|
|
347
|
+
* sidebar's regions yield even when the preset the page set last is the same.
|
|
348
|
+
*/
|
|
349
|
+
#preset(name: Preset, value: string | null, commanded = false): void {
|
|
350
|
+
this.#presetAttrs[name] = value;
|
|
351
|
+
const { sidebar, header, chrome } = this.#presetAttrs;
|
|
352
|
+
const next = presetPanels(sidebar, header, chrome);
|
|
353
|
+
const yielded = this.#yield(this.#presets, next);
|
|
354
|
+
if (commanded) {
|
|
355
|
+
for (const r of SIDEBAR_REGIONS) delete this.#runtime[r];
|
|
356
|
+
}
|
|
357
|
+
if (commanded || yielded.some((r) => SIDEBAR_REGIONS.includes(r))) this.#folded = null;
|
|
358
|
+
this.#presets = next;
|
|
359
|
+
this.#resolve('host');
|
|
417
360
|
}
|
|
418
361
|
|
|
419
|
-
|
|
420
|
-
|
|
362
|
+
/** Drop the user's in-page change of every region `before` and `after` disagree on. */
|
|
363
|
+
#yield(before: ViewerPanels, after: ViewerPanels): PanelRegion[] {
|
|
364
|
+
const changed = PANEL_REGIONS.filter((r) => before[r] !== after[r]);
|
|
365
|
+
for (const r of changed) delete this.#runtime[r];
|
|
366
|
+
return changed;
|
|
421
367
|
}
|
|
422
368
|
|
|
423
|
-
|
|
424
|
-
|
|
369
|
+
/** The user changed some regions: they hold in this page, and are remembered. */
|
|
370
|
+
#userPanels(changes: ViewerPanels): void {
|
|
371
|
+
Object.assign(this.#runtime, changes);
|
|
372
|
+
this.#remember({ panels: { ...this.#remembered.panels, ...changes } });
|
|
373
|
+
this.#resolve('user');
|
|
425
374
|
}
|
|
426
375
|
|
|
427
|
-
|
|
428
|
-
|
|
376
|
+
/** What the user chose before; theme, palette and language apply unless the host set them. */
|
|
377
|
+
#recall(): Remembered {
|
|
378
|
+
const r = readRemembered(this.#storage, this.persistKey);
|
|
379
|
+
// A preference the host set wins; one the user chose before wins over a report's view.
|
|
380
|
+
const c = this.controller;
|
|
381
|
+
if (r.theme && !this.hasAttribute('theme') && !c.explicit.has('theme')) {
|
|
382
|
+
c.setThemePreference(r.theme);
|
|
383
|
+
c.explicit.add('theme');
|
|
384
|
+
}
|
|
385
|
+
if (r.palette && !this.hasAttribute('palette') && !c.explicit.has('palette')) {
|
|
386
|
+
c.setPalette(r.palette);
|
|
387
|
+
c.explicit.add('palette');
|
|
388
|
+
}
|
|
389
|
+
if (r.locale && !this.hasAttribute('locale') && !c.explicit.has('locale')) {
|
|
390
|
+
c.setLocale(r.locale);
|
|
391
|
+
c.explicit.add('locale');
|
|
392
|
+
}
|
|
393
|
+
this.#ui?.sidebar.setSizes(r.sidebarWidth ?? SIDEBAR_WIDTH, r.detailShare ?? null, true);
|
|
394
|
+
return r;
|
|
429
395
|
}
|
|
430
396
|
|
|
431
|
-
|
|
432
|
-
|
|
397
|
+
/** The first connection: the remembered states are the first ones (not a change, no event). */
|
|
398
|
+
#loadRemembered(): void {
|
|
399
|
+
if (this.persist !== 'local') return;
|
|
400
|
+
this.#remembered = this.#recall();
|
|
401
|
+
this.#resolve('host');
|
|
433
402
|
}
|
|
434
403
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
404
|
+
/** Remember the user's choice (merged into what other viewers of this origin wrote). */
|
|
405
|
+
#remember(patch: Partial<Remembered>): void {
|
|
406
|
+
if (this.persist !== 'local') return;
|
|
407
|
+
const stored = readRemembered(this.#storage, this.persistKey);
|
|
408
|
+
const next: Remembered = {
|
|
409
|
+
...stored,
|
|
410
|
+
...patch,
|
|
411
|
+
panels: { ...stored.panels, ...(patch.panels ?? {}) },
|
|
412
|
+
};
|
|
413
|
+
this.#remembered = { ...this.#remembered, ...patch, panels: next.panels };
|
|
414
|
+
writeRemembered(this.#storage, next, this.persistKey);
|
|
440
415
|
}
|
|
441
416
|
|
|
442
|
-
|
|
443
|
-
|
|
417
|
+
/** The user resized the sidebar or moved the split (once the drag or key press ended). */
|
|
418
|
+
#resized(width: number, detailShare: number | null): void {
|
|
419
|
+
this.#remember({ sidebarWidth: width, detailShare });
|
|
420
|
+
this.#emitLayout('user');
|
|
444
421
|
}
|
|
445
422
|
|
|
446
|
-
|
|
447
|
-
this
|
|
423
|
+
#applyPanels(): void {
|
|
424
|
+
const ui = this.#ui;
|
|
425
|
+
if (!ui) return;
|
|
426
|
+
const s = this.#states;
|
|
427
|
+
ui.sidebar.setPanels(s);
|
|
428
|
+
ui.stage.setPanels(s);
|
|
429
|
+
const sidebar = SIDEBAR_REGIONS.some((r) => s[r] !== 'hidden');
|
|
430
|
+
// A `<kgdiff-title>` or `<kgdiff-actions>` slotted into the viewer replaces the HUD's.
|
|
431
|
+
const title = ui.stage.replaces('title') ? 'hidden' : s.title;
|
|
432
|
+
const actions = ui.stage.replaces('actions') ? 'hidden' : s.actions;
|
|
433
|
+
ui.header.setPanels(title, actions, sidebar, () => this.#canvas());
|
|
434
|
+
ui.header.setSidebarState(this.#sidebarOpen);
|
|
435
|
+
// The top corners sit under what the HUD shows there.
|
|
436
|
+
const hud = [title === 'open' ? 'title' : '', actions === 'open' ? 'actions' : ''];
|
|
437
|
+
ui.stage.el.dataset.hud = hud.filter(Boolean).join(' ');
|
|
448
438
|
}
|
|
449
439
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.controller.fail(error);
|
|
440
|
+
#canvas(): HTMLElement | null {
|
|
441
|
+
return this.#ui?.stage.canvasHost.querySelector('canvas') ?? null;
|
|
453
442
|
}
|
|
454
443
|
|
|
455
444
|
// ─── Internals ────────────────────────────────────────────────────────────
|
|
456
445
|
|
|
457
|
-
|
|
458
|
-
if (this.getAttribute(name) === value) return;
|
|
459
|
-
this.#reflecting = true;
|
|
460
|
-
try {
|
|
461
|
-
if (value === null) this.removeAttribute(name);
|
|
462
|
-
else this.setAttribute(name, value);
|
|
463
|
-
} finally {
|
|
464
|
-
this.#reflecting = false;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
446
|
+
/** The sidebar is open while one of its regions is. */
|
|
468
447
|
get #sidebarOpen(): boolean {
|
|
469
|
-
return
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
#applySidebar(): void {
|
|
473
|
-
const ui = this.#ui;
|
|
474
|
-
if (!ui) return;
|
|
475
|
-
ui.sidebar.setOpen(this.#sidebarOpen);
|
|
476
|
-
ui.header.setSidebarState(this.#sidebarOpen);
|
|
448
|
+
return SIDEBAR_REGIONS.some((r) => this.#states[r] === 'open');
|
|
477
449
|
}
|
|
478
450
|
|
|
479
451
|
/**
|
|
480
452
|
* `chrome` and `legend`: what the stage shows, whether clicks select, whether the view stays
|
|
481
|
-
* fitted.
|
|
453
|
+
* fitted. Which regions show is the `chrome="none"` preset's (see `panels.ts`).
|
|
482
454
|
*/
|
|
483
455
|
#applyChrome(): void {
|
|
484
456
|
const bare = this.chrome === 'none';
|
|
@@ -491,7 +463,7 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
491
463
|
ui.root.dataset.chrome = this.chrome;
|
|
492
464
|
const legend = this.legend;
|
|
493
465
|
ui.stage.setChrome(legend === 'auto' ? (bare ? 'mini' : 'full') : legend, !bare);
|
|
494
|
-
const canvas =
|
|
466
|
+
const canvas = this.#canvas();
|
|
495
467
|
if (canvas) canvas.tabIndex = bare ? -1 : 0;
|
|
496
468
|
if (bare) {
|
|
497
469
|
ui.help.hide();
|
|
@@ -499,52 +471,67 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
499
471
|
}
|
|
500
472
|
}
|
|
501
473
|
|
|
502
|
-
/** Hold a looping replay while the pointer is over the graph (`pause-on-hover`). */
|
|
503
|
-
#applyHold(): void {
|
|
504
|
-
this.controller.hold(this.#pointerOver && this.loop && this.pauseOnHover);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
474
|
/** What the layout does for the regions: preferences reflect as attributes, panels open. */
|
|
508
|
-
|
|
475
|
+
protected actions(): UiActions {
|
|
509
476
|
return {
|
|
510
|
-
|
|
511
|
-
setMode: (mode) => this.controller.setMode(mode),
|
|
512
|
-
toggleScope: () => this.controller.toggleScope(),
|
|
477
|
+
...this.baseActions(),
|
|
513
478
|
setThemePreference: (pref) => {
|
|
514
479
|
this.theme = pref;
|
|
480
|
+
this.#remember({ theme: this.theme });
|
|
515
481
|
},
|
|
516
482
|
setPalette: (p) => {
|
|
517
483
|
this.palette = p;
|
|
484
|
+
this.#remember({ palette: this.palette });
|
|
518
485
|
},
|
|
519
486
|
setLocale: (l) => {
|
|
520
487
|
this.locale = l;
|
|
488
|
+
this.#remember({ locale: this.locale });
|
|
521
489
|
},
|
|
522
490
|
toggleSidebar: (open) => this.#toggleSidebar(open),
|
|
523
491
|
sidebarOpen: () => this.#sidebarOpen,
|
|
492
|
+
hasSidebar: () => SIDEBAR_REGIONS.some((r) => this.#states[r] !== 'hidden'),
|
|
524
493
|
toggleMenu: (open) => this.#toggleMenu(open),
|
|
525
494
|
openHelp: () => this.#ui?.help.show(),
|
|
526
495
|
focusSearch: () => this.#focusSearch(),
|
|
496
|
+
setPanel: (region, state) => this.#userPanels({ [region]: state }),
|
|
527
497
|
};
|
|
528
498
|
}
|
|
529
499
|
|
|
530
500
|
#build(): void {
|
|
531
501
|
const root = h('div', { class: 'root' });
|
|
532
|
-
this
|
|
502
|
+
this.shadow.append(root);
|
|
533
503
|
// Each region binds to the root on its own (context protocol); the layout only places them.
|
|
534
504
|
const header = bindRegion(root, (ui) => new Header(ui));
|
|
535
505
|
const list = bindRegion(root, (ui) => new ChangeList(ui));
|
|
536
506
|
const search = bindRegion(root, (ui) => new Search(ui));
|
|
537
507
|
const filters = bindRegion(root, (ui) => new Filters(ui));
|
|
538
508
|
const detail = bindRegion(root, (ui) => new Detail(ui));
|
|
539
|
-
|
|
540
|
-
const
|
|
509
|
+
// The stage's slots are the viewer's: the page's content goes over the canvas.
|
|
510
|
+
const stage = bindRegion(root, (ui) => new Stage(ui, true));
|
|
511
|
+
stage.view = this.controller.view;
|
|
512
|
+
stage.onReplaced = () => this.#applyPanels();
|
|
513
|
+
this.#provideStage(stage);
|
|
514
|
+
const ui = uiContext(this.root, this.shadow);
|
|
541
515
|
const menu = new Menu(ui);
|
|
542
516
|
const help = new HelpDialog(ui);
|
|
543
|
-
const sidebar = new Sidebar(
|
|
517
|
+
const sidebar = new Sidebar(
|
|
518
|
+
ui,
|
|
519
|
+
{
|
|
520
|
+
setPanel: (region, state) => this.#userPanels({ [region]: state }),
|
|
521
|
+
resized: (width, share) => this.#resized(width, share),
|
|
522
|
+
focusOutside: () => (isOpen(header.sidebarButton) ? header.sidebarButton : this.#canvas()),
|
|
523
|
+
},
|
|
524
|
+
list,
|
|
525
|
+
search,
|
|
526
|
+
filters,
|
|
527
|
+
detail,
|
|
528
|
+
);
|
|
544
529
|
// The title and the actions float over the canvas; the menu drops from the actions.
|
|
545
530
|
header.attachMenu(menu.el);
|
|
546
531
|
stage.el.appendChild(header.el);
|
|
547
|
-
root.append(h('div', { class: 'body' }, stage.el, sidebar.el), help.el, this
|
|
532
|
+
root.append(h('div', { class: 'body' }, stage.el, sidebar.el), help.el, this.root.live);
|
|
533
|
+
// Content slotted before the layout was built.
|
|
534
|
+
stage.slotted();
|
|
548
535
|
this.#ui = { root, header, stage, list, sidebar, menu, help };
|
|
549
536
|
// Close the menu on outside clicks.
|
|
550
537
|
root.addEventListener('pointerdown', (e) => {
|
|
@@ -554,29 +541,39 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
554
541
|
});
|
|
555
542
|
const hover = (over: boolean) => (e: PointerEvent) => {
|
|
556
543
|
if (e.pointerType === 'touch') return;
|
|
557
|
-
this
|
|
558
|
-
this.#applyHold();
|
|
544
|
+
this.pointerOver(over);
|
|
559
545
|
};
|
|
560
546
|
stage.el.addEventListener('pointerenter', hover(true));
|
|
561
547
|
stage.el.addEventListener('pointerleave', hover(false));
|
|
562
|
-
|
|
548
|
+
sidebar.setSizes(SIDEBAR_WIDTH, null, true);
|
|
549
|
+
this.#applyPanels();
|
|
563
550
|
this.#applyChrome();
|
|
564
551
|
}
|
|
565
552
|
|
|
553
|
+
/**
|
|
554
|
+
* The viewer is the stage of the chrome that acts on a view (`<kgdiff-zoom>`,
|
|
555
|
+
* `<kgdiff-minimap>`) inside it or naming it with `stage`: its built-in stage's view.
|
|
556
|
+
*/
|
|
557
|
+
#provideStage(stage: Stage): void {
|
|
558
|
+
const view = this.controller.view;
|
|
559
|
+
const ctx: StageViewContext = {
|
|
560
|
+
stage: this,
|
|
561
|
+
root: this.root,
|
|
562
|
+
view,
|
|
563
|
+
registerObstacle: (el) => stage.registerObstacle(el),
|
|
564
|
+
watchView: (listener) => {
|
|
565
|
+
listener(view);
|
|
566
|
+
return () => undefined;
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
provideContext(this, stageContext, () => ctx);
|
|
570
|
+
}
|
|
571
|
+
|
|
566
572
|
/** The layout's part of a render (the root renders the regions after it). */
|
|
567
573
|
#update(topics: ReadonlySet<Topic>): void {
|
|
568
574
|
const ui = this.#ui;
|
|
569
575
|
const c = this.controller;
|
|
570
|
-
|
|
571
|
-
this.#reflect('mode', c.mode);
|
|
572
|
-
this.#reflect('scope', c.scope);
|
|
573
|
-
}
|
|
574
|
-
if (topics.has('theme')) {
|
|
575
|
-
this.#reflect(
|
|
576
|
-
'theme',
|
|
577
|
-
c.themePreference === 'auto' && !this.hasAttribute('theme') ? null : c.themePreference,
|
|
578
|
-
);
|
|
579
|
-
}
|
|
576
|
+
this.reflectSession(topics);
|
|
580
577
|
if (!ui) return;
|
|
581
578
|
if (topics.has('theme') || topics.has('status')) ui.root.dataset.theme = c.themeName;
|
|
582
579
|
if (topics.has('locale') || topics.has('status')) {
|
|
@@ -592,10 +589,27 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
592
589
|
}
|
|
593
590
|
}
|
|
594
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Fold the sidebar (its open regions collapse) or open it again (the regions that were open,
|
|
594
|
+
* or all four). Reflected to the `sidebar` attribute.
|
|
595
|
+
*/
|
|
595
596
|
#toggleSidebar(open?: boolean): void {
|
|
597
|
+
const s = this.#states;
|
|
598
|
+
const shown = SIDEBAR_REGIONS.filter((r) => s[r] !== 'hidden');
|
|
599
|
+
if (shown.length === 0) return;
|
|
596
600
|
const next = open ?? !this.#sidebarOpen;
|
|
597
|
-
this
|
|
598
|
-
this.#
|
|
601
|
+
this.reflect('sidebar', next ? 'open' : 'closed');
|
|
602
|
+
if (next === this.#sidebarOpen) return;
|
|
603
|
+
const changes: MutablePanels = {};
|
|
604
|
+
if (next) {
|
|
605
|
+
const restore = this.#folded?.filter((r) => shown.includes(r)) ?? [];
|
|
606
|
+
for (const r of restore.length > 0 ? restore : shown) changes[r] = 'open';
|
|
607
|
+
this.#folded = null;
|
|
608
|
+
} else {
|
|
609
|
+
this.#folded = shown.filter((r) => s[r] === 'open');
|
|
610
|
+
for (const r of shown) changes[r] = 'collapsed';
|
|
611
|
+
}
|
|
612
|
+
this.#userPanels(changes);
|
|
599
613
|
}
|
|
600
614
|
|
|
601
615
|
#toggleMenu(open?: boolean): void {
|
|
@@ -607,9 +621,13 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
607
621
|
ui.header.setMenuState(next);
|
|
608
622
|
}
|
|
609
623
|
|
|
624
|
+
/** `/`: an open search takes focus; else this layout's search opens, unless it is hidden. */
|
|
610
625
|
#focusSearch(): void {
|
|
626
|
+
if (this.root.focus('search')) return;
|
|
627
|
+
if (this.#states.search === 'hidden') return;
|
|
611
628
|
if (!this.#sidebarOpen) this.#toggleSidebar(true);
|
|
612
|
-
this.#
|
|
629
|
+
if (this.#states.search === 'collapsed') this.#userPanels({ search: 'open' });
|
|
630
|
+
this.root.focus('search');
|
|
613
631
|
}
|
|
614
632
|
|
|
615
633
|
#watchSize(): void {
|
|
@@ -626,7 +644,7 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
626
644
|
}
|
|
627
645
|
ui.stage.layout();
|
|
628
646
|
ui.list.renderWindow();
|
|
629
|
-
|
|
647
|
+
ui.sidebar.layout(compact);
|
|
630
648
|
this.controller.layoutChanged();
|
|
631
649
|
});
|
|
632
650
|
ro.observe(this);
|
|
@@ -634,64 +652,4 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
634
652
|
if (this.#ui) ro.observe(this.#ui.stage.el);
|
|
635
653
|
this.#cleanup.push(() => ro.disconnect());
|
|
636
654
|
}
|
|
637
|
-
|
|
638
|
-
#onKeyDown(e: KeyboardEvent): void {
|
|
639
|
-
if (e.defaultPrevented || e.isComposing) return;
|
|
640
|
-
const ui = this.#ui;
|
|
641
|
-
if (!ui || this.chrome === 'none') return;
|
|
642
|
-
const path = e.composedPath();
|
|
643
|
-
if (!path.includes(this)) {
|
|
644
|
-
// Outside the component only with `global-shortcuts`, when nothing else has focus, for
|
|
645
|
-
// the first such viewer, and never the keys pages scroll with.
|
|
646
|
-
if (!this.hasAttribute('global-shortcuts') || PAGE_KEYS.has(e.key)) return;
|
|
647
|
-
const active = document.activeElement;
|
|
648
|
-
if (active && active !== document.body && active !== document.documentElement) return;
|
|
649
|
-
if (connected.find((v) => v.hasAttribute('global-shortcuts')) !== this) return;
|
|
650
|
-
}
|
|
651
|
-
if (ui.help.open || ui.menu.open) {
|
|
652
|
-
if (e.key === 'Escape') {
|
|
653
|
-
e.preventDefault();
|
|
654
|
-
ui.help.hide();
|
|
655
|
-
this.#toggleMenu(false);
|
|
656
|
-
}
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
this.#root.handleKey(e);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// ─── Deep links ───────────────────────────────────────────────────────────
|
|
663
|
-
|
|
664
|
-
#readHash(): void {
|
|
665
|
-
if (!this.hasAttribute('sync-hash') || typeof location === 'undefined') return;
|
|
666
|
-
if (location.hash === this.#lastHash) return;
|
|
667
|
-
const state = parseHash(location.hash);
|
|
668
|
-
this.#lastHash = location.hash;
|
|
669
|
-
if (!state) return;
|
|
670
|
-
if (state.mode || state.scope) {
|
|
671
|
-
// A deep link outranks the large-graph default and a report's preferred view.
|
|
672
|
-
const c = this.controller;
|
|
673
|
-
if (state.mode) c.explicit.add('mode');
|
|
674
|
-
if (state.scope) c.explicit.add('scope');
|
|
675
|
-
c.setView(state.mode ?? c.mode, state.scope ?? c.scope);
|
|
676
|
-
}
|
|
677
|
-
if (state.hunk) this.controller.selectHunkById(state.hunk, { fly: true });
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
#writeHash(): void {
|
|
681
|
-
if (!this.hasAttribute('sync-hash') || typeof location === 'undefined') return;
|
|
682
|
-
const c = this.controller;
|
|
683
|
-
const hunk = c.selectedHunk >= 0 ? c.scene?.hunks[c.selectedHunk]?.id : undefined;
|
|
684
|
-
const hash = formatHash({
|
|
685
|
-
...(hunk !== undefined ? { hunk } : {}),
|
|
686
|
-
mode: c.mode,
|
|
687
|
-
scope: c.scope,
|
|
688
|
-
});
|
|
689
|
-
if (hash === location.hash) return;
|
|
690
|
-
this.#lastHash = hash;
|
|
691
|
-
try {
|
|
692
|
-
history.replaceState(history.state, '', hash);
|
|
693
|
-
} catch {
|
|
694
|
-
location.hash = hash;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
655
|
}
|