lispgram 0.10.0 → 0.10.3
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 +11 -5
- package/dist/index.js +331 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,10 +118,11 @@ npm install lispgram
|
|
|
118
118
|
<script type="module">
|
|
119
119
|
import lispgram from "lispgram";
|
|
120
120
|
lispgram.initialize({
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
startOnLoad: true,
|
|
122
|
+
showArrowGlyphs: true,
|
|
123
|
+
clickToFocus: true,
|
|
124
|
+
zoom: true
|
|
125
|
+
});
|
|
125
126
|
</script>
|
|
126
127
|
```
|
|
127
128
|
|
|
@@ -226,6 +227,8 @@ import { render, renderElement, initialize, createDiagramSvg } from "lispgram/la
|
|
|
226
227
|
|
|
227
228
|
`render(target, source, options?)` renders Lispgram or NCF into a DOM target. `renderElement(sourceElement, options?)` renders one source element. `initialize(options?)` auto-renders matching elements such as `<pre class="lispgram">`. `createDiagramSvg(doc, options?)` renders an already-normalized NCF document into an SVG element.
|
|
228
229
|
|
|
230
|
+
`options.zoom` is a boolean that controls the built-in Shift navigation tools. The default is `true`, which enables Shift + A + click zoom-to-target, cursor-centered Shift + wheel zoom, and Shift + drag panning. Shift + A + click fits containers to about 80% of the viewport. Ordinary nodes zoom until the node height is about 8% of the view height. Arrows zoom to their arrow glyph/label area at about 2% of the view height. Use `zoom: false`, or `data-lispgram-zoom="false"` on auto-rendered `<pre>` blocks, to disable all built-in zoom and panning behavior.
|
|
231
|
+
|
|
229
232
|
### Interaction entry point
|
|
230
233
|
|
|
231
234
|
```js
|
|
@@ -286,6 +289,8 @@ When `clickToFocus` is enabled, clicking a node, compound/container, or arrow fo
|
|
|
286
289
|
|
|
287
290
|
Click hit-testing uses a transparent interaction layer above the rendered diagram. Ordinary nodes are given priority over arrows, arrows over container interiors, and deeper containers over their ancestors. This keeps large ancestor containers clickable without letting them steal clicks from nested containers such as `BuildSystem`, `ExternalDependencies`, or `GuileBinding`.
|
|
288
291
|
|
|
292
|
+
The examples use `zoom: true`, which is also the default. It enables Shift + A + click zoom-to-target, Shift-wheel cursor zoom, and Shift-drag panning. Containers fit to roughly 80% of the viewport; ordinary nodes fit to about 8% of the view height; arrows fit to the arrow glyph/label area at about 2% of the view height. Pass `zoom: false` to disable built-in zoom and pan behavior.
|
|
293
|
+
|
|
289
294
|
## Demo
|
|
290
295
|
|
|
291
296
|
go to repo root directory
|
|
@@ -308,10 +313,11 @@ navigate to examples/basic.html in browser
|
|
|
308
313
|
</pre>
|
|
309
314
|
<script type="module">
|
|
310
315
|
import lispgram from "https://unpkg.com/lispgram@latest/dist/index.js";
|
|
311
|
-
|
|
316
|
+
lispgram.initialize({
|
|
312
317
|
startOnLoad: true,
|
|
313
318
|
showArrowGlyphs: true,
|
|
314
319
|
clickToFocus: true,
|
|
320
|
+
zoom: true,
|
|
315
321
|
arrowGlyphRadius: 5.75
|
|
316
322
|
});
|
|
317
323
|
</script>
|
package/dist/index.js
CHANGED
|
@@ -4772,7 +4772,7 @@ function renderLispgramRoute(svg, route, options) {
|
|
|
4772
4772
|
}
|
|
4773
4773
|
|
|
4774
4774
|
function renderLispgramHitLayer(svg, result, options = {}) {
|
|
4775
|
-
if (options.clickToFocus === false) return;
|
|
4775
|
+
if (options.clickToFocus === false && !zoomEnabled(options)) return;
|
|
4776
4776
|
const layer = svgEl('g', { class: 'lispgram-hit-layer', 'aria-hidden': 'true' });
|
|
4777
4777
|
|
|
4778
4778
|
// Container hit regions are separate from the visible paint order. They are
|
|
@@ -4888,6 +4888,330 @@ function routeTouchesFocus(route, focus) {
|
|
|
4888
4888
|
return endpointTouches(route.fromEndpoint, focus.id, focus.type) || endpointTouches(route.toEndpoint, focus.id, focus.type);
|
|
4889
4889
|
}
|
|
4890
4890
|
|
|
4891
|
+
function zoomEnabled(options = {}) {
|
|
4892
|
+
return options.zoom !== false;
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
const lispgramZoomKeyState = new WeakMap();
|
|
4896
|
+
|
|
4897
|
+
function normalizeKeyboardKey(event) {
|
|
4898
|
+
return String(event?.key || event?.code || '').toLowerCase();
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
function eventHasZoomClickKey(svg, event) {
|
|
4902
|
+
if (event?.aKey === true) return true;
|
|
4903
|
+
const key = normalizeKeyboardKey(event);
|
|
4904
|
+
if (key === 'a' || key === 'keya') return true;
|
|
4905
|
+
return !!lispgramZoomKeyState.get(svg)?.aDown;
|
|
4906
|
+
}
|
|
4907
|
+
|
|
4908
|
+
function isZoomClickEvent(svg, event) {
|
|
4909
|
+
return !!event?.shiftKey && eventHasZoomClickKey(svg, event);
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
function parseViewBox(svg) {
|
|
4913
|
+
const raw = svg?.getAttribute?.('viewBox') || '';
|
|
4914
|
+
const nums = raw.trim().split(/[\s,]+/).map(Number).filter(Number.isFinite);
|
|
4915
|
+
if (nums.length >= 4) return { x: nums[0], y: nums[1], w: nums[2], h: nums[3] };
|
|
4916
|
+
return null;
|
|
4917
|
+
}
|
|
4918
|
+
|
|
4919
|
+
function writeViewBox(svg, vb) {
|
|
4920
|
+
if (!svg || !vb) return;
|
|
4921
|
+
svg.setAttribute('viewBox', `${vb.x} ${vb.y} ${vb.w} ${vb.h}`);
|
|
4922
|
+
}
|
|
4923
|
+
|
|
4924
|
+
function svgDisplayAspect(svg, fallbackViewBox) {
|
|
4925
|
+
const attrW = Number(svg?.getAttribute?.('width'));
|
|
4926
|
+
const attrH = Number(svg?.getAttribute?.('height'));
|
|
4927
|
+
if (Number.isFinite(attrW) && attrW > 0 && Number.isFinite(attrH) && attrH > 0) return attrW / attrH;
|
|
4928
|
+
const rect = typeof svg?.getBoundingClientRect === 'function' ? svg.getBoundingClientRect() : null;
|
|
4929
|
+
if (rect?.width > 0 && rect?.height > 0) return rect.width / rect.height;
|
|
4930
|
+
return fallbackViewBox?.w && fallbackViewBox?.h ? fallbackViewBox.w / fallbackViewBox.h : 1;
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4933
|
+
function svgPointFromEvent(svg, event, fallbackViewBox) {
|
|
4934
|
+
const vb = parseViewBox(svg) || fallbackViewBox;
|
|
4935
|
+
if (!vb) return null;
|
|
4936
|
+
const rect = typeof svg?.getBoundingClientRect === 'function' ? svg.getBoundingClientRect() : null;
|
|
4937
|
+
if (rect?.width > 0 && rect?.height > 0 && Number.isFinite(event?.clientX) && Number.isFinite(event?.clientY)) {
|
|
4938
|
+
return {
|
|
4939
|
+
x: vb.x + ((event.clientX - rect.left) / rect.width) * vb.w,
|
|
4940
|
+
y: vb.y + ((event.clientY - rect.top) / rect.height) * vb.h
|
|
4941
|
+
};
|
|
4942
|
+
}
|
|
4943
|
+
return { x: vb.x + vb.w / 2, y: vb.y + vb.h / 2 };
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
function zoomViewBoxAt(svg, pivot, factor, originalViewBox, options = {}) {
|
|
4947
|
+
const vb = parseViewBox(svg) || originalViewBox;
|
|
4948
|
+
if (!vb || !pivot || !Number.isFinite(factor) || factor <= 0) return null;
|
|
4949
|
+
const minScale = Number.isFinite(Number(options.zoomMinScale)) ? Number(options.zoomMinScale) : 0.08;
|
|
4950
|
+
const maxScale = Number.isFinite(Number(options.zoomMaxScale)) ? Number(options.zoomMaxScale) : 6;
|
|
4951
|
+
const base = originalViewBox || vb;
|
|
4952
|
+
let nextW = vb.w * factor;
|
|
4953
|
+
let nextH = vb.h * factor;
|
|
4954
|
+
nextW = Math.max(base.w * minScale, Math.min(base.w * maxScale, nextW));
|
|
4955
|
+
nextH = Math.max(base.h * minScale, Math.min(base.h * maxScale, nextH));
|
|
4956
|
+
const actualX = nextW / vb.w;
|
|
4957
|
+
const actualY = nextH / vb.h;
|
|
4958
|
+
const next = {
|
|
4959
|
+
x: pivot.x - (pivot.x - vb.x) * actualX,
|
|
4960
|
+
y: pivot.y - (pivot.y - vb.y) * actualY,
|
|
4961
|
+
w: nextW,
|
|
4962
|
+
h: nextH
|
|
4963
|
+
};
|
|
4964
|
+
writeViewBox(svg, next);
|
|
4965
|
+
return next;
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
function viewBoxForBox(svg, box, originalViewBox, options = {}) {
|
|
4969
|
+
if (!box || !originalViewBox) return null;
|
|
4970
|
+
const ratioRaw = Number(options.zoomFitRatio ?? options.containerZoomFitRatio ?? 0.8);
|
|
4971
|
+
const fitRatio = Number.isFinite(ratioRaw) && ratioRaw > 0 && ratioRaw < 1 ? ratioRaw : 0.8;
|
|
4972
|
+
const aspect = svgDisplayAspect(svg, originalViewBox) || 1;
|
|
4973
|
+
const minW = Number(options.containerZoomMinWidth ?? 160);
|
|
4974
|
+
const minH = Number(options.containerZoomMinHeight ?? 120);
|
|
4975
|
+
let w = Math.max(box.w / fitRatio, (box.h / fitRatio) * aspect, minW);
|
|
4976
|
+
let h = w / aspect;
|
|
4977
|
+
if (h < Math.max(box.h / fitRatio, minH)) {
|
|
4978
|
+
h = Math.max(box.h / fitRatio, minH);
|
|
4979
|
+
w = h * aspect;
|
|
4980
|
+
}
|
|
4981
|
+
const maxW = originalViewBox.w * (Number(options.containerZoomMaxScale ?? 1.05) || 1.05);
|
|
4982
|
+
const maxH = originalViewBox.h * (Number(options.containerZoomMaxScale ?? 1.05) || 1.05);
|
|
4983
|
+
if (w > maxW || h > maxH) {
|
|
4984
|
+
const scale = Math.max(w / maxW, h / maxH);
|
|
4985
|
+
w /= scale;
|
|
4986
|
+
h /= scale;
|
|
4987
|
+
}
|
|
4988
|
+
return { x: box.cx - w / 2, y: box.cy - h / 2, w, h };
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4991
|
+
function finiteBounds(points) {
|
|
4992
|
+
const pts = (points || []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y));
|
|
4993
|
+
if (!pts.length) return null;
|
|
4994
|
+
let left = Infinity;
|
|
4995
|
+
let right = -Infinity;
|
|
4996
|
+
let top = Infinity;
|
|
4997
|
+
let bottom = -Infinity;
|
|
4998
|
+
for (const p of pts) {
|
|
4999
|
+
left = Math.min(left, p.x);
|
|
5000
|
+
right = Math.max(right, p.x);
|
|
5001
|
+
top = Math.min(top, p.y);
|
|
5002
|
+
bottom = Math.max(bottom, p.y);
|
|
5003
|
+
}
|
|
5004
|
+
return { left, right, top, bottom, w: right - left, h: bottom - top, cx: (left + right) / 2, cy: (top + bottom) / 2 };
|
|
5005
|
+
}
|
|
5006
|
+
|
|
5007
|
+
function routeZoomBounds(route, options = {}) {
|
|
5008
|
+
const mid = route?.anchors?.label || route?.anchors?.mid || pointAlongPolyline(route?.points || [], 0.5);
|
|
5009
|
+
const glyphRadius = Number.isFinite(Number(options.arrowGlyphRadius)) ? Number(options.arrowGlyphRadius) : 5;
|
|
5010
|
+
const glyphDiameter = Math.max(10, glyphRadius * 2);
|
|
5011
|
+
const routeBounds = finiteBounds(route?.points || []);
|
|
5012
|
+
if (!mid) return routeBounds;
|
|
5013
|
+
const desired = Math.max(glyphDiameter, Number(options.arrowZoomTargetSize || 0) || 0);
|
|
5014
|
+
const half = desired / 2;
|
|
5015
|
+
return {
|
|
5016
|
+
left: mid.x - half,
|
|
5017
|
+
right: mid.x + half,
|
|
5018
|
+
top: mid.y - half,
|
|
5019
|
+
bottom: mid.y + half,
|
|
5020
|
+
w: desired,
|
|
5021
|
+
h: desired,
|
|
5022
|
+
cx: mid.x,
|
|
5023
|
+
cy: mid.y,
|
|
5024
|
+
routeBounds
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
function viewBoxForSmallBounds(svg, bounds, originalViewBox, options = {}) {
|
|
5029
|
+
if (!bounds || !originalViewBox) return null;
|
|
5030
|
+
const aspect = svgDisplayAspect(svg, originalViewBox) || 1;
|
|
5031
|
+
const fractionRaw = Number(options.zoomTargetHeightFraction ?? options.smallZoomHeightFraction ?? 0.08);
|
|
5032
|
+
const fraction = Number.isFinite(fractionRaw) && fractionRaw > 0 && fractionRaw < 1 ? fractionRaw : 0.08;
|
|
5033
|
+
const minTargetH = Number.isFinite(Number(options.smallZoomMinTargetHeight)) ? Number(options.smallZoomMinTargetHeight) : 10;
|
|
5034
|
+
const targetH = Math.max(bounds.h || 0, minTargetH);
|
|
5035
|
+
let h = targetH / fraction;
|
|
5036
|
+
let w = h * aspect;
|
|
5037
|
+
const minW = Number(options.smallZoomMinWidth ?? 120);
|
|
5038
|
+
const minH = Number(options.smallZoomMinHeight ?? 90);
|
|
5039
|
+
if (w < minW) { w = minW; h = w / aspect; }
|
|
5040
|
+
if (h < minH) { h = minH; w = h * aspect; }
|
|
5041
|
+
return { x: bounds.cx - w / 2, y: bounds.cy - h / 2, w, h };
|
|
5042
|
+
}
|
|
5043
|
+
|
|
5044
|
+
function viewBoxForZoomTarget(svg, target, result, originalViewBox, options = {}) {
|
|
5045
|
+
if (!target || !originalViewBox) return null;
|
|
5046
|
+
if (target.type === 'node') {
|
|
5047
|
+
const box = result.boxes?.get?.(target.id);
|
|
5048
|
+
if (!box) return null;
|
|
5049
|
+
if (box.kind === 'container') return viewBoxForBox(svg, box, originalViewBox, options);
|
|
5050
|
+
return viewBoxForSmallBounds(svg, box, originalViewBox, options);
|
|
5051
|
+
}
|
|
5052
|
+
if (target.type === 'edge') {
|
|
5053
|
+
const route = result.routes?.get?.(target.id);
|
|
5054
|
+
const edgeOptions = {
|
|
5055
|
+
...options,
|
|
5056
|
+
zoomTargetHeightFraction: options.arrowZoomHeightFraction ?? options.edgeZoomHeightFraction ?? 0.02,
|
|
5057
|
+
smallZoomMinTargetHeight: options.arrowZoomMinTargetHeight ?? options.edgeZoomMinTargetHeight ?? 10
|
|
5058
|
+
};
|
|
5059
|
+
return viewBoxForSmallBounds(svg, routeZoomBounds(route, edgeOptions), originalViewBox, edgeOptions);
|
|
5060
|
+
}
|
|
5061
|
+
return null;
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5064
|
+
function enableLispgramZoom(svg, result, options = {}) {
|
|
5065
|
+
if (!svg || !result || !zoomEnabled(options)) return;
|
|
5066
|
+
const originalViewBox = parseViewBox(svg) || result.viewBox || null;
|
|
5067
|
+
if (!originalViewBox) return;
|
|
5068
|
+
let currentViewBox = { ...originalViewBox };
|
|
5069
|
+
const animationMs = Number.isFinite(Number(options.zoomAnimationMs)) ? Number(options.zoomAnimationMs) : 160;
|
|
5070
|
+
|
|
5071
|
+
function setViewBox(vb) {
|
|
5072
|
+
if (!vb) return null;
|
|
5073
|
+
currentViewBox = { ...vb };
|
|
5074
|
+
if (svg.style && animationMs > 0) svg.style.transition = `viewBox ${animationMs}ms ease`;
|
|
5075
|
+
writeViewBox(svg, currentViewBox);
|
|
5076
|
+
return currentViewBox;
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
function zoomToTarget(target) {
|
|
5080
|
+
return setViewBox(viewBoxForZoomTarget(svg, target, result, originalViewBox, options));
|
|
5081
|
+
}
|
|
5082
|
+
|
|
5083
|
+
function zoomToBox(id) {
|
|
5084
|
+
return zoomToTarget({ type: 'node', id });
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
function zoomToEdge(id) {
|
|
5088
|
+
return zoomToTarget({ type: 'edge', id });
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
function reset() {
|
|
5092
|
+
return setViewBox(originalViewBox);
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5095
|
+
let suppressNextShiftClick = false;
|
|
5096
|
+
const keyState = { aDown: false };
|
|
5097
|
+
lispgramZoomKeyState.set(svg, keyState);
|
|
5098
|
+
|
|
5099
|
+
function updateZoomKeyState(event, value) {
|
|
5100
|
+
const key = normalizeKeyboardKey(event);
|
|
5101
|
+
if (key === 'a' || key === 'keya') keyState.aDown = value;
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5104
|
+
const keyTargets = [];
|
|
5105
|
+
const ownerDocument = svg.ownerDocument || (typeof document !== 'undefined' ? document : null);
|
|
5106
|
+
if (ownerDocument?.defaultView?.addEventListener) keyTargets.push(ownerDocument.defaultView);
|
|
5107
|
+
if (ownerDocument?.addEventListener) keyTargets.push(ownerDocument);
|
|
5108
|
+
for (const target of keyTargets) {
|
|
5109
|
+
target.addEventListener('keydown', (event) => updateZoomKeyState(event, true));
|
|
5110
|
+
target.addEventListener('keyup', (event) => updateZoomKeyState(event, false));
|
|
5111
|
+
target.addEventListener('blur', () => { keyState.aDown = false; });
|
|
5112
|
+
}
|
|
5113
|
+
|
|
5114
|
+
if (typeof svg.addEventListener === 'function') {
|
|
5115
|
+
svg.addEventListener('wheel', (event) => {
|
|
5116
|
+
if (!event?.shiftKey) return;
|
|
5117
|
+
if (typeof event.preventDefault === 'function') event.preventDefault();
|
|
5118
|
+
const scale = Number.isFinite(Number(options.zoomWheelScale)) ? Number(options.zoomWheelScale) : 1.05;
|
|
5119
|
+
const delta = Number(event.deltaY || 0);
|
|
5120
|
+
const factor = delta < 0 ? 1 / scale : scale;
|
|
5121
|
+
const pivot = svgPointFromEvent(svg, event, currentViewBox);
|
|
5122
|
+
currentViewBox = zoomViewBoxAt(svg, pivot, factor, originalViewBox, options) || currentViewBox;
|
|
5123
|
+
}, { passive: false });
|
|
5124
|
+
|
|
5125
|
+
let dragState = null;
|
|
5126
|
+
const dragThreshold = Number.isFinite(Number(options.zoomDragThreshold)) ? Number(options.zoomDragThreshold) : 3;
|
|
5127
|
+
|
|
5128
|
+
function updateDrag(event) {
|
|
5129
|
+
if (!dragState) return;
|
|
5130
|
+
const rect = typeof svg.getBoundingClientRect === 'function' ? svg.getBoundingClientRect() : null;
|
|
5131
|
+
if (!rect?.width || !rect?.height) return;
|
|
5132
|
+
const dx = Number(event.clientX) - dragState.startClientX;
|
|
5133
|
+
const dy = Number(event.clientY) - dragState.startClientY;
|
|
5134
|
+
if (!dragState.moved && Math.hypot(dx, dy) >= dragThreshold) dragState.moved = true;
|
|
5135
|
+
if (typeof event.preventDefault === 'function') event.preventDefault();
|
|
5136
|
+
const next = {
|
|
5137
|
+
x: dragState.startViewBox.x - dx / rect.width * dragState.startViewBox.w,
|
|
5138
|
+
y: dragState.startViewBox.y - dy / rect.height * dragState.startViewBox.h,
|
|
5139
|
+
w: dragState.startViewBox.w,
|
|
5140
|
+
h: dragState.startViewBox.h
|
|
5141
|
+
};
|
|
5142
|
+
currentViewBox = next;
|
|
5143
|
+
writeViewBox(svg, currentViewBox);
|
|
5144
|
+
}
|
|
5145
|
+
|
|
5146
|
+
function endDrag(event) {
|
|
5147
|
+
if (!dragState) return;
|
|
5148
|
+
if (dragState.moved) {
|
|
5149
|
+
suppressNextShiftClick = true;
|
|
5150
|
+
if (typeof event?.preventDefault === 'function') event.preventDefault();
|
|
5151
|
+
}
|
|
5152
|
+
if (dragState.pointerId != null && typeof svg.releasePointerCapture === 'function') {
|
|
5153
|
+
try { svg.releasePointerCapture(dragState.pointerId); } catch {}
|
|
5154
|
+
}
|
|
5155
|
+
dragState = null;
|
|
5156
|
+
if (svg.style) svg.style.cursor = '';
|
|
5157
|
+
setTimeout(() => { suppressNextShiftClick = false; }, 0);
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
if (typeof svg.addEventListener === 'function') {
|
|
5161
|
+
svg.addEventListener('pointerdown', (event) => {
|
|
5162
|
+
if (!event?.shiftKey) return;
|
|
5163
|
+
if (isZoomClickEvent(svg, event)) return;
|
|
5164
|
+
if (event.button != null && event.button !== 0) return;
|
|
5165
|
+
const start = parseViewBox(svg) || currentViewBox;
|
|
5166
|
+
if (!start) return;
|
|
5167
|
+
dragState = {
|
|
5168
|
+
pointerId: event.pointerId,
|
|
5169
|
+
startClientX: Number(event.clientX),
|
|
5170
|
+
startClientY: Number(event.clientY),
|
|
5171
|
+
startViewBox: { ...start },
|
|
5172
|
+
moved: false
|
|
5173
|
+
};
|
|
5174
|
+
if (typeof svg.setPointerCapture === 'function' && event.pointerId != null) {
|
|
5175
|
+
try { svg.setPointerCapture(event.pointerId); } catch {}
|
|
5176
|
+
}
|
|
5177
|
+
if (svg.style) svg.style.cursor = 'grabbing';
|
|
5178
|
+
if (typeof event.preventDefault === 'function') event.preventDefault();
|
|
5179
|
+
}, { passive: false });
|
|
5180
|
+
svg.addEventListener('pointermove', updateDrag, { passive: false });
|
|
5181
|
+
svg.addEventListener('pointerup', endDrag, { passive: false });
|
|
5182
|
+
svg.addEventListener('pointercancel', endDrag, { passive: false });
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
|
|
5186
|
+
{
|
|
5187
|
+
const hitboxes = [];
|
|
5188
|
+
walkSvgElements(svg, (node) => {
|
|
5189
|
+
if (typeof node.getAttribute !== 'function') return;
|
|
5190
|
+
const cls = node.getAttribute('class');
|
|
5191
|
+
if (cls === 'lispgram-compound-hitbox' || cls === 'lispgram-node-hitbox') {
|
|
5192
|
+
const id = node.getAttribute('data-node-id');
|
|
5193
|
+
if (result.boxes?.has?.(id)) hitboxes.push([node, { type: 'node', id }]);
|
|
5194
|
+
} else if (cls === 'lispgram-edge-hitbox') {
|
|
5195
|
+
const id = node.getAttribute('data-edge-id');
|
|
5196
|
+
if (result.routes?.has?.(id)) hitboxes.push([node, { type: 'edge', id }]);
|
|
5197
|
+
}
|
|
5198
|
+
});
|
|
5199
|
+
for (const [el, target] of hitboxes) {
|
|
5200
|
+
if (typeof el.addEventListener !== 'function') continue;
|
|
5201
|
+
el.addEventListener('click', (event) => {
|
|
5202
|
+
if (!isZoomClickEvent(svg, event)) return;
|
|
5203
|
+
if (suppressNextShiftClick) return;
|
|
5204
|
+
if (typeof event.preventDefault === 'function') event.preventDefault();
|
|
5205
|
+
if (typeof event.stopPropagation === 'function') event.stopPropagation();
|
|
5206
|
+
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
|
|
5207
|
+
zoomToTarget(target);
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
|
|
5212
|
+
svg.__lispgramZoom = { enabled: true, originalViewBox, get viewBox() { return currentViewBox; }, reset, zoomToBox, zoomToEdge, zoomToTarget };
|
|
5213
|
+
}
|
|
5214
|
+
|
|
4891
5215
|
function enableLispgramClickFocus(svg, result, options = {}) {
|
|
4892
5216
|
if (!svg || !result || options.clickToFocus === false) return;
|
|
4893
5217
|
const dimOpacity = Number.isFinite(Number(options.focusDimOpacity)) ? Number(options.focusDimOpacity) : 0.12;
|
|
@@ -4960,6 +5284,7 @@ function enableLispgramClickFocus(svg, result, options = {}) {
|
|
|
4960
5284
|
if (typeof el.addEventListener !== 'function') continue;
|
|
4961
5285
|
if (el.style) el.style.cursor = 'pointer';
|
|
4962
5286
|
el.addEventListener('click', (event) => {
|
|
5287
|
+
if (zoomEnabled(options) && event?.shiftKey && result.boxes?.has?.(id)) return;
|
|
4963
5288
|
stop(event);
|
|
4964
5289
|
const next = { type: 'node', id };
|
|
4965
5290
|
applyFocus(sameFocus(currentFocus, next) ? null : next);
|
|
@@ -4972,6 +5297,7 @@ function enableLispgramClickFocus(svg, result, options = {}) {
|
|
|
4972
5297
|
if (typeof el.addEventListener !== 'function') continue;
|
|
4973
5298
|
if (el.style) el.style.cursor = 'pointer';
|
|
4974
5299
|
el.addEventListener('click', (event) => {
|
|
5300
|
+
if (zoomEnabled(options) && event?.shiftKey && result.routes?.has?.(id)) return;
|
|
4975
5301
|
stop(event);
|
|
4976
5302
|
const next = { type: 'edge', id };
|
|
4977
5303
|
applyFocus(sameFocus(currentFocus, next) ? null : next);
|
|
@@ -4999,6 +5325,7 @@ function createLispgramConstraintSvg(result, options = {}) {
|
|
|
4999
5325
|
for (const route of result.routes.values()) renderLispgramRoute(svg, route, options);
|
|
5000
5326
|
renderLispgramHitLayer(svg, result, options);
|
|
5001
5327
|
enableLispgramClickFocus(svg, result, options);
|
|
5328
|
+
enableLispgramZoom(svg, result, options);
|
|
5002
5329
|
return svg;
|
|
5003
5330
|
}
|
|
5004
5331
|
|
|
@@ -5207,6 +5534,7 @@ function elementRenderOptions(sourceElement, options = {}) {
|
|
|
5207
5534
|
...options,
|
|
5208
5535
|
showArrowGlyphs: parseBooleanOption(data.lispgramArrowGlyphs, options.showArrowGlyphs ?? false),
|
|
5209
5536
|
clickToFocus: parseBooleanOption(data.lispgramClickFocus, options.clickToFocus ?? true),
|
|
5537
|
+
zoom: parseBooleanOption(data.lispgramZoom, options.zoom ?? true),
|
|
5210
5538
|
arrowGlyphRadius: parseNumericOption(data.lispgramArrowGlyphRadius, options.arrowGlyphRadius),
|
|
5211
5539
|
diagramId: data.lispgramDiagram || data.diagramId || options.diagramId || options.name || options.id || sourceElement?.id || undefined
|
|
5212
5540
|
};
|
|
@@ -5277,6 +5605,7 @@ function initialize(options = {}) {
|
|
|
5277
5605
|
showNCF = false,
|
|
5278
5606
|
showArrowGlyphs = false,
|
|
5279
5607
|
clickToFocus = true,
|
|
5608
|
+
zoom = true,
|
|
5280
5609
|
arrowGlyphRadius = 5.75,
|
|
5281
5610
|
height,
|
|
5282
5611
|
width,
|
|
@@ -5288,7 +5617,7 @@ function initialize(options = {}) {
|
|
|
5288
5617
|
for (const element of elements) {
|
|
5289
5618
|
if (element.dataset.lispgramMounted === 'true') continue;
|
|
5290
5619
|
try {
|
|
5291
|
-
renderElement(element, { showNCF, showArrowGlyphs, clickToFocus, arrowGlyphRadius, height, width, diagramId });
|
|
5620
|
+
renderElement(element, { showNCF, showArrowGlyphs, clickToFocus, zoom, arrowGlyphRadius, height, width, diagramId });
|
|
5292
5621
|
element.dataset.lispgramMounted = 'true';
|
|
5293
5622
|
} catch {
|
|
5294
5623
|
element.dataset.lispgramMounted = 'error';
|