@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 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,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 = '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';
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: true,
124
- zoomScaleSensitivity: 0.3,
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
- // Custom wheel handler: only zoom on pinch (ctrlKey) or Ctrl+scroll.
133
- // 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;
134
176
  container.addEventListener('wheel', function (e) {
135
- if (!e.ctrlKey) return; // let normal scroll bubble to page
136
- e.preventDefault();
137
- var direction = e.deltaY < 0 ? 1.1 : 0.9;
138
- 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');
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 (!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
+ }
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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.0.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,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 = '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';
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: true,
124
- zoomScaleSensitivity: 0.3,
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
- // Custom wheel handler: only zoom on pinch (ctrlKey) or Ctrl+scroll.
133
- // 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;
134
176
  container.addEventListener('wheel', function (e) {
135
- if (!e.ctrlKey) return; // let normal scroll bubble to page
136
- e.preventDefault();
137
- var direction = e.deltaY < 0 ? 1.1 : 0.9;
138
- 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');
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 (!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
+ }
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 () {