@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
- - **Pinch-to-zoom / Ctrl+scroll** on any mermaid diagram (regular scrolling passes through to the page)
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
- - **Configurable** — min/max zoom, container height limits, render delay
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
- // Ensure clicking the diagram doesn't trap scroll/keyboard events
111
- container.addEventListener('click', function() {
112
- // Remove focus from the container so arrow keys scroll the page
113
- if (document.activeElement === container || container.contains(document.activeElement)) {
114
- container.blur();
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 = 'Pinch or Ctrl+scroll to zoom \u00B7 Drag to pan \u00B7 Resize from corner';
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: true,
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
- // Custom wheel handler: only zoom on pinch (ctrlKey) or Ctrl+scroll.
143
- // Regular two-finger scroll passes through to the page.
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
- if (!e.ctrlKey) return; // let normal scroll bubble to page
146
- e.preventDefault();
147
- var direction = e.deltaY < 0 ? 1.05 : 0.95;
148
- panZoom.zoomBy(direction);
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 (!isFullscreen) container.style.height = savedHeight;
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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.0",
6
+ "version": "1.2.0",
7
7
  "description": "Interactive mermaid diagrams for docsify — zoom, pan, resize, and fullscreen",
8
8
  "main": "dist/docsify-mermaid-zoom.js",
9
9
  "files": [
@@ -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
- // Ensure clicking the diagram doesn't trap scroll/keyboard events
111
- container.addEventListener('click', function() {
112
- // Remove focus from the container so arrow keys scroll the page
113
- if (document.activeElement === container || container.contains(document.activeElement)) {
114
- container.blur();
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 = 'Pinch or Ctrl+scroll to zoom \u00B7 Drag to pan \u00B7 Resize from corner';
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: true,
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
- // Custom wheel handler: only zoom on pinch (ctrlKey) or Ctrl+scroll.
143
- // Regular two-finger scroll passes through to the page.
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
- if (!e.ctrlKey) return; // let normal scroll bubble to page
146
- e.preventDefault();
147
- var direction = e.deltaY < 0 ? 1.05 : 0.95;
148
- panZoom.zoomBy(direction);
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 (!isFullscreen) container.style.height = savedHeight;
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 () {