@webpieces/nx-webpieces-rules 0.4.859 → 0.4.860
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.860",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules, graph tooling, executors, and inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@webpieces/api-doc-model": "0.4.
|
|
24
|
-
"@webpieces/core-util": "0.4.
|
|
25
|
-
"@webpieces/eslint-rules": "0.4.
|
|
26
|
-
"@webpieces/rules-config": "0.4.
|
|
23
|
+
"@webpieces/api-doc-model": "0.4.860",
|
|
24
|
+
"@webpieces/core-util": "0.4.860",
|
|
25
|
+
"@webpieces/eslint-rules": "0.4.860",
|
|
26
|
+
"@webpieces/rules-config": "0.4.860",
|
|
27
27
|
"madge": "8.0.0",
|
|
28
|
-
"@webpieces/rules-sdk": "0.4.
|
|
28
|
+
"@webpieces/rules-sdk": "0.4.860"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@nx/devkit": ">=18.0.0"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@webpieces/tooling-testkit": "0.4.
|
|
44
|
+
"@webpieces/tooling-testkit": "0.4.860"
|
|
45
45
|
},
|
|
46
46
|
"types": "./src/index.d.ts",
|
|
47
47
|
"bin": {
|
|
@@ -117,7 +117,9 @@ class WpFilterPage {
|
|
|
117
117
|
// webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value
|
|
118
118
|
.catch((err) => this.error(err instanceof Error ? err : new Error(String(err))));
|
|
119
119
|
}
|
|
120
|
-
captureBinding(_svg) {
|
|
120
|
+
captureBinding(_svg) {
|
|
121
|
+
return () => { };
|
|
122
|
+
}
|
|
121
123
|
wireControls() { }
|
|
122
124
|
prepareSvg(_svg) { }
|
|
123
125
|
filterItem(name) {
|
|
@@ -135,6 +137,7 @@ class WpFilterPage {
|
|
|
135
137
|
const previousAnchor = this.anchor;
|
|
136
138
|
const previousRetained = this.retained;
|
|
137
139
|
const previousSvg = host.querySelector('svg');
|
|
140
|
+
const focusName = WpNodeMenu.focusedName(previousSvg);
|
|
138
141
|
const rollback = this.captureBinding(previousSvg);
|
|
139
142
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure
|
|
140
143
|
try {
|
|
@@ -149,6 +152,7 @@ class WpFilterPage {
|
|
|
149
152
|
host.replaceChildren(svg);
|
|
150
153
|
this.wireSvg(svg);
|
|
151
154
|
this.indicator();
|
|
155
|
+
WpNodeMenu.restoreFocus(svg, focusName);
|
|
152
156
|
this.error(null);
|
|
153
157
|
// webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary
|
|
154
158
|
}
|
|
@@ -161,6 +165,8 @@ class WpFilterPage {
|
|
|
161
165
|
else
|
|
162
166
|
host.replaceChildren();
|
|
163
167
|
rollback();
|
|
168
|
+
if (previousSvg !== null)
|
|
169
|
+
WpNodeMenu.restoreFocus(previousSvg, focusName);
|
|
164
170
|
this.error(err instanceof Error ? err : new Error(String(err)));
|
|
165
171
|
}
|
|
166
172
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-filter.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-filter.client.ts"],"names":[],"mappings":"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","sourcesContent":["/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */\nclass WpGraphChain {\n readonly inNodes = new Map<string, Set<string>>();\n readonly outNodes = new Map<string, Set<string>>();\n\n constructor(private readonly model: RenderModelJson) {\n const ids = new Set(model.nodes.map((node) => node.id));\n for (const edge of model.edges) {\n if (!ids.has(edge.from) || !ids.has(edge.to)) continue;\n this.add(this.outNodes, edge.from, edge.to);\n this.add(this.inNodes, edge.to, edge.from);\n }\n }\n\n private add(map: Map<string, Set<string>>, from: string, to: string): void {\n if (!map.has(from)) map.set(from, new Set());\n map.get(from)?.add(to);\n }\n\n nodes(anchor: string): Set<string> {\n const retained = new Set<string>();\n if (!this.model.nodes.some((node) => node.id === anchor)) return retained;\n for (const direction of [this.inNodes, this.outNodes]) {\n const visited = new Set([anchor]);\n const stack = [anchor];\n while (stack.length > 0) {\n const id = stack.pop() as string;\n retained.add(id);\n for (const next of direction.get(id) ?? []) {\n if (visited.has(next)) continue;\n visited.add(next);\n stack.push(next);\n }\n }\n }\n return retained;\n }\n}\n\nclass WpRenderBand {\n constructor(\n readonly level: number,\n readonly nodeNames: string[],\n ) {}\n}\n\n/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */\nclass WpFilteredDot {\n constructor(private readonly model: RenderModelJson) {}\n\n render(retained: Set<string>): string {\n const nodes = this.model.nodes.filter((node) => retained.has(node.id));\n const edges = this.model.edges.filter(\n (edge) => retained.has(edge.from) && retained.has(edge.to),\n );\n const bands = this.model.bands\n .map(\n (band) =>\n new WpRenderBand(\n band.level,\n band.nodeNames.filter((id) => retained.has(id)),\n ),\n )\n .filter((band) => band.nodeNames.length > 0);\n return (\n this.model.header +\n nodes.map((node) => node.dot).join('') +\n this.bandDot(bands) +\n edges.map((edge) => edge.dot).join('') +\n this.model.footer\n );\n }\n\n private bandDot(bands: WpRenderBand[]): string {\n const nodeAttrs =\n '[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"wp-layout\"]';\n const edgeAttrs = '[style=invis, class=\"wp-layout\"]';\n let dot = '';\n for (const band of bands) {\n const anchor = `__wp_layout_L${band.level}`;\n dot += ` \"${anchor}\" ${nodeAttrs};\\n { rank=same; \"${anchor}\"; `;\n dot += band.nodeNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n const upper = bands[i];\n const lower = bands[i + 1];\n const from = `__wp_layout_L${upper.level}`;\n const to = `__wp_layout_L${lower.level}`;\n if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {\n dot += ` \"${from}\" -> \"${to}\" ${edgeAttrs};\\n`;\n continue;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n dot += ` \"${spacer}\" ${nodeAttrs};\\n { rank=same; \"${spacer}\"; }\\n`;\n dot += ` \"${from}\" -> \"${spacer}\" ${edgeAttrs};\\n \"${spacer}\" -> \"${to}\" ${edgeAttrs};\\n`;\n }\n return dot;\n }\n}\n\n/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */\nabstract class WpFilterPage {\n protected readonly chain: WpGraphChain;\n protected locked: string | null = null;\n protected anchor: string | null = null;\n protected retained: Set<string>;\n private viz: VizInstance | null = null;\n\n constructor(protected readonly model: RenderModelJson) {\n this.chain = new WpGraphChain(model);\n this.retained = new Set(model.nodes.map((node) => node.id));\n }\n\n render(): void {\n document\n .getElementById('wp-filter-off')\n ?.addEventListener('click', () => this.filter(null));\n this.wireControls();\n Viz.instance()\n .then((viz) => {\n this.viz = viz;\n this.filter(null);\n })\n // webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value\n .catch((err: unknown): void =>\n this.error(err instanceof Error ? err : new Error(String(err))),\n );\n }\n\n protected captureBinding(_svg: SVGSVGElement | null): () => void { return (): void => {}; }\n protected wireControls(): void {}\n protected abstract wireSvg(svg: SVGSVGElement): void;\n protected prepareSvg(_svg: SVGSVGElement): void {}\n\n filterItem(name: string): WpNodeMenuItem {\n return new WpNodeMenuItem(\n this.anchor === null ? 'Filter Unconnected' : 'Turn off Filter',\n () => this.filter(this.anchor === null ? name : null),\n );\n }\n\n filter(anchor: string | null): void {\n if (this.viz === null) return;\n const host = document.getElementById('graph');\n if (host === null) return;\n const retained =\n anchor === null\n ? new Set(this.model.nodes.map((node) => node.id))\n : this.chain.nodes(anchor);\n const previousAnchor = this.anchor;\n const previousRetained = this.retained;\n const previousSvg = host.querySelector('svg');\n const rollback = this.captureBinding(previousSvg);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure\n try {\n const dot =\n anchor === null\n ? this.model.fullDot\n : new WpFilteredDot(this.model).render(retained);\n const svg = this.viz.renderSVGElement(dot);\n this.prepareSvg(svg);\n WpNodeMenu.close();\n this.anchor = anchor;\n this.retained = retained;\n host.replaceChildren(svg);\n this.wireSvg(svg);\n this.indicator();\n this.error(null);\n // webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary\n } catch (err: unknown) {\n //const error = toError(err);\n this.anchor = previousAnchor;\n this.retained = previousRetained;\n if (previousSvg !== null) host.replaceChildren(previousSvg);\n else host.replaceChildren();\n rollback();\n this.error(err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n private indicator(): void {\n const status = document.getElementById('wp-filter-status');\n const label = document.getElementById('wp-filter-anchor');\n if (status !== null) status.hidden = this.anchor === null;\n if (label !== null) label.textContent = this.anchor;\n if (this.anchor !== null) document.getElementById('wp-filter-off')?.focus();\n else document.querySelector<SVGGElement>('#graph g.wp-node-clickable')?.focus();\n }\n\n private error(err: Error | null): void {\n const el = document.getElementById('wp-graph-error');\n if (el === null) return;\n el.hidden = err === null;\n el.textContent =\n err === null\n ? ''\n : `Graph rendering failed: ${err.message}. Try Turn off Filter or reload the page.`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-filter.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-filter.client.ts"],"names":[],"mappings":"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","sourcesContent":["/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */\nclass WpGraphChain {\n readonly inNodes = new Map<string, Set<string>>();\n readonly outNodes = new Map<string, Set<string>>();\n\n constructor(private readonly model: RenderModelJson) {\n const ids = new Set(model.nodes.map((node) => node.id));\n for (const edge of model.edges) {\n if (!ids.has(edge.from) || !ids.has(edge.to)) continue;\n this.add(this.outNodes, edge.from, edge.to);\n this.add(this.inNodes, edge.to, edge.from);\n }\n }\n\n private add(map: Map<string, Set<string>>, from: string, to: string): void {\n if (!map.has(from)) map.set(from, new Set());\n map.get(from)?.add(to);\n }\n\n nodes(anchor: string): Set<string> {\n const retained = new Set<string>();\n if (!this.model.nodes.some((node) => node.id === anchor)) return retained;\n for (const direction of [this.inNodes, this.outNodes]) {\n const visited = new Set([anchor]);\n const stack = [anchor];\n while (stack.length > 0) {\n const id = stack.pop() as string;\n retained.add(id);\n for (const next of direction.get(id) ?? []) {\n if (visited.has(next)) continue;\n visited.add(next);\n stack.push(next);\n }\n }\n }\n return retained;\n }\n}\n\nclass WpRenderBand {\n constructor(\n readonly level: number,\n readonly nodeNames: string[],\n ) {}\n}\n\n/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */\nclass WpFilteredDot {\n constructor(private readonly model: RenderModelJson) {}\n\n render(retained: Set<string>): string {\n const nodes = this.model.nodes.filter((node) => retained.has(node.id));\n const edges = this.model.edges.filter(\n (edge) => retained.has(edge.from) && retained.has(edge.to),\n );\n const bands = this.model.bands\n .map(\n (band) =>\n new WpRenderBand(\n band.level,\n band.nodeNames.filter((id) => retained.has(id)),\n ),\n )\n .filter((band) => band.nodeNames.length > 0);\n return (\n this.model.header +\n nodes.map((node) => node.dot).join('') +\n this.bandDot(bands) +\n edges.map((edge) => edge.dot).join('') +\n this.model.footer\n );\n }\n\n private bandDot(bands: WpRenderBand[]): string {\n const nodeAttrs =\n '[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"wp-layout\"]';\n const edgeAttrs = '[style=invis, class=\"wp-layout\"]';\n let dot = '';\n for (const band of bands) {\n const anchor = `__wp_layout_L${band.level}`;\n dot += ` \"${anchor}\" ${nodeAttrs};\\n { rank=same; \"${anchor}\"; `;\n dot += band.nodeNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n const upper = bands[i];\n const lower = bands[i + 1];\n const from = `__wp_layout_L${upper.level}`;\n const to = `__wp_layout_L${lower.level}`;\n if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {\n dot += ` \"${from}\" -> \"${to}\" ${edgeAttrs};\\n`;\n continue;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n dot += ` \"${spacer}\" ${nodeAttrs};\\n { rank=same; \"${spacer}\"; }\\n`;\n dot += ` \"${from}\" -> \"${spacer}\" ${edgeAttrs};\\n \"${spacer}\" -> \"${to}\" ${edgeAttrs};\\n`;\n }\n return dot;\n }\n}\n\n/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */\nabstract class WpFilterPage {\n protected readonly chain: WpGraphChain;\n protected locked: string | null = null;\n protected anchor: string | null = null;\n protected retained: Set<string>;\n private viz: VizInstance | null = null;\n\n constructor(protected readonly model: RenderModelJson) {\n this.chain = new WpGraphChain(model);\n this.retained = new Set(model.nodes.map((node) => node.id));\n }\n\n render(): void {\n document\n .getElementById('wp-filter-off')\n ?.addEventListener('click', () => this.filter(null));\n this.wireControls();\n Viz.instance()\n .then((viz) => {\n this.viz = viz;\n this.filter(null);\n })\n // webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value\n .catch((err: unknown): void =>\n this.error(err instanceof Error ? err : new Error(String(err))),\n );\n }\n\n protected captureBinding(_svg: SVGSVGElement | null): () => void {\n return (): void => {};\n }\n protected wireControls(): void {}\n protected abstract wireSvg(svg: SVGSVGElement): void;\n protected prepareSvg(_svg: SVGSVGElement): void {}\n\n filterItem(name: string): WpNodeMenuItem {\n return new WpNodeMenuItem(\n this.anchor === null ? 'Filter Unconnected' : 'Turn off Filter',\n () => this.filter(this.anchor === null ? name : null),\n );\n }\n\n filter(anchor: string | null): void {\n if (this.viz === null) return;\n const host = document.getElementById('graph');\n if (host === null) return;\n const retained =\n anchor === null\n ? new Set(this.model.nodes.map((node) => node.id))\n : this.chain.nodes(anchor);\n const previousAnchor = this.anchor;\n const previousRetained = this.retained;\n const previousSvg = host.querySelector('svg');\n const focusName = WpNodeMenu.focusedName(previousSvg);\n const rollback = this.captureBinding(previousSvg);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure\n try {\n const dot =\n anchor === null\n ? this.model.fullDot\n : new WpFilteredDot(this.model).render(retained);\n const svg = this.viz.renderSVGElement(dot);\n this.prepareSvg(svg);\n WpNodeMenu.close();\n this.anchor = anchor;\n this.retained = retained;\n host.replaceChildren(svg);\n this.wireSvg(svg);\n this.indicator();\n WpNodeMenu.restoreFocus(svg, focusName);\n this.error(null);\n // webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary\n } catch (err: unknown) {\n //const error = toError(err);\n this.anchor = previousAnchor;\n this.retained = previousRetained;\n if (previousSvg !== null) host.replaceChildren(previousSvg);\n else host.replaceChildren();\n rollback();\n if (previousSvg !== null) WpNodeMenu.restoreFocus(previousSvg, focusName);\n this.error(err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n private indicator(): void {\n const status = document.getElementById('wp-filter-status');\n const label = document.getElementById('wp-filter-anchor');\n if (status !== null) status.hidden = this.anchor === null;\n if (label !== null) label.textContent = this.anchor;\n if (this.anchor !== null) document.getElementById('wp-filter-off')?.focus();\n else document.querySelector<SVGGElement>('#graph g.wp-node-clickable')?.focus();\n }\n\n private error(err: Error | null): void {\n const el = document.getElementById('wp-graph-error');\n if (el === null) return;\n el.hidden = err === null;\n el.textContent =\n err === null\n ? ''\n : `Graph rendering failed: ${err.message}. Try Turn off Filter or reload the page.`;\n }\n}\n"]}
|
|
@@ -45,6 +45,9 @@ export declare class GraphNodeMenu {
|
|
|
45
45
|
* menu. Opening a menu closes any other, so at most one is ever on screen.
|
|
46
46
|
*/
|
|
47
47
|
private menuScript;
|
|
48
|
+
private positionScript;
|
|
49
|
+
/** Explicit input modality avoids Chromium's pointer-focus :focus-visible heuristics. */
|
|
50
|
+
private focusScript;
|
|
48
51
|
private keyboardScript;
|
|
49
52
|
/**
|
|
50
53
|
* The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities
|
|
@@ -65,7 +65,12 @@ class GraphNodeMenu {
|
|
|
65
65
|
cursor: pointer;
|
|
66
66
|
}
|
|
67
67
|
.wp-node-menu-item:hover { background: #E3F2FD; }
|
|
68
|
-
g.wp-node-clickable { cursor: pointer; }
|
|
68
|
+
g.wp-node-clickable { cursor: pointer; outline: none; }
|
|
69
|
+
/* DOM keyboard focus is independent of graph highlight/Lock and stays dim with its node. */
|
|
70
|
+
g.wp-node-clickable.wp-keyboard-focus {
|
|
71
|
+
outline: 2px dashed #7b1fa2;
|
|
72
|
+
outline-offset: 2px;
|
|
73
|
+
}
|
|
69
74
|
/* Every shape Graphviz (or the runtime page's own queue-cylinder redraw) can emit for a node
|
|
70
75
|
* body: box/record -> polygon, circle -> ellipse, cylinder and the redrawn queue -> path. A
|
|
71
76
|
* shape left out of this list would be a box that is clickable but never looks it. */
|
|
@@ -111,12 +116,11 @@ class GraphNodeMenu {
|
|
|
111
116
|
constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }
|
|
112
117
|
}
|
|
113
118
|
class WpNodeMenu {
|
|
114
|
-
|
|
115
|
-
const open = document.getElementById('wp-node-menu');
|
|
116
|
-
if (open !== null) open.remove();
|
|
117
|
-
}
|
|
119
|
+
${this.focusScript()}
|
|
118
120
|
static open(nodeEl, name, items) {
|
|
119
121
|
WpNodeMenu.close();
|
|
122
|
+
WpNodeMenu.owner = nodeEl;
|
|
123
|
+
WpNodeMenu.ownerKeyboard = WpNodeMenu.keyboard;
|
|
120
124
|
const menu = document.createElement('div');
|
|
121
125
|
menu.id = 'wp-node-menu';
|
|
122
126
|
menu.className = 'wp-node-menu';
|
|
@@ -128,6 +132,7 @@ class GraphNodeMenu {
|
|
|
128
132
|
for (const item of items) menu.appendChild(WpNodeMenu.button(item));
|
|
129
133
|
document.body.appendChild(menu);
|
|
130
134
|
WpNodeMenu.place(menu, nodeEl);
|
|
135
|
+
menu.querySelector('button')?.focus({ preventScroll: true });
|
|
131
136
|
}
|
|
132
137
|
static button(item) {
|
|
133
138
|
const button = document.createElement('button');
|
|
@@ -137,20 +142,12 @@ class GraphNodeMenu {
|
|
|
137
142
|
button.addEventListener('click', function (ev) {
|
|
138
143
|
ev.preventDefault();
|
|
139
144
|
ev.stopPropagation();
|
|
140
|
-
WpNodeMenu.close();
|
|
145
|
+
WpNodeMenu.close(true);
|
|
141
146
|
item.onSelect();
|
|
142
147
|
});
|
|
143
148
|
return button;
|
|
144
149
|
}
|
|
145
|
-
|
|
146
|
-
const box = nodeEl.getBoundingClientRect();
|
|
147
|
-
const own = menu.getBoundingClientRect();
|
|
148
|
-
const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;
|
|
149
|
-
let left = box.left + window.scrollX;
|
|
150
|
-
if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);
|
|
151
|
-
menu.style.left = left + 'px';
|
|
152
|
-
menu.style.top = (box.bottom + window.scrollY + 4) + 'px';
|
|
153
|
-
}
|
|
150
|
+
${this.positionScript()}
|
|
154
151
|
${this.keyboardScript()}
|
|
155
152
|
static wire(svg, itemsFor) {
|
|
156
153
|
svg.querySelectorAll('g.node').forEach(function (node) {
|
|
@@ -159,6 +156,14 @@ class GraphNodeMenu {
|
|
|
159
156
|
? '' : title.textContent.trim();
|
|
160
157
|
if (name === '') return;
|
|
161
158
|
if (node.classList.contains('wp-layout')) return;
|
|
159
|
+
if (WpNodeMenu.wired.has(node)) return;
|
|
160
|
+
WpNodeMenu.wired.add(node);
|
|
161
|
+
node.addEventListener('focus', function () {
|
|
162
|
+
node.classList.toggle('wp-keyboard-focus', WpNodeMenu.keyboard);
|
|
163
|
+
});
|
|
164
|
+
node.addEventListener('blur', function () {
|
|
165
|
+
node.classList.remove('wp-keyboard-focus');
|
|
166
|
+
});
|
|
162
167
|
node.classList.add('wp-node-clickable');
|
|
163
168
|
node.setAttribute('tabindex', '0');
|
|
164
169
|
node.setAttribute('role', 'button');
|
|
@@ -172,14 +177,61 @@ class GraphNodeMenu {
|
|
|
172
177
|
}
|
|
173
178
|
}`;
|
|
174
179
|
}
|
|
180
|
+
positionScript() {
|
|
181
|
+
return ` static place(menu, nodeEl) {
|
|
182
|
+
const box = nodeEl.getBoundingClientRect();
|
|
183
|
+
const own = menu.getBoundingClientRect();
|
|
184
|
+
const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;
|
|
185
|
+
let left = box.left + window.scrollX;
|
|
186
|
+
if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);
|
|
187
|
+
menu.style.left = left + 'px';
|
|
188
|
+
menu.style.top = (box.bottom + window.scrollY + 4) + 'px';
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
/** Explicit input modality avoids Chromium's pointer-focus :focus-visible heuristics. */
|
|
193
|
+
focusScript() {
|
|
194
|
+
return ` static keyboard = false;
|
|
195
|
+
static owner = null;
|
|
196
|
+
static ownerKeyboard = false;
|
|
197
|
+
static wired = new WeakSet();
|
|
198
|
+
static close(returnFocus = false) {
|
|
199
|
+
const open = document.getElementById('wp-node-menu');
|
|
200
|
+
const owner = WpNodeMenu.owner;
|
|
201
|
+
const keyboard = WpNodeMenu.ownerKeyboard;
|
|
202
|
+
WpNodeMenu.owner = null;
|
|
203
|
+
if (open === null) return;
|
|
204
|
+
open.remove();
|
|
205
|
+
if (returnFocus && keyboard && owner?.isConnected) {
|
|
206
|
+
WpNodeMenu.keyboard = true;
|
|
207
|
+
owner.focus({ preventScroll: true });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
static focusedName(svg) {
|
|
211
|
+
const active = document.activeElement;
|
|
212
|
+
const menu = document.getElementById('wp-node-menu');
|
|
213
|
+
const node = menu?.contains(active) && WpNodeMenu.ownerKeyboard
|
|
214
|
+
? WpNodeMenu.owner : active;
|
|
215
|
+
return svg?.contains(node) && (node === WpNodeMenu.owner || node?.classList.contains('wp-keyboard-focus'))
|
|
216
|
+
? node.querySelector('title')?.textContent.trim() : null;
|
|
217
|
+
}
|
|
218
|
+
static restoreFocus(svg, name) {
|
|
219
|
+
if (name == null) return;
|
|
220
|
+
const nodes = Array.from(svg.querySelectorAll('g.wp-node-clickable'));
|
|
221
|
+
const node = nodes.find(node => node.querySelector('title')?.textContent.trim() === name);
|
|
222
|
+
WpNodeMenu.keyboard = true;
|
|
223
|
+
(node || nodes[0])?.focus({ preventScroll: true });
|
|
224
|
+
}
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
175
227
|
keyboardScript() {
|
|
176
228
|
return `static wireKeyboard(node, name, itemsFor) {
|
|
177
229
|
node.addEventListener('keydown', function (ev) {
|
|
178
230
|
if (ev.key !== 'Enter' && ev.key !== ' ') return;
|
|
179
231
|
ev.preventDefault();
|
|
180
232
|
ev.stopPropagation();
|
|
233
|
+
WpNodeMenu.keyboard = true;
|
|
181
234
|
WpNodeMenu.open(node, name, itemsFor(name, node));
|
|
182
|
-
document.querySelector('#wp-node-menu button')?.focus();
|
|
183
235
|
});
|
|
184
236
|
}`;
|
|
185
237
|
}
|
|
@@ -227,10 +279,20 @@ class GraphNodeMenu {
|
|
|
227
279
|
/** Outside click and Escape both dismiss — wired once, on the document. */
|
|
228
280
|
dismissScript() {
|
|
229
281
|
return `
|
|
282
|
+
document.addEventListener('pointerdown', function () {
|
|
283
|
+
WpNodeMenu.keyboard = false;
|
|
284
|
+
document.querySelectorAll('.wp-keyboard-focus').forEach(function (node) {
|
|
285
|
+
node.classList.remove('wp-keyboard-focus');
|
|
286
|
+
});
|
|
287
|
+
}, true);
|
|
230
288
|
document.addEventListener('click', function () { WpNodeMenu.close(); });
|
|
231
289
|
document.addEventListener('keydown', function (ev) {
|
|
232
|
-
if (ev.
|
|
233
|
-
|
|
290
|
+
if (ev.altKey || ev.ctrlKey || ev.metaKey) return;
|
|
291
|
+
WpNodeMenu.keyboard = true;
|
|
292
|
+
const active = document.activeElement;
|
|
293
|
+
if (active?.classList.contains('wp-node-clickable')) active.classList.add('wp-keyboard-focus');
|
|
294
|
+
if (ev.key === 'Escape') WpNodeMenu.close(true);
|
|
295
|
+
}, true);`;
|
|
234
296
|
}
|
|
235
297
|
}
|
|
236
298
|
exports.GraphNodeMenu = GraphNodeMenu;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-node-menu.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-node-menu.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,aAAa;IACtB;;;;OAIG;IACH,MAAM;QACF,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8CL,CAAC;IACP,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,KAAa;QACnB,OAAO;UACL,KAAK,WAAW,KAAK;UACrB,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK,yCAAyC,CAAC;IACrD,CAAC;IAED,+FAA+F;IAC/F,MAAM;QACF,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;IACjF,CAAC;IAED;;;;OAIG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA6CD,IAAI,CAAC,cAAc,EAAE;;;;;;;;;;;;;;;;;;;UAmBzB,CAAC;IACP,CAAC;IAEO,cAAc;QAClB,OAAO;;;;;;;;UAQL,CAAC;IACP,CAAC;IAED;;;OAGG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAkCL,CAAC;IACP,CAAC;IAED,2EAA2E;IACnE,aAAa;QACjB,OAAO;;;;YAIH,CAAC;IACT,CAAC;CACJ;AArND,sCAqNC","sourcesContent":["/**\n * The floating per-node menu, shared by ALL THREE generated graphs.\n *\n * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the\n * click wiring that opens it — and `architecture/dependencies.html` (graph-visualizer.ts), every\n * project's `design.html` (di-graph/design-visualizer.ts) and `tmp/webpieces/runtime-architecture.html`\n * (runtime-visualizer.ts) all inline these same bytes. Duplicating it into the three emitters is what\n * this module exists to prevent.\n *\n * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`\n * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the\n * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a\n * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one\n * copy reachable by every emitter with no build-order coupling. The two pages that DO have a compiled\n * client (`graph-visualizer.client.ts`, `runtime-visualizer.client.ts`) call into these classes as\n * ambient globals — see viz-client-globals.d.ts.\n *\n * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:\n * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page and the runtime page have neither dropdown nor responsibilities, so they use\n * `WpNodeLock` below — dim every other box in that graph, light the locked one.\n * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.\n */\nexport class GraphNodeMenu {\n /**\n * Menu chrome + the \"this box is clickable\" affordance. Every node is clickable now (the menu\n * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with\n * a design page.\n */\n styles(): string {\n return `\n .wp-node-menu {\n position: absolute;\n z-index: 1000;\n min-width: 150px;\n padding: 4px 0;\n background: white;\n border: 1px solid #cfcfcf;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0,0,0,0.22);\n font-family: Arial, sans-serif;\n font-size: 14px;\n }\n .wp-node-menu-title {\n padding: 4px 14px 6px;\n color: #777;\n font-size: 12px;\n font-family: monospace;\n border-bottom: 1px solid #eee;\n margin-bottom: 4px;\n }\n .wp-node-menu-item {\n display: block;\n width: 100%;\n padding: 7px 14px;\n border: 0;\n background: none;\n color: #1565C0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n }\n .wp-node-menu-item:hover { background: #E3F2FD; }\n g.wp-node-clickable { cursor: pointer; }\n /* Every shape Graphviz (or the runtime page's own queue-cylinder redraw) can emit for a node\n * body: box/record -> polygon, circle -> ellipse, cylinder and the redrawn queue -> path. A\n * shape left out of this list would be a box that is clickable but never looks it. */\n g.wp-node-clickable polygon,\n g.wp-node-clickable ellipse,\n g.wp-node-clickable path { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n g.wp-node-clickable:hover polygon,\n g.wp-node-clickable:hover ellipse,\n g.wp-node-clickable:hover path {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }`;\n }\n\n /**\n * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered\n * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).\n *\n * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat\n * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.\n */\n dimStyles(scope: string): string {\n return `\n ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }\n ${scope} svg.wp-dim .node,\n ${scope} svg.wp-dim .edge { opacity: 0.15; }\n ${scope} svg.wp-dim .node.wp-focus,\n ${scope} svg.wp-dim .node.wp-neighbor,\n ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;\n }\n\n /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */\n script(): string {\n return `${this.menuScript()}\\n${this.lockScript()}\\n${this.dismissScript()}`;\n }\n\n /**\n * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,\n * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole\n * menu. Opening a menu closes any other, so at most one is ever on screen.\n */\n private menuScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }\n }\n class WpNodeMenu {\n static close() {\n const open = document.getElementById('wp-node-menu');\n if (open !== null) open.remove();\n }\n static open(nodeEl, name, items) {\n WpNodeMenu.close();\n const menu = document.createElement('div');\n menu.id = 'wp-node-menu';\n menu.className = 'wp-node-menu';\n menu.addEventListener('click', function (ev) { ev.stopPropagation(); });\n const heading = document.createElement('div');\n heading.className = 'wp-node-menu-title';\n heading.textContent = name;\n menu.appendChild(heading);\n for (const item of items) menu.appendChild(WpNodeMenu.button(item));\n document.body.appendChild(menu);\n WpNodeMenu.place(menu, nodeEl);\n }\n static button(item) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n button.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.close();\n item.onSelect();\n });\n return button;\n }\n static place(menu, nodeEl) {\n const box = nodeEl.getBoundingClientRect();\n const own = menu.getBoundingClientRect();\n const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;\n let left = box.left + window.scrollX;\n if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);\n menu.style.left = left + 'px';\n menu.style.top = (box.bottom + window.scrollY + 4) + 'px';\n }\n ${this.keyboardScript()}\n static wire(svg, itemsFor) {\n svg.querySelectorAll('g.node').forEach(function (node) {\n const title = node.querySelector('title');\n const name = title === null || title.textContent === null\n ? '' : title.textContent.trim();\n if (name === '') return;\n if (node.classList.contains('wp-layout')) return;\n node.classList.add('wp-node-clickable');\n node.setAttribute('tabindex', '0');\n node.setAttribute('role', 'button');\n WpNodeMenu.wireKeyboard(node, name, itemsFor);\n node.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n });\n }\n }`;\n }\n\n private keyboardScript(): string {\n return `static wireKeyboard(node, name, itemsFor) {\n node.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Enter' && ev.key !== ' ') return;\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n document.querySelector('#wp-node-menu button')?.focus();\n });\n }`;\n }\n\n /**\n * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities\n * list, so locking a box dims every other box in that graph and lights the locked one.\n */\n private lockScript(): string {\n return `\n class WpNodeLock {\n constructor(svg) { this.svg = svg; this.locked = null; }\n rebind(svg) {\n const name = this.locked;\n this.svg = svg;\n this.clear();\n this.locked = name;\n if (name === null) return;\n for (const node of svg.querySelectorAll('g.node')) {\n if (node.querySelector('title')?.textContent === name) {\n this.lock(name, node);\n break;\n }\n }\n }\n isLocked(name) { return this.locked === name; }\n toggle(name, nodeEl) {\n if (this.locked === name) this.clear();\n else this.lock(name, nodeEl);\n }\n lock(name, nodeEl) {\n this.clear();\n this.locked = name;\n this.svg.classList.add('wp-dim');\n nodeEl.classList.add('wp-focus');\n }\n clear() {\n this.locked = null;\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus').forEach(function (el) {\n el.classList.remove('wp-focus');\n });\n }\n }`;\n }\n\n /** Outside click and Escape both dismiss — wired once, on the document. */\n private dismissScript(): string {\n return `\n document.addEventListener('click', function () { WpNodeMenu.close(); });\n document.addEventListener('keydown', function (ev) {\n if (ev.key === 'Escape') WpNodeMenu.close();\n });`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-node-menu.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-node-menu.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,aAAa;IACtB;;;;OAIG;IACH,MAAM;QACF,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmDL,CAAC;IACP,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,KAAa;QACnB,OAAO;UACL,KAAK,WAAW,KAAK;UACrB,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK,yCAAyC,CAAC;IACrD,CAAC;IAED,+FAA+F;IAC/F,MAAM;QACF,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;IACjF,CAAC;IAED;;;;OAIG;IACK,UAAU;QACd,OAAO;;;;;cAKD,IAAI,CAAC,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+BlB,IAAI,CAAC,cAAc,EAAE;cACrB,IAAI,CAAC,cAAc,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;UA2BzB,CAAC;IACP,CAAC;IAEO,cAAc;QAClB,OAAO;;;;;;;;;CASd,CAAC;IACE,CAAC;IAED,yFAAyF;IACjF,WAAW;QACf,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+Bd,CAAC;IACE,CAAC;IAEO,cAAc;QAClB,OAAO;;;;;;;;UAQL,CAAC;IACP,CAAC;IAED;;;OAGG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAkCL,CAAC;IACP,CAAC;IAED,2EAA2E;IACnE,aAAa;QACjB,OAAO;;;;;;;;;;;;;;kBAcG,CAAC;IACf,CAAC;CACJ;AArRD,sCAqRC","sourcesContent":["/**\n * The floating per-node menu, shared by ALL THREE generated graphs.\n *\n * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the\n * click wiring that opens it — and `architecture/dependencies.html` (graph-visualizer.ts), every\n * project's `design.html` (di-graph/design-visualizer.ts) and `tmp/webpieces/runtime-architecture.html`\n * (runtime-visualizer.ts) all inline these same bytes. Duplicating it into the three emitters is what\n * this module exists to prevent.\n *\n * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`\n * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the\n * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a\n * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one\n * copy reachable by every emitter with no build-order coupling. The two pages that DO have a compiled\n * client (`graph-visualizer.client.ts`, `runtime-visualizer.client.ts`) call into these classes as\n * ambient globals — see viz-client-globals.d.ts.\n *\n * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:\n * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page and the runtime page have neither dropdown nor responsibilities, so they use\n * `WpNodeLock` below — dim every other box in that graph, light the locked one.\n * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.\n */\nexport class GraphNodeMenu {\n /**\n * Menu chrome + the \"this box is clickable\" affordance. Every node is clickable now (the menu\n * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with\n * a design page.\n */\n styles(): string {\n return `\n .wp-node-menu {\n position: absolute;\n z-index: 1000;\n min-width: 150px;\n padding: 4px 0;\n background: white;\n border: 1px solid #cfcfcf;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0,0,0,0.22);\n font-family: Arial, sans-serif;\n font-size: 14px;\n }\n .wp-node-menu-title {\n padding: 4px 14px 6px;\n color: #777;\n font-size: 12px;\n font-family: monospace;\n border-bottom: 1px solid #eee;\n margin-bottom: 4px;\n }\n .wp-node-menu-item {\n display: block;\n width: 100%;\n padding: 7px 14px;\n border: 0;\n background: none;\n color: #1565C0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n }\n .wp-node-menu-item:hover { background: #E3F2FD; }\n g.wp-node-clickable { cursor: pointer; outline: none; }\n /* DOM keyboard focus is independent of graph highlight/Lock and stays dim with its node. */\n g.wp-node-clickable.wp-keyboard-focus {\n outline: 2px dashed #7b1fa2;\n outline-offset: 2px;\n }\n /* Every shape Graphviz (or the runtime page's own queue-cylinder redraw) can emit for a node\n * body: box/record -> polygon, circle -> ellipse, cylinder and the redrawn queue -> path. A\n * shape left out of this list would be a box that is clickable but never looks it. */\n g.wp-node-clickable polygon,\n g.wp-node-clickable ellipse,\n g.wp-node-clickable path { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n g.wp-node-clickable:hover polygon,\n g.wp-node-clickable:hover ellipse,\n g.wp-node-clickable:hover path {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }`;\n }\n\n /**\n * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered\n * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).\n *\n * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat\n * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.\n */\n dimStyles(scope: string): string {\n return `\n ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }\n ${scope} svg.wp-dim .node,\n ${scope} svg.wp-dim .edge { opacity: 0.15; }\n ${scope} svg.wp-dim .node.wp-focus,\n ${scope} svg.wp-dim .node.wp-neighbor,\n ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;\n }\n\n /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */\n script(): string {\n return `${this.menuScript()}\\n${this.lockScript()}\\n${this.dismissScript()}`;\n }\n\n /**\n * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,\n * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole\n * menu. Opening a menu closes any other, so at most one is ever on screen.\n */\n private menuScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }\n }\n class WpNodeMenu {\n ${this.focusScript()}\n static open(nodeEl, name, items) {\n WpNodeMenu.close();\n WpNodeMenu.owner = nodeEl;\n WpNodeMenu.ownerKeyboard = WpNodeMenu.keyboard;\n const menu = document.createElement('div');\n menu.id = 'wp-node-menu';\n menu.className = 'wp-node-menu';\n menu.addEventListener('click', function (ev) { ev.stopPropagation(); });\n const heading = document.createElement('div');\n heading.className = 'wp-node-menu-title';\n heading.textContent = name;\n menu.appendChild(heading);\n for (const item of items) menu.appendChild(WpNodeMenu.button(item));\n document.body.appendChild(menu);\n WpNodeMenu.place(menu, nodeEl);\n menu.querySelector('button')?.focus({ preventScroll: true });\n }\n static button(item) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n button.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.close(true);\n item.onSelect();\n });\n return button;\n }\n ${this.positionScript()}\n ${this.keyboardScript()}\n static wire(svg, itemsFor) {\n svg.querySelectorAll('g.node').forEach(function (node) {\n const title = node.querySelector('title');\n const name = title === null || title.textContent === null\n ? '' : title.textContent.trim();\n if (name === '') return;\n if (node.classList.contains('wp-layout')) return;\n if (WpNodeMenu.wired.has(node)) return;\n WpNodeMenu.wired.add(node);\n node.addEventListener('focus', function () {\n node.classList.toggle('wp-keyboard-focus', WpNodeMenu.keyboard);\n });\n node.addEventListener('blur', function () {\n node.classList.remove('wp-keyboard-focus');\n });\n node.classList.add('wp-node-clickable');\n node.setAttribute('tabindex', '0');\n node.setAttribute('role', 'button');\n WpNodeMenu.wireKeyboard(node, name, itemsFor);\n node.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n });\n }\n }`;\n }\n\n private positionScript(): string {\n return ` static place(menu, nodeEl) {\n const box = nodeEl.getBoundingClientRect();\n const own = menu.getBoundingClientRect();\n const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;\n let left = box.left + window.scrollX;\n if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);\n menu.style.left = left + 'px';\n menu.style.top = (box.bottom + window.scrollY + 4) + 'px';\n }\n`;\n }\n\n /** Explicit input modality avoids Chromium's pointer-focus :focus-visible heuristics. */\n private focusScript(): string {\n return ` static keyboard = false;\n static owner = null;\n static ownerKeyboard = false;\n static wired = new WeakSet();\n static close(returnFocus = false) {\n const open = document.getElementById('wp-node-menu');\n const owner = WpNodeMenu.owner;\n const keyboard = WpNodeMenu.ownerKeyboard;\n WpNodeMenu.owner = null;\n if (open === null) return;\n open.remove();\n if (returnFocus && keyboard && owner?.isConnected) {\n WpNodeMenu.keyboard = true;\n owner.focus({ preventScroll: true });\n }\n }\n static focusedName(svg) {\n const active = document.activeElement;\n const menu = document.getElementById('wp-node-menu');\n const node = menu?.contains(active) && WpNodeMenu.ownerKeyboard\n ? WpNodeMenu.owner : active;\n return svg?.contains(node) && (node === WpNodeMenu.owner || node?.classList.contains('wp-keyboard-focus'))\n ? node.querySelector('title')?.textContent.trim() : null;\n }\n static restoreFocus(svg, name) {\n if (name == null) return;\n const nodes = Array.from(svg.querySelectorAll('g.wp-node-clickable'));\n const node = nodes.find(node => node.querySelector('title')?.textContent.trim() === name);\n WpNodeMenu.keyboard = true;\n (node || nodes[0])?.focus({ preventScroll: true });\n }\n`;\n }\n\n private keyboardScript(): string {\n return `static wireKeyboard(node, name, itemsFor) {\n node.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Enter' && ev.key !== ' ') return;\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.keyboard = true;\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n }`;\n }\n\n /**\n * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities\n * list, so locking a box dims every other box in that graph and lights the locked one.\n */\n private lockScript(): string {\n return `\n class WpNodeLock {\n constructor(svg) { this.svg = svg; this.locked = null; }\n rebind(svg) {\n const name = this.locked;\n this.svg = svg;\n this.clear();\n this.locked = name;\n if (name === null) return;\n for (const node of svg.querySelectorAll('g.node')) {\n if (node.querySelector('title')?.textContent === name) {\n this.lock(name, node);\n break;\n }\n }\n }\n isLocked(name) { return this.locked === name; }\n toggle(name, nodeEl) {\n if (this.locked === name) this.clear();\n else this.lock(name, nodeEl);\n }\n lock(name, nodeEl) {\n this.clear();\n this.locked = name;\n this.svg.classList.add('wp-dim');\n nodeEl.classList.add('wp-focus');\n }\n clear() {\n this.locked = null;\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus').forEach(function (el) {\n el.classList.remove('wp-focus');\n });\n }\n }`;\n }\n\n /** Outside click and Escape both dismiss — wired once, on the document. */\n private dismissScript(): string {\n return `\n document.addEventListener('pointerdown', function () {\n WpNodeMenu.keyboard = false;\n document.querySelectorAll('.wp-keyboard-focus').forEach(function (node) {\n node.classList.remove('wp-keyboard-focus');\n });\n }, true);\n document.addEventListener('click', function () { WpNodeMenu.close(); });\n document.addEventListener('keydown', function (ev) {\n if (ev.altKey || ev.ctrlKey || ev.metaKey) return;\n WpNodeMenu.keyboard = true;\n const active = document.activeElement;\n if (active?.classList.contains('wp-node-clickable')) active.classList.add('wp-keyboard-focus');\n if (ev.key === 'Escape') WpNodeMenu.close(true);\n }, true);`;\n }\n}\n"]}
|