@chadfurman/docsify-mermaid-zoom 1.0.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 +5 -3
- package/dist/docsify-mermaid-zoom.css +8 -0
- package/dist/docsify-mermaid-zoom.js +103 -19
- package/package.json +1 -1
- package/src/docsify-mermaid-zoom.css +8 -0
- package/src/docsify-mermaid-zoom.js +103 -19
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,21 +116,51 @@
|
|
|
107
116
|
controls.appendChild(fullscreenBtn);
|
|
108
117
|
container.appendChild(controls);
|
|
109
118
|
|
|
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');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
110
148
|
// Hint
|
|
111
149
|
var hint = document.createElement('div');
|
|
112
150
|
hint.className = 'mermaid-zoom-hint';
|
|
113
|
-
hint.textContent = '
|
|
151
|
+
hint.textContent = 'Click to interact \u00B7 Pinch to zoom \u00B7 ESC to release';
|
|
114
152
|
container.appendChild(hint);
|
|
115
153
|
|
|
116
154
|
// Initialize svg-pan-zoom
|
|
117
155
|
try {
|
|
156
|
+
log('initializing svg-pan-zoom');
|
|
118
157
|
var panZoom = svgPanZoom(svg, {
|
|
119
158
|
zoomEnabled: true,
|
|
120
159
|
panEnabled: true,
|
|
121
160
|
controlIconsEnabled: false,
|
|
122
161
|
mouseWheelZoomEnabled: false,
|
|
123
|
-
preventMouseEventsDefault:
|
|
124
|
-
zoomScaleSensitivity: 0.
|
|
162
|
+
preventMouseEventsDefault: false,
|
|
163
|
+
zoomScaleSensitivity: 0.15,
|
|
125
164
|
minZoom: config.minZoom,
|
|
126
165
|
maxZoom: config.maxZoom,
|
|
127
166
|
fit: true,
|
|
@@ -129,15 +168,61 @@
|
|
|
129
168
|
contain: false
|
|
130
169
|
});
|
|
131
170
|
|
|
132
|
-
//
|
|
133
|
-
//
|
|
171
|
+
// Wheel handler:
|
|
172
|
+
// - Pinch (ctrlKey): zoom
|
|
173
|
+
// - Focused: pan the diagram
|
|
174
|
+
// - Not focused: scroll the page
|
|
175
|
+
var PAN_SPEED = 3;
|
|
134
176
|
container.addEventListener('wheel', function (e) {
|
|
135
|
-
|
|
136
|
-
e.
|
|
137
|
-
|
|
138
|
-
|
|
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');
|
|
139
194
|
}, { passive: false });
|
|
140
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
|
+
|
|
141
226
|
// Force fit after layout settles
|
|
142
227
|
setTimeout(function () {
|
|
143
228
|
panZoom.resize();
|
|
@@ -170,7 +255,13 @@
|
|
|
170
255
|
fullscreenBtn.textContent = isFullscreen ? '\u2716' : '\u26F6';
|
|
171
256
|
fullscreenBtn.title = isFullscreen ? 'Exit fullscreen' : 'Fullscreen';
|
|
172
257
|
document.body.style.overflow = isFullscreen ? 'hidden' : '';
|
|
173
|
-
if (
|
|
258
|
+
if (isFullscreen) {
|
|
259
|
+
container.focus();
|
|
260
|
+
setFocused(true);
|
|
261
|
+
} else {
|
|
262
|
+
container.style.height = savedHeight;
|
|
263
|
+
setFocused(false);
|
|
264
|
+
}
|
|
174
265
|
setTimeout(function () {
|
|
175
266
|
panZoom.resize();
|
|
176
267
|
panZoom.fit();
|
|
@@ -178,13 +269,6 @@
|
|
|
178
269
|
}, 50);
|
|
179
270
|
});
|
|
180
271
|
|
|
181
|
-
// ESC to exit fullscreen
|
|
182
|
-
document.addEventListener('keydown', function (e) {
|
|
183
|
-
if (e.key === 'Escape' && container.classList.contains('fullscreen')) {
|
|
184
|
-
fullscreenBtn.click();
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
|
|
188
272
|
// Refit on window resize and container resize (draggable corner)
|
|
189
273
|
var resizeTimer;
|
|
190
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,21 +116,51 @@
|
|
|
107
116
|
controls.appendChild(fullscreenBtn);
|
|
108
117
|
container.appendChild(controls);
|
|
109
118
|
|
|
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');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
110
148
|
// Hint
|
|
111
149
|
var hint = document.createElement('div');
|
|
112
150
|
hint.className = 'mermaid-zoom-hint';
|
|
113
|
-
hint.textContent = '
|
|
151
|
+
hint.textContent = 'Click to interact \u00B7 Pinch to zoom \u00B7 ESC to release';
|
|
114
152
|
container.appendChild(hint);
|
|
115
153
|
|
|
116
154
|
// Initialize svg-pan-zoom
|
|
117
155
|
try {
|
|
156
|
+
log('initializing svg-pan-zoom');
|
|
118
157
|
var panZoom = svgPanZoom(svg, {
|
|
119
158
|
zoomEnabled: true,
|
|
120
159
|
panEnabled: true,
|
|
121
160
|
controlIconsEnabled: false,
|
|
122
161
|
mouseWheelZoomEnabled: false,
|
|
123
|
-
preventMouseEventsDefault:
|
|
124
|
-
zoomScaleSensitivity: 0.
|
|
162
|
+
preventMouseEventsDefault: false,
|
|
163
|
+
zoomScaleSensitivity: 0.15,
|
|
125
164
|
minZoom: config.minZoom,
|
|
126
165
|
maxZoom: config.maxZoom,
|
|
127
166
|
fit: true,
|
|
@@ -129,15 +168,61 @@
|
|
|
129
168
|
contain: false
|
|
130
169
|
});
|
|
131
170
|
|
|
132
|
-
//
|
|
133
|
-
//
|
|
171
|
+
// Wheel handler:
|
|
172
|
+
// - Pinch (ctrlKey): zoom
|
|
173
|
+
// - Focused: pan the diagram
|
|
174
|
+
// - Not focused: scroll the page
|
|
175
|
+
var PAN_SPEED = 3;
|
|
134
176
|
container.addEventListener('wheel', function (e) {
|
|
135
|
-
|
|
136
|
-
e.
|
|
137
|
-
|
|
138
|
-
|
|
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');
|
|
139
194
|
}, { passive: false });
|
|
140
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
|
+
|
|
141
226
|
// Force fit after layout settles
|
|
142
227
|
setTimeout(function () {
|
|
143
228
|
panZoom.resize();
|
|
@@ -170,7 +255,13 @@
|
|
|
170
255
|
fullscreenBtn.textContent = isFullscreen ? '\u2716' : '\u26F6';
|
|
171
256
|
fullscreenBtn.title = isFullscreen ? 'Exit fullscreen' : 'Fullscreen';
|
|
172
257
|
document.body.style.overflow = isFullscreen ? 'hidden' : '';
|
|
173
|
-
if (
|
|
258
|
+
if (isFullscreen) {
|
|
259
|
+
container.focus();
|
|
260
|
+
setFocused(true);
|
|
261
|
+
} else {
|
|
262
|
+
container.style.height = savedHeight;
|
|
263
|
+
setFocused(false);
|
|
264
|
+
}
|
|
174
265
|
setTimeout(function () {
|
|
175
266
|
panZoom.resize();
|
|
176
267
|
panZoom.fit();
|
|
@@ -178,13 +269,6 @@
|
|
|
178
269
|
}, 50);
|
|
179
270
|
});
|
|
180
271
|
|
|
181
|
-
// ESC to exit fullscreen
|
|
182
|
-
document.addEventListener('keydown', function (e) {
|
|
183
|
-
if (e.key === 'Escape' && container.classList.contains('fullscreen')) {
|
|
184
|
-
fullscreenBtn.click();
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
|
|
188
272
|
// Refit on window resize and container resize (draggable corner)
|
|
189
273
|
var resizeTimer;
|
|
190
274
|
var resizeHandler = function () {
|