joplin-plugin-minimap 1.1.8 → 1.1.10

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 lim0513
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lim0513
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,69 +1,71 @@
1
- # Joplin Minimap
2
-
3
- A hover-to-expand table of contents minimap for the [Joplin](https://joplinapp.org/) note viewer — heading tick marks on the right edge that expand into a clickable outline.
4
-
5
- [中文说明](README-CN.md) | [日本語](README-JA.md)
6
-
7
- ## Features
8
-
9
- - **Tick Bar Minimap** — Each heading is shown as a small horizontal bar on the right edge of the note viewer; bar length reflects the heading level (H1 longest)
10
- - **Hover to Expand** — Move the mouse over the bars and they expand into a full table of contents overlay, indented by level, with long titles ellipsized
11
- - **Click to Jump** — Click any entry to smooth-scroll to that heading
12
- - **Reading Position** — The section currently in view is highlighted automatically
13
- - **Live Rebuild** — Rebuilds itself when you switch notes or edit content (listens to `joplin-noteDidUpdate` with a MutationObserver fallback)
14
- - **Theme Aware** — Follows light and dark themes automatically (colors derive from `currentColor`)
15
- - **Unobtrusive** — Hidden for notes with fewer than 2 headings, hidden when printing/exporting, never steals focus or keyboard input
16
-
17
- ## Install
18
-
19
- ### From the Joplin plugin repository (once published)
20
-
21
- 1. In Joplin, go to **Tools → Options → Plugins**
22
- 2. Search for **Joplin Minimap**
23
- 3. Install and restart Joplin
24
-
25
- ### From file
26
-
27
- 1. Download `plugin.jpl` from the [latest release](https://github.com/lim0513/joplin-minimap/releases/latest)
28
- 2. In Joplin, go to **Tools → Options → Plugins**
29
- 3. Click the gear icon and select **Install from file**
30
- 4. Choose the downloaded `.jpl` file
31
- 5. Restart Joplin
32
-
33
- ## Usage
34
-
35
- Open any note with 2 or more headings in the **rendered Markdown viewer** (reading view, or the preview side of the split editor):
36
-
37
- - A column of tick bars appears at the right edge
38
- - **Hover** over it to expand the full table of contents
39
- - **Click** an entry to jump to that heading
40
-
41
- Note: the minimap only appears in the rendered viewer, not in the plain Markdown editor or the Rich Text editor.
42
-
43
- ## How It Works
44
-
45
- A markdown-it content script injects two assets into the rendered viewer: `minimap-view.js` builds the minimap dynamically from the rendered `h1`–`h6` elements after each render, and `minimap.css` handles the collapsed/expanded styling. The Markdown rendering itself is untouched.
46
-
47
- ## Settings
48
-
49
- Configurable in **Tools → Options → Joplin Minimap**:
50
-
51
- - **Minimum headings** — hide the minimap when the note has fewer headings than this (default 2)
52
- - **Expanded panel width (px)** — maximum width of the hover-expanded table of contents (default 240)
53
- - **Distance from right edge (px)** — gap between the minimap and the viewer's right edge (default 6)
54
-
55
- Changed settings apply on the next render (switch notes or edit the note).
56
-
57
- ## Development
58
-
59
- Zero dependencies — no `npm install` needed:
60
-
61
- ```bash
62
- npm run dist
63
- ```
64
-
65
- This builds `dist/` (loadable via Joplin's **Development plugins** setting pointed at the project root) and `publish/` including `publish/plugin.jpl`.
66
-
67
- ## License
68
-
69
- MIT
1
+ # Joplin Minimap
2
+
3
+ A hover-to-expand table of contents minimap for the [Joplin](https://joplinapp.org/) note viewer — heading tick marks on the right edge that expand into a clickable outline.
4
+
5
+ [中文说明](README-CN.md) | [日本語](README-JA.md)
6
+
7
+ ## Features
8
+
9
+ - **Tick Bar Minimap** — Each heading is shown as a small horizontal bar on the right edge of the note viewer; bar length reflects the heading level (H1 longest)
10
+ - **Hover to Expand** — Move the mouse over the bars and they expand into a full table of contents overlay, indented by level, with long titles ellipsized
11
+ - **Click to Jump** — Click any entry to smooth-scroll to that heading
12
+ - **Reading Position** — The section currently in view is highlighted automatically
13
+ - **Live Rebuild** — Rebuilds itself when you switch notes or edit content (listens to `joplin-noteDidUpdate` with a MutationObserver fallback)
14
+ - **RTL Aware** — Headings in right-to-left scripts (Persian, Arabic, Hebrew) are right-aligned in the expanded panel and indent inward from the right; direction is detected per heading, so mixed notes stay readable
15
+ - **Theme Aware** — Follows light and dark themes automatically (colors derive from `currentColor`)
16
+ - **Unobtrusive** — Hidden for notes with fewer than 2 headings, hidden when printing/exporting, never steals focus or keyboard input
17
+
18
+ ## Install
19
+
20
+ ### From the Joplin plugin repository (once published)
21
+
22
+ 1. In Joplin, go to **Tools → Options → Plugins**
23
+ 2. Search for **Joplin Minimap**
24
+ 3. Install and restart Joplin
25
+
26
+ ### From file
27
+
28
+ 1. Download `plugin.jpl` from the [latest release](https://github.com/lim0513/joplin-minimap/releases/latest)
29
+ 2. In Joplin, go to **Tools → Options → Plugins**
30
+ 3. Click the gear icon and select **Install from file**
31
+ 4. Choose the downloaded `.jpl` file
32
+ 5. Restart Joplin
33
+
34
+ ## Usage
35
+
36
+ Open any note with 2 or more headings in the **rendered Markdown viewer** (reading view, or the preview side of the split editor):
37
+
38
+ - A column of tick bars appears at the right edge
39
+ - **Hover** over it to expand the full table of contents
40
+ - **Click** an entry to jump to that heading
41
+
42
+ Note: the minimap only appears in the rendered viewer, not in the plain Markdown editor or the Rich Text editor.
43
+
44
+ ## How It Works
45
+
46
+ A markdown-it content script injects two assets into the rendered viewer: `minimap-view.js` builds the minimap dynamically from the rendered `h1`–`h6` elements after each render, and `minimap.css` handles the collapsed/expanded styling. The Markdown rendering itself is untouched.
47
+
48
+ ## Settings
49
+
50
+ Configurable in **Tools → Options → Joplin Minimap**:
51
+
52
+ - **Minimum headings** — hide the minimap when the note has fewer headings than this (default 2)
53
+ - **Expanded panel width (px)** — maximum width of the hover-expanded table of contents (default 240)
54
+ - **Minimap side** — which edge of the viewer the minimap docks to, right or left (default right)
55
+ - **Edge distance (px)** — gap between the minimap and the viewer edge it sits on (default 6)
56
+
57
+ Changed settings apply on the next render (switch notes or edit the note).
58
+
59
+ ## Development
60
+
61
+ Zero dependencies — no `npm install` needed:
62
+
63
+ ```bash
64
+ npm run dist
65
+ ```
66
+
67
+ This builds `dist/` (loadable via Joplin's **Development plugins** setting pointed at the project root) and `publish/` including `publish/plugin.jpl`.
68
+
69
+ ## License
70
+
71
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joplin-plugin-minimap",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
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",
package/publish/index.js CHANGED
@@ -1,145 +1,182 @@
1
- /* Joplin Minimap — plugin entry (runs in the plugin host, Node context).
2
- * Registers the settings UI and a markdown-it content script whose only job
3
- * is to inject the minimap assets (JS + CSS) into the rendered note viewer.
4
- * The viewer asset fetches settings via webviewApi.postMessage -> onMessage.
5
- */
6
-
7
- // SettingItemType numeric values: Int=1, String=2, Bool=3 (no 'api' import in plain JS)
8
- const TYPE_INT = 1;
9
- const TYPE_BOOL = 3;
10
-
11
- // Settings-screen strings, resolved from the app locale at registration time
12
- // (matching Joplin's own restart-on-language-switch behavior).
13
- const SETTINGS_I18N = {
14
- en_US: {
15
- minHeadings: 'Minimum headings',
16
- minHeadingsDesc: 'Hide the minimap when the note has fewer headings than this. Default: 2.',
17
- panelWidth: 'Expanded panel width (px)',
18
- panelWidthDesc: 'Maximum width of the hover-expanded table of contents. Default: 240.',
19
- rightOffset: 'Distance from right edge (px)',
20
- rightOffsetDesc: 'Gap between the minimap and the right edge of the viewer. Default: 6.',
21
- showTodos: 'Show to-do markers',
22
- showTodosDesc: 'Show a small red dot before a section\'s tick bar when it contains open to-dos. Default: on.',
23
- },
24
- zh_CN: {
25
- minHeadings: '最少标题数',
26
- minHeadingsDesc: '笔记标题数少于此值时隐藏小地图。默认 2。',
27
- panelWidth: '展开面板宽度(px)',
28
- panelWidthDesc: '悬停展开的目录面板最大宽度。默认 240。',
29
- rightOffset: '距右边缘距离(px)',
30
- rightOffsetDesc: '小地图与阅读器右边缘的间距。默认 6。',
31
- showTodos: '显示待办标记',
32
- showTodosDesc: '当章节内含未完成待办时,在该章节横线前显示一个小红点提醒。默认开启。',
33
- },
34
- zh_TW: {
35
- minHeadings: '最少標題數',
36
- minHeadingsDesc: '筆記標題數少於此值時隱藏小地圖。預設 2。',
37
- panelWidth: '展開面板寬度(px)',
38
- panelWidthDesc: '懸停展開的目錄面板最大寬度。預設 240。',
39
- rightOffset: '距右邊緣距離(px)',
40
- rightOffsetDesc: '小地圖與檢視器右邊緣的間距。預設 6。',
41
- showTodos: '顯示待辦標記',
42
- showTodosDesc: '當章節內含未完成待辦時,在該章節橫線前顯示一個小紅點提醒。預設開啟。',
43
- },
44
- ru: {
45
- minHeadings: 'Минимум заголовков',
46
- minHeadingsDesc: 'Скрывать миникарту, если заголовков в заметке меньше. По умолчанию: 2.',
47
- panelWidth: 'Ширина развёрнутой панели (px)',
48
- panelWidthDesc: 'Максимальная ширина оглавления при наведении. По умолчанию: 240.',
49
- rightOffset: 'Отступ от правого края (px)',
50
- rightOffsetDesc: 'Зазор между миникартой и правым краем просмотра. По умолчанию: 6.',
51
- showTodos: 'Показывать метки задач',
52
- showTodosDesc: 'Показывать маленькую красную точку перед линией раздела, если в нём есть открытые задачи. По умолчанию: вкл.',
53
- },
54
- ja_JP: {
55
- minHeadings: '最小見出し数',
56
- minHeadingsDesc: 'ノートの見出しがこの数より少ない場合はミニマップを隠します。既定値:2。',
57
- panelWidth: '展開パネルの幅(px)',
58
- panelWidthDesc: 'ホバーで展開する目次の最大幅。既定値:240。',
59
- rightOffset: '右端からの距離(px)',
60
- rightOffsetDesc: 'ミニマップとビューアー右端の間隔。既定値:6。',
61
- showTodos: 'ToDoマーカーを表示',
62
- showTodosDesc: '未完了のToDoを含むセクションの線の前に小さな赤い点を表示します。既定値:オン。',
63
- },
64
- };
65
-
66
- function settingsI18n(locale) {
67
- if (SETTINGS_I18N[locale]) return SETTINGS_I18N[locale];
68
- const lang = String(locale || '').split('_')[0];
69
- if (lang === 'zh') return SETTINGS_I18N.zh_CN;
70
- if (lang === 'ru') return SETTINGS_I18N.ru;
71
- if (lang === 'ja') return SETTINGS_I18N.ja_JP;
72
- return SETTINGS_I18N.en_US;
73
- }
74
-
75
- joplin.plugins.register({
76
- onStart: async function () {
77
- const locale = (await joplin.settings.globalValue('locale')) || 'en_US';
78
- const t = settingsI18n(locale);
79
-
80
- await joplin.settings.registerSection('minimap', {
81
- label: 'Joplin Minimap',
82
- iconName: 'fas fa-list',
83
- });
84
-
85
- try {
86
- await joplin.settings.registerSettings({
87
- 'minimapMinHeadings': {
88
- value: 2,
89
- minimum: 1,
90
- type: TYPE_INT,
91
- section: 'minimap',
92
- public: true,
93
- label: t.minHeadings,
94
- description: t.minHeadingsDesc,
95
- },
96
- 'minimapPanelWidth': {
97
- value: 240,
98
- minimum: 120,
99
- type: TYPE_INT,
100
- section: 'minimap',
101
- public: true,
102
- label: t.panelWidth,
103
- description: t.panelWidthDesc,
104
- },
105
- 'minimapRightOffset': {
106
- value: 6,
107
- minimum: 0,
108
- type: TYPE_INT,
109
- section: 'minimap',
110
- public: true,
111
- label: t.rightOffset,
112
- description: t.rightOffsetDesc,
113
- },
114
- 'minimapShowTodos': {
115
- value: true,
116
- type: TYPE_BOOL,
117
- section: 'minimap',
118
- public: true,
119
- label: t.showTodos,
120
- description: t.showTodosDesc,
121
- },
122
- });
123
- } catch (error) {
124
- console.error('Joplin Minimap: registerSettings failed:', error);
125
- }
126
-
127
- await joplin.contentScripts.register(
128
- 'markdownItPlugin',
129
- 'joplin-minimap',
130
- './minimapContentScript.js'
131
- );
132
-
133
- await joplin.contentScripts.onMessage('joplin-minimap', async function (message) {
134
- if (message === 'getSettings') {
135
- return {
136
- minHeadings: await joplin.settings.value('minimapMinHeadings'),
137
- panelWidth: await joplin.settings.value('minimapPanelWidth'),
138
- rightOffset: await joplin.settings.value('minimapRightOffset'),
139
- showTodos: await joplin.settings.value('minimapShowTodos'),
140
- };
141
- }
142
- return null;
143
- });
144
- },
145
- });
1
+ /* Joplin Minimap — plugin entry (runs in the plugin host, Node context).
2
+ * Registers the settings UI and a markdown-it content script whose only job
3
+ * is to inject the minimap assets (JS + CSS) into the rendered note viewer.
4
+ * The viewer asset fetches settings via webviewApi.postMessage -> onMessage.
5
+ */
6
+
7
+ // SettingItemType numeric values: Int=1, String=2, Bool=3 (no 'api' import in plain JS)
8
+ const TYPE_INT = 1;
9
+ const TYPE_STRING = 2;
10
+ const TYPE_BOOL = 3;
11
+
12
+ // Settings-screen strings, resolved from the app locale at registration time
13
+ // (matching Joplin's own restart-on-language-switch behavior).
14
+ const SETTINGS_I18N = {
15
+ en_US: {
16
+ minHeadings: 'Minimum headings',
17
+ minHeadingsDesc: 'Hide the minimap when the note has fewer headings than this. Default: 2.',
18
+ panelWidth: 'Expanded panel width (px)',
19
+ panelWidthDesc: 'Maximum width of the hover-expanded table of contents. Default: 240.',
20
+ edgeOffset: 'Edge distance (px)',
21
+ edgeOffsetDesc: 'Gap between the minimap and the viewer edge it sits on. Default: 6.',
22
+ side: 'Minimap side',
23
+ sideDesc: 'Which edge of the viewer the minimap sits on. Default: right.',
24
+ sideRight: 'Right',
25
+ sideLeft: 'Left',
26
+ showTodos: 'Show to-do markers',
27
+ showTodosDesc: 'Show a small red dot before a section\'s tick bar when it contains open to-dos. Default: on.',
28
+ },
29
+ zh_CN: {
30
+ minHeadings: '最少标题数',
31
+ minHeadingsDesc: '笔记标题数少于此值时隐藏小地图。默认 2。',
32
+ panelWidth: '展开面板宽度(px)',
33
+ panelWidthDesc: '悬停展开的目录面板最大宽度。默认 240。',
34
+ edgeOffset: '边缘距离(px)',
35
+ edgeOffsetDesc: '小地图与阅读器边缘的间距。默认 6。',
36
+ side: '小地图位置',
37
+ sideDesc: '小地图停靠在阅读器的哪一侧边缘。默认靠右。',
38
+ sideRight: '右侧',
39
+ sideLeft: '左侧',
40
+ showTodos: '显示待办标记',
41
+ showTodosDesc: '当章节内含未完成待办时,在该章节横线前显示一个小红点提醒。默认开启。',
42
+ },
43
+ zh_TW: {
44
+ minHeadings: '最少標題數',
45
+ minHeadingsDesc: '筆記標題數少於此值時隱藏小地圖。預設 2。',
46
+ panelWidth: '展開面板寬度(px)',
47
+ panelWidthDesc: '懸停展開的目錄面板最大寬度。預設 240。',
48
+ edgeOffset: '邊緣距離(px)',
49
+ edgeOffsetDesc: '小地圖與檢視器邊緣的間距。預設 6。',
50
+ side: '小地圖位置',
51
+ sideDesc: '小地圖停靠在檢視器的哪一側邊緣。預設靠右。',
52
+ sideRight: '右側',
53
+ sideLeft: '左側',
54
+ showTodos: '顯示待辦標記',
55
+ showTodosDesc: '當章節內含未完成待辦時,在該章節橫線前顯示一個小紅點提醒。預設開啟。',
56
+ },
57
+ ru: {
58
+ minHeadings: 'Минимум заголовков',
59
+ minHeadingsDesc: 'Скрывать миникарту, если заголовков в заметке меньше. По умолчанию: 2.',
60
+ panelWidth: 'Ширина развёрнутой панели (px)',
61
+ panelWidthDesc: 'Максимальная ширина оглавления при наведении. По умолчанию: 240.',
62
+ edgeOffset: 'Отступ от края (px)',
63
+ edgeOffsetDesc: 'Зазор между миникартой и краем просмотра. По умолчанию: 6.',
64
+ side: 'Сторона миникарты',
65
+ sideDesc: 'У какого края области просмотра располагается миникарта. По умолчанию: справа.',
66
+ sideRight: 'Справа',
67
+ sideLeft: 'Слева',
68
+ showTodos: 'Показывать метки задач',
69
+ showTodosDesc: 'Показывать маленькую красную точку перед линией раздела, если в нём есть открытые задачи. По умолчанию: вкл.',
70
+ },
71
+ ja_JP: {
72
+ minHeadings: '最小見出し数',
73
+ minHeadingsDesc: 'ノートの見出しがこの数より少ない場合はミニマップを隠します。既定値:2。',
74
+ panelWidth: '展開パネルの幅(px)',
75
+ panelWidthDesc: 'ホバーで展開する目次の最大幅。既定値:240。',
76
+ edgeOffset: '端からの距離(px)',
77
+ edgeOffsetDesc: 'ミニマップとビューアー端の間隔。既定値:6。',
78
+ side: 'ミニマップの位置',
79
+ sideDesc: 'ミニマップをビューアーのどちら側の端に表示するか。既定値:右。',
80
+ sideRight: '右',
81
+ sideLeft: '左',
82
+ showTodos: 'ToDoマーカーを表示',
83
+ showTodosDesc: '未完了のToDoを含むセクションの線の前に小さな赤い点を表示します。既定値:オン。',
84
+ },
85
+ };
86
+
87
+ function settingsI18n(locale) {
88
+ if (SETTINGS_I18N[locale]) return SETTINGS_I18N[locale];
89
+ const lang = String(locale || '').split('_')[0];
90
+ if (lang === 'zh') return SETTINGS_I18N.zh_CN;
91
+ if (lang === 'ru') return SETTINGS_I18N.ru;
92
+ if (lang === 'ja') return SETTINGS_I18N.ja_JP;
93
+ return SETTINGS_I18N.en_US;
94
+ }
95
+
96
+ joplin.plugins.register({
97
+ onStart: async function () {
98
+ const locale = (await joplin.settings.globalValue('locale')) || 'en_US';
99
+ const t = settingsI18n(locale);
100
+
101
+ await joplin.settings.registerSection('minimap', {
102
+ label: 'Joplin Minimap',
103
+ iconName: 'fas fa-list',
104
+ });
105
+
106
+ try {
107
+ await joplin.settings.registerSettings({
108
+ 'minimapMinHeadings': {
109
+ value: 2,
110
+ minimum: 1,
111
+ type: TYPE_INT,
112
+ section: 'minimap',
113
+ public: true,
114
+ label: t.minHeadings,
115
+ description: t.minHeadingsDesc,
116
+ },
117
+ 'minimapPanelWidth': {
118
+ value: 240,
119
+ minimum: 120,
120
+ type: TYPE_INT,
121
+ section: 'minimap',
122
+ public: true,
123
+ label: t.panelWidth,
124
+ description: t.panelWidthDesc,
125
+ },
126
+ // Stored key stays 'minimapRightOffset' even though the label is now
127
+ // side-neutral: renaming a setting key orphans the saved value.
128
+ 'minimapRightOffset': {
129
+ value: 6,
130
+ minimum: 0,
131
+ type: TYPE_INT,
132
+ section: 'minimap',
133
+ public: true,
134
+ label: t.edgeOffset,
135
+ description: t.edgeOffsetDesc,
136
+ },
137
+ // Enum rather than a bool: 'Left'/'Right' reads unambiguously in the
138
+ // settings screen, and leaves room for a future 'auto' that follows
139
+ // the note's own text direction.
140
+ 'minimapSide': {
141
+ value: 'right',
142
+ type: TYPE_STRING,
143
+ isEnum: true,
144
+ options: { right: t.sideRight, left: t.sideLeft },
145
+ section: 'minimap',
146
+ public: true,
147
+ label: t.side,
148
+ description: t.sideDesc,
149
+ },
150
+ 'minimapShowTodos': {
151
+ value: true,
152
+ type: TYPE_BOOL,
153
+ section: 'minimap',
154
+ public: true,
155
+ label: t.showTodos,
156
+ description: t.showTodosDesc,
157
+ },
158
+ });
159
+ } catch (error) {
160
+ console.error('Joplin Minimap: registerSettings failed:', error);
161
+ }
162
+
163
+ await joplin.contentScripts.register(
164
+ 'markdownItPlugin',
165
+ 'joplin-minimap',
166
+ './minimapContentScript.js'
167
+ );
168
+
169
+ await joplin.contentScripts.onMessage('joplin-minimap', async function (message) {
170
+ if (message === 'getSettings') {
171
+ return {
172
+ minHeadings: await joplin.settings.value('minimapMinHeadings'),
173
+ panelWidth: await joplin.settings.value('minimapPanelWidth'),
174
+ rightOffset: await joplin.settings.value('minimapRightOffset'),
175
+ side: await joplin.settings.value('minimapSide'),
176
+ showTodos: await joplin.settings.value('minimapShowTodos'),
177
+ };
178
+ }
179
+ return null;
180
+ });
181
+ },
182
+ });
@@ -2,12 +2,26 @@
2
2
  "manifest_version": 1,
3
3
  "id": "com.github.joplin-minimap",
4
4
  "app_min_version": "2.8",
5
- "version": "1.1.8",
5
+ "version": "1.1.10",
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",
9
9
  "homepage_url": "https://github.com/lim0513/joplin-minimap",
10
10
  "repository_url": "https://github.com/lim0513/joplin-minimap",
11
- "keywords": ["toc", "outline", "minimap", "table-of-contents"],
12
- "categories": ["appearance", "viewer"]
11
+ "keywords": [
12
+ "toc",
13
+ "outline",
14
+ "minimap",
15
+ "table-of-contents"
16
+ ],
17
+ "categories": [
18
+ "appearance",
19
+ "viewer"
20
+ ],
21
+ "icons": {
22
+ "16": "assets/icons/icon16.png",
23
+ "32": "assets/icons/icon32.png",
24
+ "48": "assets/icons/icon48.png",
25
+ "128": "assets/icons/icon128.png"
26
+ }
13
27
  }
@@ -1,283 +1,325 @@
1
- /* Joplin Minimap - runs inside the rendered note viewer.
2
- * Collects h1-h6 from the rendered DOM, draws collapsed tick bars on the
3
- * right edge, expands into a full ToC on hover, and jumps on click.
4
- * Rebuilds itself whenever the note content changes (note switch, edit).
5
- */
6
- (function () {
7
- 'use strict';
8
-
9
- // Defaults; overridden from Joplin's plugin settings (Tools > Options > Minimap)
10
- // when webviewApi is available in this webview.
11
- var settings = { minHeadings: 2, panelWidth: 240, rightOffset: 6, showTodos: true };
12
-
13
- function loadSettings() {
14
- if (typeof webviewApi === 'undefined' || !webviewApi.postMessage) {
15
- return Promise.resolve();
16
- }
17
- return webviewApi.postMessage('joplin-minimap', 'getSettings').then(function (s) {
18
- if (s && typeof s.minHeadings === 'number') settings = s;
19
- }).catch(function () { /* keep defaults (e.g. print/export context) */ });
20
- }
21
-
22
- // NEVER run inside an editable context (the Rich Text editor renders
23
- // notes through the same pipeline and would execute this asset). If the
24
- // nav lands in the RTE document, Joplin's HTML->markdown round-trip
25
- // SERIALIZES it into the note body on save - the heading list becomes
26
- // real note content and syncs to every device. This was the true root
27
- // cause of the "outline below the document" reports.
28
- function isEditableContext() {
29
- var b = document.body;
30
- if (!b) return false;
31
- if (b.isContentEditable) return true;
32
- if (b.id === 'tinymce') return true;
33
- if (b.classList && b.classList.contains('mce-content-body')) return true;
34
- return false;
35
- }
36
- if (isEditableContext()) return;
37
-
38
- // Only ever install one instance of the watcher per webview session.
39
- if (window.__jpMinimapInstalled) return;
40
- window.__jpMinimapInstalled = true;
41
-
42
- var cleanup = null; // removes listeners belonging to the current build
43
-
44
- // The stylesheet is injected BY THIS SCRIPT and re-checked on every build.
45
- // Rationale: Joplin can re-render the document in ways that drop injected
46
- // asset stylesheets while this script's watcher survives - the rebuilt nav
47
- // then renders UNSTYLED as flow content below the note (looks like a
48
- // duplicated outline under the document). Keeping the CSS inline and
49
- // re-injecting guarantees the nav and its styling live and die together.
50
- 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/* ---- open-to-do section dot: one muted red dot before the tick bar of\n * any section that contains at least one unchecked checkbox ---- */\n.jp-mm-dot {\n\tdisplay: block;\n\twidth: 4px;\n\theight: 4px;\n\tborder-radius: 50%;\n\tbackground: rgba(205, 97, 85, 0.85);\n\tmargin-right: 5px;\n\tflex: none;\n}\n\n/* don't show over printed/exported output */\n@media print {\n\t#jp-minimap { display: none; }\n}\n";
51
-
52
- function ensureStyle() {
53
- if (document.getElementById('jp-minimap-style')) return;
54
- var styleEl = document.createElement('style');
55
- styleEl.id = 'jp-minimap-style';
56
- styleEl.textContent = MINIMAP_CSS;
57
- (document.head || document.documentElement).appendChild(styleEl);
58
- }
59
-
60
- // Clicking the minimap gives the webview focus, which can make Joplin
61
- // re-render the whole note. That detaches every heading element we hold,
62
- // and scrollIntoView on a detached node is a silent no-op. So jumps are
63
- // index/text-based against the LIVE DOM, and if a rebuild happens right
64
- // after a click (the re-render case), the jump is re-applied afterwards.
65
- var pendingJump = null; // { index, text, until }
66
-
67
- function liveHeadings() {
68
- var root = document.getElementById('rendered-md') || document.body;
69
- return Array.prototype.slice.call(root.querySelectorAll('h1, h2, h3, h4, h5, h6'));
70
- }
71
-
72
- function jumpTo(index, text) {
73
- var hs = liveHeadings();
74
- var h = hs[index];
75
- if (!h || (text && (h.textContent || '').trim() !== text)) {
76
- for (var i = 0; i < hs.length; i++) {
77
- if ((hs[i].textContent || '').trim() === text) { h = hs[i]; break; }
78
- }
79
- }
80
- if (h && h.isConnected) h.scrollIntoView({ behavior: 'smooth', block: 'start' });
81
- }
82
-
83
- // Width of the note viewer's right-edge scrollbar zone. Overlay scrollbars
84
- // (e.g. Windows 11) reserve no layout space but still intercept clicks with
85
- // priority over page content, so when nothing is measurable we keep a safety
86
- // gap anyway. The minimap must stay clear of that zone or clicks on the
87
- // rightmost part of the panel silently hit the scrollbar instead.
88
- function scrollbarGap(root) {
89
- var gap = 0;
90
- var node = root;
91
- while (node && node !== document.documentElement) {
92
- if (node.scrollHeight > node.clientHeight + 1) {
93
- var w = node.offsetWidth - node.clientWidth;
94
- if (w > gap) gap = w;
95
- }
96
- node = node.parentElement;
97
- }
98
- return gap > 0 ? gap : 14;
99
- }
100
-
101
- function build() {
102
- if (cleanup) { cleanup(); cleanup = null; }
103
- if (isEditableContext()) {
104
- var stale = document.getElementById('jp-minimap');
105
- if (stale) stale.remove();
106
- return;
107
- }
108
- ensureStyle();
109
-
110
- var old = document.getElementById('jp-minimap');
111
- if (old) old.remove();
112
-
113
- var root = document.getElementById('rendered-md') || document.body;
114
- var headings = Array.prototype.slice.call(
115
- root.querySelectorAll('h1, h2, h3, h4, h5, h6')
116
- );
117
- if (headings.length < settings.minHeadings) return;
118
-
119
- // Which sections contain at least one OPEN checkbox: walk headings and
120
- // checkboxes in one document-ordered pass, attributing each unchecked
121
- // box to the nearest preceding heading. One dot per section, however
122
- // many open to-dos it holds.
123
- var sectionHasTodo = {};
124
- if (settings.showTodos !== false) {
125
- var walk = Array.prototype.slice.call(
126
- root.querySelectorAll('h1, h2, h3, h4, h5, h6, input[type="checkbox"]')
127
- );
128
- var lastHeading = -1;
129
- for (var wi = 0; wi < walk.length; wi++) {
130
- var wn = walk[wi];
131
- if (/^H[1-6]$/.test(wn.tagName)) lastHeading++;
132
- else if (!wn.checked && lastHeading >= 0) sectionHasTodo[lastHeading] = true;
133
- }
134
- }
135
-
136
- var nav = document.createElement('nav');
137
- nav.id = 'jp-minimap';
138
- // Applied in BOTH collapsed and expanded states: shifting only on hover
139
- // would move the panel out from under the cursor and cause a
140
- // hover/unhover flicker loop.
141
- nav.style.right = (settings.rightOffset + scrollbarGap(root)) + 'px';
142
- nav.style.setProperty('--jp-mm-width', settings.panelWidth + 'px');
143
- // The viewer DOM can be editable in some contexts; make sure the
144
- // minimap never shows a caret or accepts keyboard input.
145
- // NOTE: no preventDefault on mousedown! Blocking the default mouse-focus
146
- // path makes Joplin's later programmatic focus count as keyboard-like,
147
- // and the browser then draws a :focus-visible ring on the scroll
148
- // container. Natural mouse focus never shows a ring.
149
- nav.setAttribute('contenteditable', 'false');
150
- // Own the wheel entirely while the cursor is over the minimap:
151
- // scroll the ToC list ourselves and never let the event chain
152
- // through to the note underneath (scroll chaining feels erratic).
153
- nav.addEventListener('wheel', function (e) {
154
- e.preventDefault();
155
- list.scrollTop += e.deltaY;
156
- }, { passive: false });
157
-
158
- var list = document.createElement('div');
159
- list.className = 'jp-mm-list';
160
-
161
- var items = headings.map(function (h, index) {
162
- var level = Number(h.tagName.charAt(1));
163
-
164
- // NOT an <a>: Joplin's viewer shows a "Ctrl+click to open" tooltip
165
- // on anchors and treats them as external links.
166
- var item = document.createElement('div');
167
- item.className = 'jp-mm-item jp-mm-l' + level;
168
-
169
- var bar = document.createElement('span');
170
- bar.className = 'jp-mm-bar';
171
-
172
- var label = document.createElement('span');
173
- label.className = 'jp-mm-label';
174
- label.textContent = (h.textContent || '').trim();
175
-
176
- // Muted red reminder dot before the bar (and before the label when
177
- // expanded - same element, flex order does the work).
178
- if (sectionHasTodo[index]) {
179
- var dot = document.createElement('span');
180
- dot.className = 'jp-mm-dot';
181
- item.appendChild(dot);
182
- }
183
- item.appendChild(bar);
184
- item.appendChild(label);
185
-
186
- // MOUSEDOWN, not click: the press gives the webview focus, Joplin may
187
- // re-render the note, and our panel gets rebuilt BETWEEN mousedown and
188
- // mouseup - so the click event (which needs the same target for both)
189
- // never fires. mousedown runs before any of that can happen.
190
- item.addEventListener('mousedown', function (e) {
191
- if (e.button !== 0) return;
192
- var text = (label.textContent || '').trim();
193
- // If the press triggers a note re-render, the rebuild will
194
- // re-apply this jump against the fresh DOM.
195
- pendingJump = { index: index, text: text, until: Date.now() + 1200 };
196
- jumpTo(index, text);
197
- });
198
-
199
- list.appendChild(item);
200
- return item;
201
- });
202
-
203
- nav.appendChild(list);
204
- document.body.appendChild(nav);
205
-
206
- function updateActive() {
207
- var activeIndex = 0;
208
- for (var i = 0; i < headings.length; i++) {
209
- if (headings[i].getBoundingClientRect().top <= 90) activeIndex = i;
210
- }
211
- for (var j = 0; j < items.length; j++) {
212
- items[j].classList.toggle('jp-mm-active', j === activeIndex);
213
- }
214
- }
215
-
216
- document.addEventListener('scroll', updateActive, { passive: true, capture: true });
217
- updateActive();
218
-
219
- // A rebuild arriving right after a click means the note was re-rendered
220
- // and the original scrollIntoView hit a detached node - redo the jump.
221
- if (pendingJump && Date.now() < pendingJump.until) {
222
- jumpTo(pendingJump.index, pendingJump.text);
223
- pendingJump = null;
224
- }
225
-
226
- cleanup = function () {
227
- document.removeEventListener('scroll', updateActive, { capture: true });
228
- };
229
- }
230
-
231
- function settingsAndBuild() {
232
- loadSettings().then(build);
233
- }
234
-
235
- var timer = null;
236
- function scheduleBuild() {
237
- clearTimeout(timer);
238
- timer = setTimeout(settingsAndBuild, 150);
239
- }
240
-
241
- // True if a mutation was caused by the minimap itself (avoid rebuild loops).
242
- function isOwnMutation(m) {
243
- if (m.target && m.target.closest && m.target.closest('#jp-minimap')) return true;
244
- var nodes = Array.prototype.slice.call(m.addedNodes)
245
- .concat(Array.prototype.slice.call(m.removedNodes));
246
- if (!nodes.length) return false;
247
- return nodes.every(function (n) {
248
- return n.id === 'jp-minimap' || (n.closest && n.closest('#jp-minimap'));
249
- });
250
- }
251
-
252
- function watch() {
253
- // Joplin fires this after each note render/update.
254
- document.addEventListener('joplin-noteDidUpdate', scheduleBuild);
255
-
256
- // Joplin's asset cleanup can remove our <style> from <head> AFTER the
257
- // last body mutation - nothing rebuilds, and the nav sits unstyled in
258
- // the page (the "outline below the document" bug, second incarnation:
259
- // v1.1.4 only re-checked the style during rebuilds). Watch the head
260
- // and re-inject immediately.
261
- var headObserver = new MutationObserver(function () {
262
- if (!document.getElementById('jp-minimap-style')) ensureStyle();
263
- });
264
- if (document.head) headObserver.observe(document.head, { childList: true });
265
-
266
- // Fallback: watch for the rendered content being swapped out
267
- // (note switch replaces the DOM without re-running this script).
268
- var mo = new MutationObserver(function (mutations) {
269
- for (var i = 0; i < mutations.length; i++) {
270
- if (!isOwnMutation(mutations[i])) { scheduleBuild(); return; }
271
- }
272
- });
273
- mo.observe(document.body, { childList: true, subtree: true });
274
-
275
- settingsAndBuild();
276
- }
277
-
278
- if (document.readyState === 'loading') {
279
- document.addEventListener('DOMContentLoaded', watch);
280
- } else {
281
- watch();
282
- }
283
- })();
1
+ /* Joplin Minimap - runs inside the rendered note viewer.
2
+ * Collects h1-h6 from the rendered DOM, draws collapsed tick bars on the
3
+ * right edge, expands into a full ToC on hover, and jumps on click.
4
+ * Rebuilds itself whenever the note content changes (note switch, edit).
5
+ */
6
+ (function () {
7
+ 'use strict';
8
+
9
+ // Defaults; overridden from Joplin's plugin settings (Tools > Options > Minimap)
10
+ // when webviewApi is available in this webview.
11
+ var settings = { minHeadings: 2, panelWidth: 240, rightOffset: 6, side: 'right', showTodos: true };
12
+
13
+ function loadSettings() {
14
+ if (typeof webviewApi === 'undefined' || !webviewApi.postMessage) {
15
+ return Promise.resolve();
16
+ }
17
+ return webviewApi.postMessage('joplin-minimap', 'getSettings').then(function (s) {
18
+ if (s && typeof s.minHeadings === 'number') settings = s;
19
+ }).catch(function () { /* keep defaults (e.g. print/export context) */ });
20
+ }
21
+
22
+ // NEVER run inside an editable context (the Rich Text editor renders
23
+ // notes through the same pipeline and would execute this asset). If the
24
+ // nav lands in the RTE document, Joplin's HTML->markdown round-trip
25
+ // SERIALIZES it into the note body on save - the heading list becomes
26
+ // real note content and syncs to every device. This was the true root
27
+ // cause of the "outline below the document" reports.
28
+ function isEditableContext() {
29
+ var b = document.body;
30
+ if (!b) return false;
31
+ if (b.isContentEditable) return true;
32
+ if (b.id === 'tinymce') return true;
33
+ if (b.classList && b.classList.contains('mce-content-body')) return true;
34
+ return false;
35
+ }
36
+ if (isEditableContext()) return;
37
+
38
+ // Only ever install one instance of the watcher per webview session.
39
+ if (window.__jpMinimapInstalled) return;
40
+ window.__jpMinimapInstalled = true;
41
+
42
+ var cleanup = null; // removes listeners belonging to the current build
43
+
44
+ // The stylesheet is injected BY THIS SCRIPT and re-checked on every build.
45
+ // Rationale: Joplin can re-render the document in ways that drop injected
46
+ // asset stylesheets while this script's watcher survives - the rebuilt nav
47
+ // then renders UNSTYLED as flow content below the note (looks like a
48
+ // duplicated outline under the document). Keeping the CSS inline and
49
+ // re-injecting guarantees the nav and its styling live and die together.
50
+ 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\t/* Collapsed geometry is direction-independent: the tick bars always\n\t * hug the docked edge, whichever side that is, even in an RTL note. Per-item direction applies\n\t * to the expanded panel only (see the dir=rtl rule below). */\n\tdirection: ltr;\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\tdirection: ltr;\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/* ---- docked on the left edge ---- */\n\n/* Mirror the collapsed alignment so the tick bars hug the left border.\n * direction is pinned to ltr above, so flex-start is unambiguously the left\n * side whatever language the note is in. Placed BEFORE the :hover rules on\n * purpose: they carry the same specificity, so source order decides. */\n#jp-minimap.jp-mm-left .jp-mm-list { align-items: flex-start; }\n#jp-minimap.jp-mm-left .jp-mm-item { justify-content: flex-start; }\n\n/* The to-do dot mirrors too: docked right it hangs left of the tick bar,\n * docked left it must hang RIGHT of it - otherwise a dotted row pushes its\n * bar 9px inward (4px dot + 5px gap) and the flush bar column goes ragged.\n * order= reorders the flex row without touching the DOM, so the expanded\n * panel can put the dot back in front of the label. */\n#jp-minimap.jp-mm-left .jp-mm-dot {\n\torder: 1;\n\tmargin-inline-start: 5px;\n\tmargin-inline-end: 0;\n}\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\ttext-align: start;\n}\n\n/* RTL headings (Persian/Arabic/Hebrew) read right-to-left in the expanded\n * panel: justify-content, padding-inline-start and text-align:start all flip\n * with the row direction, so the label hugs the right edge and nested levels\n * indent inward from the right. build() sets dir per row (first strong char). */\n#jp-minimap:hover .jp-mm-item[dir=\"rtl\"] { direction: rtl; }\n\n/* indent by heading level when expanded (logical: left in LTR, right in RTL) */\n#jp-minimap:hover .jp-mm-l2 { padding-inline-start: 16px; }\n#jp-minimap:hover .jp-mm-l3 { padding-inline-start: 28px; }\n#jp-minimap:hover .jp-mm-l4 { padding-inline-start: 40px; }\n#jp-minimap:hover .jp-mm-l5 { padding-inline-start: 52px; }\n#jp-minimap:hover .jp-mm-l6 { padding-inline-start: 52px; }\n\n/* Expanded, the dot reads as a marker BEFORE the title on either edge. */\n#jp-minimap.jp-mm-left:hover .jp-mm-dot {\n\torder: 0;\n\tmargin-inline-start: 0;\n\tmargin-inline-end: 5px;\n}\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/* ---- open-to-do section dot: one muted red dot before the tick bar of\n * any section that contains at least one unchecked checkbox ---- */\n.jp-mm-dot {\n\tdisplay: block;\n\twidth: 4px;\n\theight: 4px;\n\tborder-radius: 50%;\n\tbackground: rgba(205, 97, 85, 0.85);\n\tmargin-inline-end: 5px;\n\tflex: none;\n}\n\n/* don't show over printed/exported output */\n@media print {\n\t#jp-minimap { display: none; }\n}\n";
51
+
52
+ function ensureStyle() {
53
+ if (document.getElementById('jp-minimap-style')) return;
54
+ var styleEl = document.createElement('style');
55
+ styleEl.id = 'jp-minimap-style';
56
+ styleEl.textContent = MINIMAP_CSS;
57
+ (document.head || document.documentElement).appendChild(styleEl);
58
+ }
59
+
60
+ // Bidi: the panel hangs off the right edge, so RTL headings (Persian,
61
+ // Arabic, Hebrew) must read right-to-left inside it - otherwise their text
62
+ // is forced left and the level indent grows from the wrong side. Resolve
63
+ // each heading the way dir="auto" does: the FIRST strong character wins.
64
+ // Ranges are deliberately coarse but disjoint (RTL blocks 0590-08FF plus the
65
+ // Arabic/Hebrew presentation forms; everything else strong counts as LTR).
66
+ var RTL_STRONG = /[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]/;
67
+ var LTR_STRONG = /[A-Za-z\u00C0-\u058F\u0900-\u1FFF\u2C00-\uD7FF\uF900-\uFAFF]/;
68
+
69
+ function textDirection(text, heading) {
70
+ var s = String(text || '');
71
+ for (var i = 0; i < s.length; i++) {
72
+ var c = s.charAt(i);
73
+ if (RTL_STRONG.test(c)) return 'rtl';
74
+ if (LTR_STRONG.test(c)) return 'ltr';
75
+ }
76
+ // No strong character at all (digits, punctuation, emoji): inherit the
77
+ // direction the rendered heading itself got from the note.
78
+ try {
79
+ return window.getComputedStyle(heading).direction === 'rtl' ? 'rtl' : 'ltr';
80
+ } catch (e) {
81
+ return 'ltr';
82
+ }
83
+ }
84
+
85
+ // Clicking the minimap gives the webview focus, which can make Joplin
86
+ // re-render the whole note. That detaches every heading element we hold,
87
+ // and scrollIntoView on a detached node is a silent no-op. So jumps are
88
+ // index/text-based against the LIVE DOM, and if a rebuild happens right
89
+ // after a click (the re-render case), the jump is re-applied afterwards.
90
+ var pendingJump = null; // { index, text, until }
91
+
92
+ function liveHeadings() {
93
+ var root = document.getElementById('rendered-md') || document.body;
94
+ return Array.prototype.slice.call(root.querySelectorAll('h1, h2, h3, h4, h5, h6'));
95
+ }
96
+
97
+ function jumpTo(index, text) {
98
+ var hs = liveHeadings();
99
+ var h = hs[index];
100
+ if (!h || (text && (h.textContent || '').trim() !== text)) {
101
+ for (var i = 0; i < hs.length; i++) {
102
+ if ((hs[i].textContent || '').trim() === text) { h = hs[i]; break; }
103
+ }
104
+ }
105
+ if (h && h.isConnected) h.scrollIntoView({ behavior: 'smooth', block: 'start' });
106
+ }
107
+
108
+ // Width of the note viewer's right-edge scrollbar zone. Overlay scrollbars
109
+ // (e.g. Windows 11) reserve no layout space but still intercept clicks with
110
+ // priority over page content, so when nothing is measurable we keep a safety
111
+ // gap anyway. The minimap must stay clear of that zone or clicks on the
112
+ // rightmost part of the panel silently hit the scrollbar instead.
113
+ function scrollbarGap(root) {
114
+ var gap = 0;
115
+ var node = root;
116
+ while (node && node !== document.documentElement) {
117
+ if (node.scrollHeight > node.clientHeight + 1) {
118
+ var w = node.offsetWidth - node.clientWidth;
119
+ if (w > gap) gap = w;
120
+ }
121
+ node = node.parentElement;
122
+ }
123
+ return gap > 0 ? gap : 14;
124
+ }
125
+
126
+ function build() {
127
+ if (cleanup) { cleanup(); cleanup = null; }
128
+ if (isEditableContext()) {
129
+ var stale = document.getElementById('jp-minimap');
130
+ if (stale) stale.remove();
131
+ return;
132
+ }
133
+ ensureStyle();
134
+
135
+ var old = document.getElementById('jp-minimap');
136
+ if (old) old.remove();
137
+
138
+ var root = document.getElementById('rendered-md') || document.body;
139
+ var headings = Array.prototype.slice.call(
140
+ root.querySelectorAll('h1, h2, h3, h4, h5, h6')
141
+ );
142
+ if (headings.length < settings.minHeadings) return;
143
+
144
+ // Which sections contain at least one OPEN checkbox: walk headings and
145
+ // checkboxes in one document-ordered pass, attributing each unchecked
146
+ // box to the nearest preceding heading. One dot per section, however
147
+ // many open to-dos it holds.
148
+ var sectionHasTodo = {};
149
+ if (settings.showTodos !== false) {
150
+ var walk = Array.prototype.slice.call(
151
+ root.querySelectorAll('h1, h2, h3, h4, h5, h6, input[type="checkbox"]')
152
+ );
153
+ var lastHeading = -1;
154
+ for (var wi = 0; wi < walk.length; wi++) {
155
+ var wn = walk[wi];
156
+ if (/^H[1-6]$/.test(wn.tagName)) lastHeading++;
157
+ else if (!wn.checked && lastHeading >= 0) sectionHasTodo[lastHeading] = true;
158
+ }
159
+ }
160
+
161
+ var nav = document.createElement('nav');
162
+ nav.id = 'jp-minimap';
163
+ // Applied in BOTH collapsed and expanded states: shifting only on hover
164
+ // would move the panel out from under the cursor and cause a
165
+ // hover/unhover flicker loop.
166
+ // Docking side. ALWAYS clear the opposite offset: a fixed box with both
167
+ // left and right set stretches between them instead of hugging one edge.
168
+ // The scrollbar gap is a right-edge concern only - the viewer's scrollbar
169
+ // never sits on the left, so no gap is added there.
170
+ if (settings.side === 'left') {
171
+ nav.className = 'jp-mm-left';
172
+ nav.style.left = settings.rightOffset + 'px';
173
+ nav.style.right = 'auto';
174
+ } else {
175
+ nav.style.right = (settings.rightOffset + scrollbarGap(root)) + 'px';
176
+ nav.style.left = 'auto';
177
+ }
178
+ nav.style.setProperty('--jp-mm-width', settings.panelWidth + 'px');
179
+ // The viewer DOM can be editable in some contexts; make sure the
180
+ // minimap never shows a caret or accepts keyboard input.
181
+ // NOTE: no preventDefault on mousedown! Blocking the default mouse-focus
182
+ // path makes Joplin's later programmatic focus count as keyboard-like,
183
+ // and the browser then draws a :focus-visible ring on the scroll
184
+ // container. Natural mouse focus never shows a ring.
185
+ nav.setAttribute('contenteditable', 'false');
186
+ // Own the wheel entirely while the cursor is over the minimap:
187
+ // scroll the ToC list ourselves and never let the event chain
188
+ // through to the note underneath (scroll chaining feels erratic).
189
+ nav.addEventListener('wheel', function (e) {
190
+ e.preventDefault();
191
+ list.scrollTop += e.deltaY;
192
+ }, { passive: false });
193
+
194
+ var list = document.createElement('div');
195
+ list.className = 'jp-mm-list';
196
+
197
+ var items = headings.map(function (h, index) {
198
+ var level = Number(h.tagName.charAt(1));
199
+
200
+ // NOT an <a>: Joplin's viewer shows a "Ctrl+click to open" tooltip
201
+ // on anchors and treats them as external links.
202
+ var item = document.createElement('div');
203
+ item.className = 'jp-mm-item jp-mm-l' + level;
204
+
205
+ var bar = document.createElement('span');
206
+ bar.className = 'jp-mm-bar';
207
+
208
+ var label = document.createElement('span');
209
+ label.className = 'jp-mm-label';
210
+ label.textContent = (h.textContent || '').trim();
211
+
212
+ // Explicit on BOTH directions, not just RTL: dir also turns on bidi
213
+ // isolation, which keeps an LTR title readable inside an RTL note
214
+ // (and vice versa). CSS pins the collapsed rows back to ltr so the
215
+ // tick bars stay flush with the docked edge either way.
216
+ item.setAttribute('dir', textDirection(label.textContent, h));
217
+
218
+ // Muted red reminder dot before the bar (and before the label when
219
+ // expanded - same element, flex order does the work).
220
+ if (sectionHasTodo[index]) {
221
+ var dot = document.createElement('span');
222
+ dot.className = 'jp-mm-dot';
223
+ item.appendChild(dot);
224
+ }
225
+ item.appendChild(bar);
226
+ item.appendChild(label);
227
+
228
+ // MOUSEDOWN, not click: the press gives the webview focus, Joplin may
229
+ // re-render the note, and our panel gets rebuilt BETWEEN mousedown and
230
+ // mouseup - so the click event (which needs the same target for both)
231
+ // never fires. mousedown runs before any of that can happen.
232
+ item.addEventListener('mousedown', function (e) {
233
+ if (e.button !== 0) return;
234
+ var text = (label.textContent || '').trim();
235
+ // If the press triggers a note re-render, the rebuild will
236
+ // re-apply this jump against the fresh DOM.
237
+ pendingJump = { index: index, text: text, until: Date.now() + 1200 };
238
+ jumpTo(index, text);
239
+ });
240
+
241
+ list.appendChild(item);
242
+ return item;
243
+ });
244
+
245
+ nav.appendChild(list);
246
+ document.body.appendChild(nav);
247
+
248
+ function updateActive() {
249
+ var activeIndex = 0;
250
+ for (var i = 0; i < headings.length; i++) {
251
+ if (headings[i].getBoundingClientRect().top <= 90) activeIndex = i;
252
+ }
253
+ for (var j = 0; j < items.length; j++) {
254
+ items[j].classList.toggle('jp-mm-active', j === activeIndex);
255
+ }
256
+ }
257
+
258
+ document.addEventListener('scroll', updateActive, { passive: true, capture: true });
259
+ updateActive();
260
+
261
+ // A rebuild arriving right after a click means the note was re-rendered
262
+ // and the original scrollIntoView hit a detached node - redo the jump.
263
+ if (pendingJump && Date.now() < pendingJump.until) {
264
+ jumpTo(pendingJump.index, pendingJump.text);
265
+ pendingJump = null;
266
+ }
267
+
268
+ cleanup = function () {
269
+ document.removeEventListener('scroll', updateActive, { capture: true });
270
+ };
271
+ }
272
+
273
+ function settingsAndBuild() {
274
+ loadSettings().then(build);
275
+ }
276
+
277
+ var timer = null;
278
+ function scheduleBuild() {
279
+ clearTimeout(timer);
280
+ timer = setTimeout(settingsAndBuild, 150);
281
+ }
282
+
283
+ // True if a mutation was caused by the minimap itself (avoid rebuild loops).
284
+ function isOwnMutation(m) {
285
+ if (m.target && m.target.closest && m.target.closest('#jp-minimap')) return true;
286
+ var nodes = Array.prototype.slice.call(m.addedNodes)
287
+ .concat(Array.prototype.slice.call(m.removedNodes));
288
+ if (!nodes.length) return false;
289
+ return nodes.every(function (n) {
290
+ return n.id === 'jp-minimap' || (n.closest && n.closest('#jp-minimap'));
291
+ });
292
+ }
293
+
294
+ function watch() {
295
+ // Joplin fires this after each note render/update.
296
+ document.addEventListener('joplin-noteDidUpdate', scheduleBuild);
297
+
298
+ // Joplin's asset cleanup can remove our <style> from <head> AFTER the
299
+ // last body mutation - nothing rebuilds, and the nav sits unstyled in
300
+ // the page (the "outline below the document" bug, second incarnation:
301
+ // v1.1.4 only re-checked the style during rebuilds). Watch the head
302
+ // and re-inject immediately.
303
+ var headObserver = new MutationObserver(function () {
304
+ if (!document.getElementById('jp-minimap-style')) ensureStyle();
305
+ });
306
+ if (document.head) headObserver.observe(document.head, { childList: true });
307
+
308
+ // Fallback: watch for the rendered content being swapped out
309
+ // (note switch replaces the DOM without re-running this script).
310
+ var mo = new MutationObserver(function (mutations) {
311
+ for (var i = 0; i < mutations.length; i++) {
312
+ if (!isOwnMutation(mutations[i])) { scheduleBuild(); return; }
313
+ }
314
+ });
315
+ mo.observe(document.body, { childList: true, subtree: true });
316
+
317
+ settingsAndBuild();
318
+ }
319
+
320
+ if (document.readyState === 'loading') {
321
+ document.addEventListener('DOMContentLoaded', watch);
322
+ } else {
323
+ watch();
324
+ }
325
+ })();
@@ -1,22 +1,22 @@
1
- /* Content script: no markdown transformation needed.
2
- * We only ship two assets into the rendered viewer:
3
- * - minimap-view.js builds the minimap from the rendered DOM
4
- * - minimap.css collapsed tick bars + hover-expanded ToC panel
5
- */
6
- module.exports = {
7
- default: function (_context) {
8
- return {
9
- plugin: function (_markdownIt, _options) {
10
- // intentionally empty — rendering is untouched
11
- },
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.
16
- return [
17
- { name: 'minimap-view.js' },
18
- ];
19
- },
20
- };
21
- },
22
- };
1
+ /* Content script: no markdown transformation needed.
2
+ * We only ship two assets into the rendered viewer:
3
+ * - minimap-view.js builds the minimap from the rendered DOM
4
+ * - minimap.css collapsed tick bars + hover-expanded ToC panel
5
+ */
6
+ module.exports = {
7
+ default: function (_context) {
8
+ return {
9
+ plugin: function (_markdownIt, _options) {
10
+ // intentionally empty — rendering is untouched
11
+ },
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.
16
+ return [
17
+ { name: 'minimap-view.js' },
18
+ ];
19
+ },
20
+ };
21
+ },
22
+ };
Binary file