joplin-plugin-minimap 1.1.1 → 1.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joplin-plugin-minimap",
3
- "version": "1.1.1",
3
+ "version": "1.1.4",
4
4
  "description": "A hover-to-expand table of contents minimap for the Joplin note viewer",
5
5
  "author": "lim0513",
6
6
  "homepage": "https://github.com/lim0513/joplin-minimap",
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 1,
3
3
  "id": "com.github.joplin-minimap",
4
4
  "app_min_version": "2.8",
5
- "version": "1.1.1",
5
+ "version": "1.1.4",
6
6
  "name": "Joplin Minimap",
7
7
  "description": "A hover-to-expand table of contents minimap for the note viewer. Shows heading tick marks on the right edge; hover to expand into a full ToC, click to jump.",
8
8
  "author": "lim0513",
@@ -25,8 +25,66 @@
25
25
 
26
26
  var cleanup = null; // removes listeners belonging to the current build
27
27
 
28
+ // The stylesheet is injected BY THIS SCRIPT and re-checked on every build.
29
+ // Rationale: Joplin can re-render the document in ways that drop injected
30
+ // asset stylesheets while this script's watcher survives - the rebuilt nav
31
+ // then renders UNSTYLED as flow content below the note (looks like a
32
+ // duplicated outline under the document). Keeping the CSS inline and
33
+ // re-injecting guarantees the nav and its styling live and die together.
34
+ var MINIMAP_CSS = "/* Joplin Minimap \u2014 collapsed tick bars, hover-expanded ToC panel.\n * Colors use currentColor / rgba so it follows both light and dark themes.\n */\n\n#jp-minimap {\n\tuser-select: none;\n\t-webkit-user-select: none;\n\tcaret-color: transparent;\n\tcursor: default;\n\tposition: fixed;\n\ttop: 50%;\n\tright: 6px;\n\ttransform: translateY(-50%);\n\tz-index: 9999;\n\tfont-size: 12.5px;\n\tline-height: 1.35;\n\tcolor: inherit;\n}\n\n.jp-mm-list {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: flex-end;\n\tpadding: 8px 6px;\n\tmax-height: 84vh;\n\toverflow: hidden;\n\tborder-radius: 10px;\n\ttransition: background 0.15s ease, box-shadow 0.15s ease;\n}\n\n.jp-mm-item {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: flex-end;\n\tpadding: 3px 4px;\n\tborder-radius: 6px;\n\ttext-decoration: none;\n\tcolor: inherit;\n\topacity: 0.5;\n\tcursor: pointer;\n\toutline: none;\n}\n\n/* ---- collapsed state: tick bars, width by heading level ---- */\n\n.jp-mm-bar {\n\tdisplay: block;\n\theight: 2px;\n\tborder-radius: 1px;\n\tbackground: currentColor;\n}\n\n.jp-mm-l1 .jp-mm-bar { width: 18px; }\n.jp-mm-l2 .jp-mm-bar { width: 13px; }\n.jp-mm-l3 .jp-mm-bar { width: 9px; }\n.jp-mm-l4 .jp-mm-bar { width: 7px; }\n.jp-mm-l5 .jp-mm-bar { width: 5px; }\n.jp-mm-l6 .jp-mm-bar { width: 5px; }\n\n.jp-mm-label { display: none; }\n\n/* ---- expanded state (hover) ---- */\n\n#jp-minimap:hover .jp-mm-list {\n\talign-items: stretch;\n\toverflow-y: auto;\n\toverscroll-behavior: contain;\n\tbackground: rgba(127, 127, 127, 0.16);\n\tbackdrop-filter: blur(10px);\n\t-webkit-backdrop-filter: blur(10px);\n\tbox-shadow: 0 6px 28px rgba(0, 0, 0, 0.28);\n}\n\n#jp-minimap:hover .jp-mm-bar { display: none; }\n\n#jp-minimap:hover .jp-mm-item { justify-content: flex-start; }\n\n#jp-minimap:hover .jp-mm-label {\n\tdisplay: block;\n\tmax-width: var(--jp-mm-width, 240px);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n/* indent by heading level when expanded */\n#jp-minimap:hover .jp-mm-l2 { padding-left: 16px; }\n#jp-minimap:hover .jp-mm-l3 { padding-left: 28px; }\n#jp-minimap:hover .jp-mm-l4 { padding-left: 40px; }\n#jp-minimap:hover .jp-mm-l5 { padding-left: 52px; }\n#jp-minimap:hover .jp-mm-l6 { padding-left: 52px; }\n\n/* ---- shared states ---- */\n\n.jp-mm-item:hover {\n\topacity: 1;\n\tbackground: rgba(127, 127, 127, 0.22);\n}\n\n.jp-mm-active { opacity: 1; }\n\n#jp-minimap:hover .jp-mm-active {\n\tbackground: rgba(127, 127, 127, 0.18);\n}\n\n/* No scrollbar in the expanded panel: the wheel handler owns scrolling,\n * and a visible scrollbar at the panel edge invites overlay-scrollbar\n * style hover/click interference. */\n.jp-mm-list::-webkit-scrollbar { display: none; }\n.jp-mm-list { scrollbar-width: none; }\n\n/* don't show over printed/exported output */\n@media print {\n\t#jp-minimap { display: none; }\n}\n";
35
+
36
+ function ensureStyle() {
37
+ if (document.getElementById('jp-minimap-style')) return;
38
+ var styleEl = document.createElement('style');
39
+ styleEl.id = 'jp-minimap-style';
40
+ styleEl.textContent = MINIMAP_CSS;
41
+ (document.head || document.documentElement).appendChild(styleEl);
42
+ }
43
+
44
+ // Clicking the minimap gives the webview focus, which can make Joplin
45
+ // re-render the whole note. That detaches every heading element we hold,
46
+ // and scrollIntoView on a detached node is a silent no-op. So jumps are
47
+ // index/text-based against the LIVE DOM, and if a rebuild happens right
48
+ // after a click (the re-render case), the jump is re-applied afterwards.
49
+ var pendingJump = null; // { index, text, until }
50
+
51
+ function liveHeadings() {
52
+ var root = document.getElementById('rendered-md') || document.body;
53
+ return Array.prototype.slice.call(root.querySelectorAll('h1, h2, h3, h4, h5, h6'));
54
+ }
55
+
56
+ function jumpTo(index, text) {
57
+ var hs = liveHeadings();
58
+ var h = hs[index];
59
+ if (!h || (text && (h.textContent || '').trim() !== text)) {
60
+ for (var i = 0; i < hs.length; i++) {
61
+ if ((hs[i].textContent || '').trim() === text) { h = hs[i]; break; }
62
+ }
63
+ }
64
+ if (h && h.isConnected) h.scrollIntoView({ behavior: 'smooth', block: 'start' });
65
+ }
66
+
67
+ // Width of the note viewer's right-edge scrollbar zone. Overlay scrollbars
68
+ // (e.g. Windows 11) reserve no layout space but still intercept clicks with
69
+ // priority over page content, so when nothing is measurable we keep a safety
70
+ // gap anyway. The minimap must stay clear of that zone or clicks on the
71
+ // rightmost part of the panel silently hit the scrollbar instead.
72
+ function scrollbarGap(root) {
73
+ var gap = 0;
74
+ var node = root;
75
+ while (node && node !== document.documentElement) {
76
+ if (node.scrollHeight > node.clientHeight + 1) {
77
+ var w = node.offsetWidth - node.clientWidth;
78
+ if (w > gap) gap = w;
79
+ }
80
+ node = node.parentElement;
81
+ }
82
+ return gap > 0 ? gap : 14;
83
+ }
84
+
28
85
  function build() {
29
86
  if (cleanup) { cleanup(); cleanup = null; }
87
+ ensureStyle();
30
88
 
31
89
  var old = document.getElementById('jp-minimap');
32
90
  if (old) old.remove();
@@ -39,12 +97,18 @@
39
97
 
40
98
  var nav = document.createElement('nav');
41
99
  nav.id = 'jp-minimap';
42
- nav.style.right = settings.rightOffset + 'px';
100
+ // Applied in BOTH collapsed and expanded states: shifting only on hover
101
+ // would move the panel out from under the cursor and cause a
102
+ // hover/unhover flicker loop.
103
+ nav.style.right = (settings.rightOffset + scrollbarGap(root)) + 'px';
43
104
  nav.style.setProperty('--jp-mm-width', settings.panelWidth + 'px');
44
105
  // The viewer DOM can be editable in some contexts; make sure the
45
106
  // minimap never shows a caret or accepts keyboard input.
107
+ // NOTE: no preventDefault on mousedown! Blocking the default mouse-focus
108
+ // path makes Joplin's later programmatic focus count as keyboard-like,
109
+ // and the browser then draws a :focus-visible ring on the scroll
110
+ // container. Natural mouse focus never shows a ring.
46
111
  nav.setAttribute('contenteditable', 'false');
47
- nav.addEventListener('mousedown', function (e) { e.preventDefault(); });
48
112
  // Own the wheel entirely while the cursor is over the minimap:
49
113
  // scroll the ToC list ourselves and never let the event chain
50
114
  // through to the note underneath (scroll chaining feels erratic).
@@ -56,7 +120,7 @@
56
120
  var list = document.createElement('div');
57
121
  list.className = 'jp-mm-list';
58
122
 
59
- var items = headings.map(function (h) {
123
+ var items = headings.map(function (h, index) {
60
124
  var level = Number(h.tagName.charAt(1));
61
125
 
62
126
  // NOT an <a>: Joplin's viewer shows a "Ctrl+click to open" tooltip
@@ -74,8 +138,17 @@
74
138
  item.appendChild(bar);
75
139
  item.appendChild(label);
76
140
 
77
- item.addEventListener('click', function () {
78
- h.scrollIntoView({ behavior: 'smooth', block: 'start' });
141
+ // MOUSEDOWN, not click: the press gives the webview focus, Joplin may
142
+ // re-render the note, and our panel gets rebuilt BETWEEN mousedown and
143
+ // mouseup - so the click event (which needs the same target for both)
144
+ // never fires. mousedown runs before any of that can happen.
145
+ item.addEventListener('mousedown', function (e) {
146
+ if (e.button !== 0) return;
147
+ var text = (label.textContent || '').trim();
148
+ // If the press triggers a note re-render, the rebuild will
149
+ // re-apply this jump against the fresh DOM.
150
+ pendingJump = { index: index, text: text, until: Date.now() + 1200 };
151
+ jumpTo(index, text);
79
152
  });
80
153
 
81
154
  list.appendChild(item);
@@ -98,6 +171,13 @@
98
171
  document.addEventListener('scroll', updateActive, { passive: true, capture: true });
99
172
  updateActive();
100
173
 
174
+ // A rebuild arriving right after a click means the note was re-rendered
175
+ // and the original scrollIntoView hit a detached node - redo the jump.
176
+ if (pendingJump && Date.now() < pendingJump.until) {
177
+ jumpTo(pendingJump.index, pendingJump.text);
178
+ pendingJump = null;
179
+ }
180
+
101
181
  cleanup = function () {
102
182
  document.removeEventListener('scroll', updateActive, { capture: true });
103
183
  };
@@ -10,8 +10,10 @@ module.exports = {
10
10
  // intentionally empty — rendering is untouched
11
11
  },
12
12
  assets: function () {
13
+ // CSS is injected by minimap-view.js itself (see ensureStyle) so
14
+ // that a re-render which drops asset stylesheets cannot leave an
15
+ // unstyled nav in the document flow.
13
16
  return [
14
- { name: 'minimap.css' },
15
17
  { name: 'minimap-view.js' },
16
18
  ];
17
19
  },
Binary file
@@ -1,116 +0,0 @@
1
- /* Joplin Minimap — collapsed tick bars, hover-expanded ToC panel.
2
- * Colors use currentColor / rgba so it follows both light and dark themes.
3
- */
4
-
5
- #jp-minimap {
6
- user-select: none;
7
- -webkit-user-select: none;
8
- caret-color: transparent;
9
- cursor: default;
10
- position: fixed;
11
- top: 50%;
12
- right: 6px;
13
- transform: translateY(-50%);
14
- z-index: 9999;
15
- font-size: 12.5px;
16
- line-height: 1.35;
17
- color: inherit;
18
- }
19
-
20
- .jp-mm-list {
21
- display: flex;
22
- flex-direction: column;
23
- align-items: flex-end;
24
- padding: 8px 6px;
25
- max-height: 84vh;
26
- overflow: hidden;
27
- border-radius: 10px;
28
- transition: background 0.15s ease, box-shadow 0.15s ease;
29
- }
30
-
31
- .jp-mm-item {
32
- display: flex;
33
- align-items: center;
34
- justify-content: flex-end;
35
- padding: 3px 4px;
36
- border-radius: 6px;
37
- text-decoration: none;
38
- color: inherit;
39
- opacity: 0.5;
40
- cursor: pointer;
41
- outline: none;
42
- }
43
-
44
- /* ---- collapsed state: tick bars, width by heading level ---- */
45
-
46
- .jp-mm-bar {
47
- display: block;
48
- height: 2px;
49
- border-radius: 1px;
50
- background: currentColor;
51
- }
52
-
53
- .jp-mm-l1 .jp-mm-bar { width: 18px; }
54
- .jp-mm-l2 .jp-mm-bar { width: 13px; }
55
- .jp-mm-l3 .jp-mm-bar { width: 9px; }
56
- .jp-mm-l4 .jp-mm-bar { width: 7px; }
57
- .jp-mm-l5 .jp-mm-bar { width: 5px; }
58
- .jp-mm-l6 .jp-mm-bar { width: 5px; }
59
-
60
- .jp-mm-label { display: none; }
61
-
62
- /* ---- expanded state (hover) ---- */
63
-
64
- #jp-minimap:hover .jp-mm-list {
65
- align-items: stretch;
66
- overflow-y: auto;
67
- overscroll-behavior: contain;
68
- background: rgba(127, 127, 127, 0.16);
69
- backdrop-filter: blur(10px);
70
- -webkit-backdrop-filter: blur(10px);
71
- box-shadow: 0 6px 28px rgba(0, 0, 0, 0.28);
72
- }
73
-
74
- #jp-minimap:hover .jp-mm-bar { display: none; }
75
-
76
- #jp-minimap:hover .jp-mm-item { justify-content: flex-start; }
77
-
78
- #jp-minimap:hover .jp-mm-label {
79
- display: block;
80
- max-width: var(--jp-mm-width, 240px);
81
- white-space: nowrap;
82
- overflow: hidden;
83
- text-overflow: ellipsis;
84
- }
85
-
86
- /* indent by heading level when expanded */
87
- #jp-minimap:hover .jp-mm-l2 { padding-left: 16px; }
88
- #jp-minimap:hover .jp-mm-l3 { padding-left: 28px; }
89
- #jp-minimap:hover .jp-mm-l4 { padding-left: 40px; }
90
- #jp-minimap:hover .jp-mm-l5 { padding-left: 52px; }
91
- #jp-minimap:hover .jp-mm-l6 { padding-left: 52px; }
92
-
93
- /* ---- shared states ---- */
94
-
95
- .jp-mm-item:hover {
96
- opacity: 1;
97
- background: rgba(127, 127, 127, 0.22);
98
- }
99
-
100
- .jp-mm-active { opacity: 1; }
101
-
102
- #jp-minimap:hover .jp-mm-active {
103
- background: rgba(127, 127, 127, 0.18);
104
- }
105
-
106
- /* slim scrollbar in the expanded panel */
107
- .jp-mm-list::-webkit-scrollbar { width: 4px; }
108
- .jp-mm-list::-webkit-scrollbar-thumb {
109
- background: rgba(127, 127, 127, 0.4);
110
- border-radius: 2px;
111
- }
112
-
113
- /* don't show over printed/exported output */
114
- @media print {
115
- #jp-minimap { display: none; }
116
- }