lispgram 0.10.2 → 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 +4 -4
- package/dist/index.js +115 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ npm install lispgram
|
|
|
121
121
|
startOnLoad: true,
|
|
122
122
|
showArrowGlyphs: true,
|
|
123
123
|
clickToFocus: true,
|
|
124
|
-
zoom:
|
|
124
|
+
zoom: true
|
|
125
125
|
});
|
|
126
126
|
</script>
|
|
127
127
|
```
|
|
@@ -227,7 +227,7 @@ import { render, renderElement, initialize, createDiagramSvg } from "lispgram/la
|
|
|
227
227
|
|
|
228
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.
|
|
229
229
|
|
|
230
|
-
`options.zoom` controls built-in
|
|
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
231
|
|
|
232
232
|
### Interaction entry point
|
|
233
233
|
|
|
@@ -289,7 +289,7 @@ When `clickToFocus` is enabled, clicking a node, compound/container, or arrow fo
|
|
|
289
289
|
|
|
290
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`.
|
|
291
291
|
|
|
292
|
-
The examples use `zoom:
|
|
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
293
|
|
|
294
294
|
## Demo
|
|
295
295
|
|
|
@@ -317,7 +317,7 @@ navigate to examples/basic.html in browser
|
|
|
317
317
|
startOnLoad: true,
|
|
318
318
|
showArrowGlyphs: true,
|
|
319
319
|
clickToFocus: true,
|
|
320
|
-
zoom:
|
|
320
|
+
zoom: true,
|
|
321
321
|
arrowGlyphRadius: 5.75
|
|
322
322
|
});
|
|
323
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 &&
|
|
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,26 +4888,25 @@ 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
|
|
4892
|
-
|
|
4893
|
-
if (raw === false || raw == null && options.noZoom === true) return 'none';
|
|
4894
|
-
if (raw == null || raw === true || raw === '') return 'both';
|
|
4895
|
-
const value = String(raw).trim().toLowerCase().replace(/_/g, '-');
|
|
4896
|
-
if (['none', 'off', 'false', '0', 'no', 'disabled', 'disable'].includes(value)) return 'none';
|
|
4897
|
-
if (['wheel', 'scroll', 'cursor', 'cursor-wheel', 'shift-wheel', 'shift-scroll', 'wheel-at-cursor', 'scroll-at-cursor'].includes(value)) return 'shift-wheel';
|
|
4898
|
-
if (['container', 'click-container', 'shift-click', 'shift-click-container', 'container-fit', 'fit-container', 'zoom-container', 'container-zoom'].includes(value)) return 'shift-click-container';
|
|
4899
|
-
if (['both', 'all', 'shift-both'].includes(value)) return 'both';
|
|
4900
|
-
return 'both';
|
|
4891
|
+
function zoomEnabled(options = {}) {
|
|
4892
|
+
return options.zoom !== false;
|
|
4901
4893
|
}
|
|
4902
4894
|
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
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
4906
|
}
|
|
4907
4907
|
|
|
4908
|
-
function
|
|
4909
|
-
|
|
4910
|
-
return mode === 'shift-wheel' || mode === 'both';
|
|
4908
|
+
function isZoomClickEvent(svg, event) {
|
|
4909
|
+
return !!event?.shiftKey && eventHasZoomClickKey(svg, event);
|
|
4911
4910
|
}
|
|
4912
4911
|
|
|
4913
4912
|
function parseViewBox(svg) {
|
|
@@ -5029,8 +5028,8 @@ function routeZoomBounds(route, options = {}) {
|
|
|
5029
5028
|
function viewBoxForSmallBounds(svg, bounds, originalViewBox, options = {}) {
|
|
5030
5029
|
if (!bounds || !originalViewBox) return null;
|
|
5031
5030
|
const aspect = svgDisplayAspect(svg, originalViewBox) || 1;
|
|
5032
|
-
const fractionRaw = Number(options.zoomTargetHeightFraction ?? options.smallZoomHeightFraction ?? 0.
|
|
5033
|
-
const fraction = Number.isFinite(fractionRaw) && fractionRaw > 0 && fractionRaw < 1 ? fractionRaw : 0.
|
|
5031
|
+
const fractionRaw = Number(options.zoomTargetHeightFraction ?? options.smallZoomHeightFraction ?? 0.08);
|
|
5032
|
+
const fraction = Number.isFinite(fractionRaw) && fractionRaw > 0 && fractionRaw < 1 ? fractionRaw : 0.08;
|
|
5034
5033
|
const minTargetH = Number.isFinite(Number(options.smallZoomMinTargetHeight)) ? Number(options.smallZoomMinTargetHeight) : 10;
|
|
5035
5034
|
const targetH = Math.max(bounds.h || 0, minTargetH);
|
|
5036
5035
|
let h = targetH / fraction;
|
|
@@ -5052,15 +5051,18 @@ function viewBoxForZoomTarget(svg, target, result, originalViewBox, options = {}
|
|
|
5052
5051
|
}
|
|
5053
5052
|
if (target.type === 'edge') {
|
|
5054
5053
|
const route = result.routes?.get?.(target.id);
|
|
5055
|
-
|
|
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);
|
|
5056
5060
|
}
|
|
5057
5061
|
return null;
|
|
5058
5062
|
}
|
|
5059
5063
|
|
|
5060
5064
|
function enableLispgramZoom(svg, result, options = {}) {
|
|
5061
|
-
if (!svg || !result) return;
|
|
5062
|
-
const mode = normalizeZoomMode(options);
|
|
5063
|
-
if (mode === 'none') return;
|
|
5065
|
+
if (!svg || !result || !zoomEnabled(options)) return;
|
|
5064
5066
|
const originalViewBox = parseViewBox(svg) || result.viewBox || null;
|
|
5065
5067
|
if (!originalViewBox) return;
|
|
5066
5068
|
let currentViewBox = { ...originalViewBox };
|
|
@@ -5090,19 +5092,98 @@ function enableLispgramZoom(svg, result, options = {}) {
|
|
|
5090
5092
|
return setViewBox(originalViewBox);
|
|
5091
5093
|
}
|
|
5092
5094
|
|
|
5093
|
-
|
|
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') {
|
|
5094
5115
|
svg.addEventListener('wheel', (event) => {
|
|
5095
5116
|
if (!event?.shiftKey) return;
|
|
5096
5117
|
if (typeof event.preventDefault === 'function') event.preventDefault();
|
|
5097
|
-
const scale = Number.isFinite(Number(options.zoomWheelScale)) ? Number(options.zoomWheelScale) : 1.
|
|
5118
|
+
const scale = Number.isFinite(Number(options.zoomWheelScale)) ? Number(options.zoomWheelScale) : 1.05;
|
|
5098
5119
|
const delta = Number(event.deltaY || 0);
|
|
5099
5120
|
const factor = delta < 0 ? 1 / scale : scale;
|
|
5100
5121
|
const pivot = svgPointFromEvent(svg, event, currentViewBox);
|
|
5101
5122
|
currentViewBox = zoomViewBoxAt(svg, pivot, factor, originalViewBox, options) || currentViewBox;
|
|
5102
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
|
+
}
|
|
5103
5184
|
}
|
|
5104
5185
|
|
|
5105
|
-
|
|
5186
|
+
{
|
|
5106
5187
|
const hitboxes = [];
|
|
5107
5188
|
walkSvgElements(svg, (node) => {
|
|
5108
5189
|
if (typeof node.getAttribute !== 'function') return;
|
|
@@ -5118,7 +5199,8 @@ function enableLispgramZoom(svg, result, options = {}) {
|
|
|
5118
5199
|
for (const [el, target] of hitboxes) {
|
|
5119
5200
|
if (typeof el.addEventListener !== 'function') continue;
|
|
5120
5201
|
el.addEventListener('click', (event) => {
|
|
5121
|
-
if (!event
|
|
5202
|
+
if (!isZoomClickEvent(svg, event)) return;
|
|
5203
|
+
if (suppressNextShiftClick) return;
|
|
5122
5204
|
if (typeof event.preventDefault === 'function') event.preventDefault();
|
|
5123
5205
|
if (typeof event.stopPropagation === 'function') event.stopPropagation();
|
|
5124
5206
|
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
|
|
@@ -5127,7 +5209,7 @@ function enableLispgramZoom(svg, result, options = {}) {
|
|
|
5127
5209
|
}
|
|
5128
5210
|
}
|
|
5129
5211
|
|
|
5130
|
-
svg.__lispgramZoom = {
|
|
5212
|
+
svg.__lispgramZoom = { enabled: true, originalViewBox, get viewBox() { return currentViewBox; }, reset, zoomToBox, zoomToEdge, zoomToTarget };
|
|
5131
5213
|
}
|
|
5132
5214
|
|
|
5133
5215
|
function enableLispgramClickFocus(svg, result, options = {}) {
|
|
@@ -5202,7 +5284,7 @@ function enableLispgramClickFocus(svg, result, options = {}) {
|
|
|
5202
5284
|
if (typeof el.addEventListener !== 'function') continue;
|
|
5203
5285
|
if (el.style) el.style.cursor = 'pointer';
|
|
5204
5286
|
el.addEventListener('click', (event) => {
|
|
5205
|
-
if (event?.shiftKey &&
|
|
5287
|
+
if (zoomEnabled(options) && event?.shiftKey && result.boxes?.has?.(id)) return;
|
|
5206
5288
|
stop(event);
|
|
5207
5289
|
const next = { type: 'node', id };
|
|
5208
5290
|
applyFocus(sameFocus(currentFocus, next) ? null : next);
|
|
@@ -5215,6 +5297,7 @@ function enableLispgramClickFocus(svg, result, options = {}) {
|
|
|
5215
5297
|
if (typeof el.addEventListener !== 'function') continue;
|
|
5216
5298
|
if (el.style) el.style.cursor = 'pointer';
|
|
5217
5299
|
el.addEventListener('click', (event) => {
|
|
5300
|
+
if (zoomEnabled(options) && event?.shiftKey && result.routes?.has?.(id)) return;
|
|
5218
5301
|
stop(event);
|
|
5219
5302
|
const next = { type: 'edge', id };
|
|
5220
5303
|
applyFocus(sameFocus(currentFocus, next) ? null : next);
|
|
@@ -5451,8 +5534,7 @@ function elementRenderOptions(sourceElement, options = {}) {
|
|
|
5451
5534
|
...options,
|
|
5452
5535
|
showArrowGlyphs: parseBooleanOption(data.lispgramArrowGlyphs, options.showArrowGlyphs ?? false),
|
|
5453
5536
|
clickToFocus: parseBooleanOption(data.lispgramClickFocus, options.clickToFocus ?? true),
|
|
5454
|
-
zoom: data.lispgramZoom
|
|
5455
|
-
zoomMode: data.lispgramZoomMode || data.lispgramZoom || options.zoomMode || options.zoom,
|
|
5537
|
+
zoom: parseBooleanOption(data.lispgramZoom, options.zoom ?? true),
|
|
5456
5538
|
arrowGlyphRadius: parseNumericOption(data.lispgramArrowGlyphRadius, options.arrowGlyphRadius),
|
|
5457
5539
|
diagramId: data.lispgramDiagram || data.diagramId || options.diagramId || options.name || options.id || sourceElement?.id || undefined
|
|
5458
5540
|
};
|
|
@@ -5523,8 +5605,7 @@ function initialize(options = {}) {
|
|
|
5523
5605
|
showNCF = false,
|
|
5524
5606
|
showArrowGlyphs = false,
|
|
5525
5607
|
clickToFocus = true,
|
|
5526
|
-
zoom,
|
|
5527
|
-
zoomMode,
|
|
5608
|
+
zoom = true,
|
|
5528
5609
|
arrowGlyphRadius = 5.75,
|
|
5529
5610
|
height,
|
|
5530
5611
|
width,
|
|
@@ -5536,7 +5617,7 @@ function initialize(options = {}) {
|
|
|
5536
5617
|
for (const element of elements) {
|
|
5537
5618
|
if (element.dataset.lispgramMounted === 'true') continue;
|
|
5538
5619
|
try {
|
|
5539
|
-
renderElement(element, { showNCF, showArrowGlyphs, clickToFocus, zoom
|
|
5620
|
+
renderElement(element, { showNCF, showArrowGlyphs, clickToFocus, zoom, arrowGlyphRadius, height, width, diagramId });
|
|
5540
5621
|
element.dataset.lispgramMounted = 'true';
|
|
5541
5622
|
} catch {
|
|
5542
5623
|
element.dataset.lispgramMounted = 'error';
|