@chadfurman/docsify-mermaid-zoom 1.1.0 → 1.2.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
CHANGED
|
@@ -6,13 +6,15 @@ Interactive mermaid diagrams for [docsify](https://docsify.js.org/) — zoom, pa
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- **
|
|
9
|
+
- **Click-to-focus interaction** — click a diagram to focus it. When focused: two-finger scroll pans the diagram, arrow keys navigate. ESC releases focus. Unfocused diagrams let scroll pass through to the page.
|
|
10
|
+
- **Pinch-to-zoom / Ctrl+scroll** — always works regardless of focus state
|
|
10
11
|
- **Click-and-drag to pan** with grab/grabbing cursor
|
|
11
12
|
- **Resize handle** — drag the bottom-right corner to make the diagram taller/shorter
|
|
12
|
-
- **Fullscreen mode** — expand any diagram to fill the viewport (ESC to exit)
|
|
13
|
+
- **Fullscreen mode** — expand any diagram to fill the viewport (auto-focused, ESC to exit)
|
|
13
14
|
- **Zoom controls** — +, -, reset buttons in the top-right corner
|
|
14
15
|
- **Auto-fit** — diagrams fit and center on load, resize, and page navigation
|
|
15
|
-
- **
|
|
16
|
+
- **Visual focus indicator** — teal outline ring shows when a diagram is active
|
|
17
|
+
- **Configurable** — min/max zoom, container height limits, render delay, debug mode
|
|
16
18
|
- **Graceful fallback** — if svg-pan-zoom fails, diagrams still render normally
|
|
17
19
|
|
|
18
20
|
## Install
|
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
border-color: var(--mermaid-zoom-accent, #0F766E);
|
|
30
30
|
box-shadow: 0 2px 8px rgba(15, 118, 110, 0.12);
|
|
31
31
|
}
|
|
32
|
+
.mermaid-zoom-container.focused {
|
|
33
|
+
border-color: var(--mermaid-zoom-accent, #0F766E);
|
|
34
|
+
box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.25);
|
|
35
|
+
outline: none;
|
|
36
|
+
}
|
|
37
|
+
.mermaid-zoom-container:focus {
|
|
38
|
+
outline: none;
|
|
39
|
+
}
|
|
32
40
|
.mermaid-zoom-container svg {
|
|
33
41
|
cursor: grab;
|
|
34
42
|
max-width: none;
|
|
@@ -27,9 +27,16 @@
|
|
|
27
27
|
minZoom: 0.1,
|
|
28
28
|
maxZoom: 10,
|
|
29
29
|
minHeight: 300,
|
|
30
|
-
maxHeight: 800
|
|
30
|
+
maxHeight: 800,
|
|
31
|
+
debug: false
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
function log() {
|
|
35
|
+
if (getConfig().debug) {
|
|
36
|
+
console.log.apply(console, ['[mermaid-zoom]'].concat(Array.prototype.slice.call(arguments)));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
function getConfig() {
|
|
34
41
|
var userConfig = (window.$docsify && window.$docsify.mermaidZoom) || {};
|
|
35
42
|
return {
|
|
@@ -37,7 +44,8 @@
|
|
|
37
44
|
minZoom: userConfig.minZoom || DEFAULTS.minZoom,
|
|
38
45
|
maxZoom: userConfig.maxZoom || DEFAULTS.maxZoom,
|
|
39
46
|
minHeight: userConfig.minHeight || DEFAULTS.minHeight,
|
|
40
|
-
maxHeight: userConfig.maxHeight || DEFAULTS.maxHeight
|
|
47
|
+
maxHeight: userConfig.maxHeight || DEFAULTS.maxHeight,
|
|
48
|
+
debug: userConfig.debug || DEFAULTS.debug
|
|
41
49
|
};
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -52,6 +60,7 @@
|
|
|
52
60
|
function initZoomContainers() {
|
|
53
61
|
var config = getConfig();
|
|
54
62
|
var diagrams = document.querySelectorAll('.mermaid');
|
|
63
|
+
log('initZoomContainers called, found', diagrams.length, 'diagrams, config:', JSON.stringify(config));
|
|
55
64
|
|
|
56
65
|
diagrams.forEach(function (el) {
|
|
57
66
|
// Skip if already wrapped
|
|
@@ -107,30 +116,50 @@
|
|
|
107
116
|
controls.appendChild(fullscreenBtn);
|
|
108
117
|
container.appendChild(controls);
|
|
109
118
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
// Focus model: click diagram to focus it (scroll pans, arrows pan).
|
|
120
|
+
// ESC or click outside to release. Fullscreen always focused.
|
|
121
|
+
container.setAttribute('tabindex', '0');
|
|
122
|
+
var focused = false;
|
|
123
|
+
|
|
124
|
+
function setFocused(val) {
|
|
125
|
+
focused = val;
|
|
126
|
+
container.classList.toggle('focused', val);
|
|
127
|
+
log('focus changed:', val);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
container.addEventListener('click', function (e) {
|
|
131
|
+
// Don't focus if clicking a control button
|
|
132
|
+
if (e.target.closest && e.target.closest('.mermaid-zoom-controls')) return;
|
|
133
|
+
if (!focused) {
|
|
134
|
+
container.focus();
|
|
135
|
+
setFocused(true);
|
|
136
|
+
log('diagram focused via click');
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
container.addEventListener('blur', function () {
|
|
141
|
+
// Don't unfocus if in fullscreen
|
|
142
|
+
if (!container.classList.contains('fullscreen')) {
|
|
143
|
+
setFocused(false);
|
|
144
|
+
log('diagram blurred');
|
|
115
145
|
}
|
|
116
146
|
});
|
|
117
|
-
// Make sure the container doesn't capture tabindex
|
|
118
|
-
container.setAttribute('tabindex', '-1');
|
|
119
147
|
|
|
120
148
|
// Hint
|
|
121
149
|
var hint = document.createElement('div');
|
|
122
150
|
hint.className = 'mermaid-zoom-hint';
|
|
123
|
-
hint.textContent = '
|
|
151
|
+
hint.textContent = 'Click to interact \u00B7 Pinch to zoom \u00B7 ESC to release';
|
|
124
152
|
container.appendChild(hint);
|
|
125
153
|
|
|
126
154
|
// Initialize svg-pan-zoom
|
|
127
155
|
try {
|
|
156
|
+
log('initializing svg-pan-zoom');
|
|
128
157
|
var panZoom = svgPanZoom(svg, {
|
|
129
158
|
zoomEnabled: true,
|
|
130
159
|
panEnabled: true,
|
|
131
160
|
controlIconsEnabled: false,
|
|
132
161
|
mouseWheelZoomEnabled: false,
|
|
133
|
-
preventMouseEventsDefault:
|
|
162
|
+
preventMouseEventsDefault: false,
|
|
134
163
|
zoomScaleSensitivity: 0.15,
|
|
135
164
|
minZoom: config.minZoom,
|
|
136
165
|
maxZoom: config.maxZoom,
|
|
@@ -139,15 +168,61 @@
|
|
|
139
168
|
contain: false
|
|
140
169
|
});
|
|
141
170
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
171
|
+
// Wheel handler:
|
|
172
|
+
// - Pinch (ctrlKey): zoom
|
|
173
|
+
// - Focused: pan the diagram
|
|
174
|
+
// - Not focused: scroll the page
|
|
175
|
+
var PAN_SPEED = 3;
|
|
144
176
|
container.addEventListener('wheel', function (e) {
|
|
145
|
-
|
|
146
|
-
e.
|
|
147
|
-
|
|
148
|
-
|
|
177
|
+
// Pinch-to-zoom (ctrlKey) always works regardless of focus
|
|
178
|
+
if (e.ctrlKey) {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
var direction = e.deltaY < 0 ? 1.05 : 0.95;
|
|
181
|
+
log('pinch zoom by', direction);
|
|
182
|
+
panZoom.zoomBy(direction);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// When focused: scroll pans the diagram
|
|
186
|
+
if (focused || container.classList.contains('fullscreen')) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
panZoom.panBy({ x: -e.deltaX * PAN_SPEED, y: -e.deltaY * PAN_SPEED });
|
|
189
|
+
log('pan by', -e.deltaX * PAN_SPEED, -e.deltaY * PAN_SPEED);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Not focused: let scroll bubble to page
|
|
193
|
+
log('not focused, scroll passes through');
|
|
149
194
|
}, { passive: false });
|
|
150
195
|
|
|
196
|
+
// Arrow keys pan the diagram when focused
|
|
197
|
+
container.addEventListener('keydown', function (e) {
|
|
198
|
+
var PAN_STEP = 40;
|
|
199
|
+
// ESC: unfocus (or exit fullscreen)
|
|
200
|
+
if (e.key === 'Escape') {
|
|
201
|
+
if (container.classList.contains('fullscreen')) {
|
|
202
|
+
fullscreenBtn.click();
|
|
203
|
+
} else {
|
|
204
|
+
container.blur();
|
|
205
|
+
setFocused(false);
|
|
206
|
+
}
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Arrow keys only when focused
|
|
211
|
+
if (!focused && !container.classList.contains('fullscreen')) return;
|
|
212
|
+
var handled = true;
|
|
213
|
+
switch (e.key) {
|
|
214
|
+
case 'ArrowUp': panZoom.panBy({ x: 0, y: PAN_STEP }); break;
|
|
215
|
+
case 'ArrowDown': panZoom.panBy({ x: 0, y: -PAN_STEP }); break;
|
|
216
|
+
case 'ArrowLeft': panZoom.panBy({ x: PAN_STEP, y: 0 }); break;
|
|
217
|
+
case 'ArrowRight': panZoom.panBy({ x: -PAN_STEP, y: 0 }); break;
|
|
218
|
+
default: handled = false;
|
|
219
|
+
}
|
|
220
|
+
if (handled) {
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
log('arrow pan:', e.key);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
151
226
|
// Force fit after layout settles
|
|
152
227
|
setTimeout(function () {
|
|
153
228
|
panZoom.resize();
|
|
@@ -180,7 +255,13 @@
|
|
|
180
255
|
fullscreenBtn.textContent = isFullscreen ? '\u2716' : '\u26F6';
|
|
181
256
|
fullscreenBtn.title = isFullscreen ? 'Exit fullscreen' : 'Fullscreen';
|
|
182
257
|
document.body.style.overflow = isFullscreen ? 'hidden' : '';
|
|
183
|
-
if (
|
|
258
|
+
if (isFullscreen) {
|
|
259
|
+
container.focus();
|
|
260
|
+
setFocused(true);
|
|
261
|
+
} else {
|
|
262
|
+
container.style.height = savedHeight;
|
|
263
|
+
setFocused(false);
|
|
264
|
+
}
|
|
184
265
|
setTimeout(function () {
|
|
185
266
|
panZoom.resize();
|
|
186
267
|
panZoom.fit();
|
|
@@ -188,13 +269,6 @@
|
|
|
188
269
|
}, 50);
|
|
189
270
|
});
|
|
190
271
|
|
|
191
|
-
// ESC to exit fullscreen
|
|
192
|
-
document.addEventListener('keydown', function (e) {
|
|
193
|
-
if (e.key === 'Escape' && container.classList.contains('fullscreen')) {
|
|
194
|
-
fullscreenBtn.click();
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
272
|
// Refit on window resize and container resize (draggable corner)
|
|
199
273
|
var resizeTimer;
|
|
200
274
|
var resizeHandler = function () {
|
package/package.json
CHANGED
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
border-color: var(--mermaid-zoom-accent, #0F766E);
|
|
30
30
|
box-shadow: 0 2px 8px rgba(15, 118, 110, 0.12);
|
|
31
31
|
}
|
|
32
|
+
.mermaid-zoom-container.focused {
|
|
33
|
+
border-color: var(--mermaid-zoom-accent, #0F766E);
|
|
34
|
+
box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.25);
|
|
35
|
+
outline: none;
|
|
36
|
+
}
|
|
37
|
+
.mermaid-zoom-container:focus {
|
|
38
|
+
outline: none;
|
|
39
|
+
}
|
|
32
40
|
.mermaid-zoom-container svg {
|
|
33
41
|
cursor: grab;
|
|
34
42
|
max-width: none;
|
|
@@ -27,9 +27,16 @@
|
|
|
27
27
|
minZoom: 0.1,
|
|
28
28
|
maxZoom: 10,
|
|
29
29
|
minHeight: 300,
|
|
30
|
-
maxHeight: 800
|
|
30
|
+
maxHeight: 800,
|
|
31
|
+
debug: false
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
function log() {
|
|
35
|
+
if (getConfig().debug) {
|
|
36
|
+
console.log.apply(console, ['[mermaid-zoom]'].concat(Array.prototype.slice.call(arguments)));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
function getConfig() {
|
|
34
41
|
var userConfig = (window.$docsify && window.$docsify.mermaidZoom) || {};
|
|
35
42
|
return {
|
|
@@ -37,7 +44,8 @@
|
|
|
37
44
|
minZoom: userConfig.minZoom || DEFAULTS.minZoom,
|
|
38
45
|
maxZoom: userConfig.maxZoom || DEFAULTS.maxZoom,
|
|
39
46
|
minHeight: userConfig.minHeight || DEFAULTS.minHeight,
|
|
40
|
-
maxHeight: userConfig.maxHeight || DEFAULTS.maxHeight
|
|
47
|
+
maxHeight: userConfig.maxHeight || DEFAULTS.maxHeight,
|
|
48
|
+
debug: userConfig.debug || DEFAULTS.debug
|
|
41
49
|
};
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -52,6 +60,7 @@
|
|
|
52
60
|
function initZoomContainers() {
|
|
53
61
|
var config = getConfig();
|
|
54
62
|
var diagrams = document.querySelectorAll('.mermaid');
|
|
63
|
+
log('initZoomContainers called, found', diagrams.length, 'diagrams, config:', JSON.stringify(config));
|
|
55
64
|
|
|
56
65
|
diagrams.forEach(function (el) {
|
|
57
66
|
// Skip if already wrapped
|
|
@@ -107,30 +116,50 @@
|
|
|
107
116
|
controls.appendChild(fullscreenBtn);
|
|
108
117
|
container.appendChild(controls);
|
|
109
118
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
// Focus model: click diagram to focus it (scroll pans, arrows pan).
|
|
120
|
+
// ESC or click outside to release. Fullscreen always focused.
|
|
121
|
+
container.setAttribute('tabindex', '0');
|
|
122
|
+
var focused = false;
|
|
123
|
+
|
|
124
|
+
function setFocused(val) {
|
|
125
|
+
focused = val;
|
|
126
|
+
container.classList.toggle('focused', val);
|
|
127
|
+
log('focus changed:', val);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
container.addEventListener('click', function (e) {
|
|
131
|
+
// Don't focus if clicking a control button
|
|
132
|
+
if (e.target.closest && e.target.closest('.mermaid-zoom-controls')) return;
|
|
133
|
+
if (!focused) {
|
|
134
|
+
container.focus();
|
|
135
|
+
setFocused(true);
|
|
136
|
+
log('diagram focused via click');
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
container.addEventListener('blur', function () {
|
|
141
|
+
// Don't unfocus if in fullscreen
|
|
142
|
+
if (!container.classList.contains('fullscreen')) {
|
|
143
|
+
setFocused(false);
|
|
144
|
+
log('diagram blurred');
|
|
115
145
|
}
|
|
116
146
|
});
|
|
117
|
-
// Make sure the container doesn't capture tabindex
|
|
118
|
-
container.setAttribute('tabindex', '-1');
|
|
119
147
|
|
|
120
148
|
// Hint
|
|
121
149
|
var hint = document.createElement('div');
|
|
122
150
|
hint.className = 'mermaid-zoom-hint';
|
|
123
|
-
hint.textContent = '
|
|
151
|
+
hint.textContent = 'Click to interact \u00B7 Pinch to zoom \u00B7 ESC to release';
|
|
124
152
|
container.appendChild(hint);
|
|
125
153
|
|
|
126
154
|
// Initialize svg-pan-zoom
|
|
127
155
|
try {
|
|
156
|
+
log('initializing svg-pan-zoom');
|
|
128
157
|
var panZoom = svgPanZoom(svg, {
|
|
129
158
|
zoomEnabled: true,
|
|
130
159
|
panEnabled: true,
|
|
131
160
|
controlIconsEnabled: false,
|
|
132
161
|
mouseWheelZoomEnabled: false,
|
|
133
|
-
preventMouseEventsDefault:
|
|
162
|
+
preventMouseEventsDefault: false,
|
|
134
163
|
zoomScaleSensitivity: 0.15,
|
|
135
164
|
minZoom: config.minZoom,
|
|
136
165
|
maxZoom: config.maxZoom,
|
|
@@ -139,15 +168,61 @@
|
|
|
139
168
|
contain: false
|
|
140
169
|
});
|
|
141
170
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
171
|
+
// Wheel handler:
|
|
172
|
+
// - Pinch (ctrlKey): zoom
|
|
173
|
+
// - Focused: pan the diagram
|
|
174
|
+
// - Not focused: scroll the page
|
|
175
|
+
var PAN_SPEED = 3;
|
|
144
176
|
container.addEventListener('wheel', function (e) {
|
|
145
|
-
|
|
146
|
-
e.
|
|
147
|
-
|
|
148
|
-
|
|
177
|
+
// Pinch-to-zoom (ctrlKey) always works regardless of focus
|
|
178
|
+
if (e.ctrlKey) {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
var direction = e.deltaY < 0 ? 1.05 : 0.95;
|
|
181
|
+
log('pinch zoom by', direction);
|
|
182
|
+
panZoom.zoomBy(direction);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// When focused: scroll pans the diagram
|
|
186
|
+
if (focused || container.classList.contains('fullscreen')) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
panZoom.panBy({ x: -e.deltaX * PAN_SPEED, y: -e.deltaY * PAN_SPEED });
|
|
189
|
+
log('pan by', -e.deltaX * PAN_SPEED, -e.deltaY * PAN_SPEED);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Not focused: let scroll bubble to page
|
|
193
|
+
log('not focused, scroll passes through');
|
|
149
194
|
}, { passive: false });
|
|
150
195
|
|
|
196
|
+
// Arrow keys pan the diagram when focused
|
|
197
|
+
container.addEventListener('keydown', function (e) {
|
|
198
|
+
var PAN_STEP = 40;
|
|
199
|
+
// ESC: unfocus (or exit fullscreen)
|
|
200
|
+
if (e.key === 'Escape') {
|
|
201
|
+
if (container.classList.contains('fullscreen')) {
|
|
202
|
+
fullscreenBtn.click();
|
|
203
|
+
} else {
|
|
204
|
+
container.blur();
|
|
205
|
+
setFocused(false);
|
|
206
|
+
}
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Arrow keys only when focused
|
|
211
|
+
if (!focused && !container.classList.contains('fullscreen')) return;
|
|
212
|
+
var handled = true;
|
|
213
|
+
switch (e.key) {
|
|
214
|
+
case 'ArrowUp': panZoom.panBy({ x: 0, y: PAN_STEP }); break;
|
|
215
|
+
case 'ArrowDown': panZoom.panBy({ x: 0, y: -PAN_STEP }); break;
|
|
216
|
+
case 'ArrowLeft': panZoom.panBy({ x: PAN_STEP, y: 0 }); break;
|
|
217
|
+
case 'ArrowRight': panZoom.panBy({ x: -PAN_STEP, y: 0 }); break;
|
|
218
|
+
default: handled = false;
|
|
219
|
+
}
|
|
220
|
+
if (handled) {
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
log('arrow pan:', e.key);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
151
226
|
// Force fit after layout settles
|
|
152
227
|
setTimeout(function () {
|
|
153
228
|
panZoom.resize();
|
|
@@ -180,7 +255,13 @@
|
|
|
180
255
|
fullscreenBtn.textContent = isFullscreen ? '\u2716' : '\u26F6';
|
|
181
256
|
fullscreenBtn.title = isFullscreen ? 'Exit fullscreen' : 'Fullscreen';
|
|
182
257
|
document.body.style.overflow = isFullscreen ? 'hidden' : '';
|
|
183
|
-
if (
|
|
258
|
+
if (isFullscreen) {
|
|
259
|
+
container.focus();
|
|
260
|
+
setFocused(true);
|
|
261
|
+
} else {
|
|
262
|
+
container.style.height = savedHeight;
|
|
263
|
+
setFocused(false);
|
|
264
|
+
}
|
|
184
265
|
setTimeout(function () {
|
|
185
266
|
panZoom.resize();
|
|
186
267
|
panZoom.fit();
|
|
@@ -188,13 +269,6 @@
|
|
|
188
269
|
}, 50);
|
|
189
270
|
});
|
|
190
271
|
|
|
191
|
-
// ESC to exit fullscreen
|
|
192
|
-
document.addEventListener('keydown', function (e) {
|
|
193
|
-
if (e.key === 'Escape' && container.classList.contains('fullscreen')) {
|
|
194
|
-
fullscreenBtn.click();
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
272
|
// Refit on window resize and container resize (draggable corner)
|
|
199
273
|
var resizeTimer;
|
|
200
274
|
var resizeHandler = function () {
|