├── .eslintrc.json
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── demo.gif
├── index.html
├── keybinding-settings.png
├── package.json
├── pnpm-lock.yaml
├── readme.md
├── release.config.js
├── renovate.json
├── src
├── App.tsx
├── PageTabs.css
├── PageTabs.tsx
├── main.tsx
├── reset.css
├── settings.ts
├── types.ts
└── utils.ts
├── tsconfig.json
└── vite.config.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:@typescript-eslint/eslint-recommended",
5 | "plugin:@typescript-eslint/recommended"
6 | ],
7 | "plugins": ["@typescript-eslint", "react-hooks"],
8 | "parser": "@typescript-eslint/parser",
9 | "rules": {
10 | "react-hooks/rules-of-hooks": "error",
11 | "react-hooks/exhaustive-deps": "warn",
12 | "import/prefer-default-export": "off",
13 | "@typescript-eslint/ban-ts-comment": "off",
14 | "@typescript-eslint/no-non-null-assertion": "off",
15 | "@typescript-eslint/explicit-module-boundary-types": "off"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Releases
4 |
5 | # Controls when the action will run.
6 | on:
7 | push:
8 | branches:
9 | - "master"
10 | # Allows you to run this workflow manually from the Actions tab
11 | workflow_dispatch:
12 |
13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
14 | jobs:
15 | release:
16 | # The type of runner that the job will run on
17 | runs-on: ubuntu-latest
18 |
19 | # Steps represent a sequence of tasks that will be executed as part of the job
20 | steps:
21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
22 | - uses: actions/checkout@v2
23 | - uses: actions/setup-node@v2
24 | with:
25 | node-version: "16"
26 | - uses: pnpm/action-setup@v2.0.1
27 | with:
28 | version: 6.0.2
29 | - run: pnpm install
30 | - run: pnpm build
31 | - name: Install zip
32 | uses: montudor/action-zip@v1
33 | - name: Release
34 | run: npx semantic-release
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.19.4](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.19.3...v1.19.4) (2024-01-03)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * name of the Journals tab ([d495c6b](https://github.com/pengx17/logseq-plugin-tabs/commit/d495c6b325a8c48a71f2fc2feda0cde17e2ad2b4))
7 |
8 | ## [1.19.3](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.19.2...v1.19.3) (2023-09-28)
9 |
10 |
11 | ### Bug Fixes
12 |
13 | * support draggable app region for the awesome UI plugin ([7472067](https://github.com/pengx17/logseq-plugin-tabs/commit/74720672ae192b68e230d2f6ec452e1b68d47a66))
14 |
15 | ## [1.19.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.19.1...v1.19.2) (2023-09-27)
16 |
17 |
18 | ### Bug Fixes
19 |
20 | * tabs unclickable occasionally ([6fab7a1](https://github.com/pengx17/logseq-plugin-tabs/commit/6fab7a15dcdd551d422449cc2ae861e4056ab19a))
21 |
22 | ## [1.19.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.19.0...v1.19.1) (2023-05-05)
23 |
24 |
25 | ### Bug Fixes
26 |
27 | * a bug where a tab for a block is converted to a Journals tab when activating it ([d94921a](https://github.com/pengx17/logseq-plugin-tabs/commit/d94921af8a61251e58fab3eca1d0fba9d9686eff))
28 |
29 | # [1.19.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.18.3...v1.19.0) (2023-04-28)
30 |
31 |
32 | ### Features
33 |
34 | * bump version for new features ([e00b972](https://github.com/pengx17/logseq-plugin-tabs/commit/e00b972fcd15dcf9226ed640d49528e7d1968f0a))
35 |
36 | ## [1.18.3](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.18.2...v1.18.3) (2023-03-07)
37 |
38 |
39 | ### Bug Fixes
40 |
41 | * Page padding ([5197e28](https://github.com/pengx17/logseq-plugin-tabs/commit/5197e28ec3fff91ff0ba961e94be973ee45e38e5))
42 |
43 | ## [1.18.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.18.1...v1.18.2) (2023-02-17)
44 |
45 |
46 | ### Bug Fixes
47 |
48 | * bump new version ([7bea47a](https://github.com/pengx17/logseq-plugin-tabs/commit/7bea47a1e874f7aa201a3375212dce5c77f5c008))
49 |
50 | ## [1.18.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.18.0...v1.18.1) (2022-11-02)
51 |
52 |
53 | ### Bug Fixes
54 |
55 | * issue on opening non-existed tab ([6bfb0a6](https://github.com/pengx17/logseq-plugin-tabs/commit/6bfb0a6de56cc6a4f7abebb19dd5d6439a9712c7))
56 |
57 | # [1.18.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.17.3...v1.18.0) (2022-10-29)
58 |
59 |
60 | ### Features
61 |
62 | * option to hide "Close All" button ([5e93c8a](https://github.com/pengx17/logseq-plugin-tabs/commit/5e93c8af0ce5ba2c950fac695cdff6312db64ac7))
63 |
64 | ## [1.17.3](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.17.2...v1.17.3) (2022-10-27)
65 |
66 |
67 | ### Bug Fixes
68 |
69 | * graggable area ([909ee6a](https://github.com/pengx17/logseq-plugin-tabs/commit/909ee6ac2cb128517da0a378ba9c25b286c4b2ca))
70 |
71 | ## [1.17.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.17.1...v1.17.2) (2022-09-30)
72 |
73 |
74 | ### Bug Fixes
75 |
76 | * bump version for Add BODY class on load ([6334bc3](https://github.com/pengx17/logseq-plugin-tabs/commit/6334bc3583bc4e2d95482939d9a9fd7bf670a54b))
77 |
78 | ## [1.17.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.17.0...v1.17.1) (2022-08-23)
79 |
80 |
81 | ### Bug Fixes
82 |
83 | * key collision issue ([a1ec22a](https://github.com/pengx17/logseq-plugin-tabs/commit/a1ec22a0787243c44fe0d82b07c4735fdd839dca))
84 |
85 | # [1.17.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.16.0...v1.17.0) (2022-08-22)
86 |
87 |
88 | ### Features
89 |
90 | * option to have tab closeButton on left ([183a494](https://github.com/pengx17/logseq-plugin-tabs/commit/183a4946a62e7cb39c0e764bba44aa6d703359ac))
91 |
92 | # [1.16.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.15.3...v1.16.0) (2022-08-22)
93 |
94 |
95 | ### Features
96 |
97 | * middleMouseClick opens new tab ([2cf6de5](https://github.com/pengx17/logseq-plugin-tabs/commit/2cf6de57c9bc53dca098c84ed581d31c2868c61a))
98 |
99 | ## [1.15.3](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.15.2...v1.15.3) (2022-08-01)
100 |
101 |
102 | ### Bug Fixes
103 |
104 | * set toggle pin to empty binding by default since it conflicts with default ones ([dcf08e6](https://github.com/pengx17/logseq-plugin-tabs/commit/dcf08e6517bb1ca4d37e01e66a8dd011abd60b3b))
105 |
106 | ## [1.15.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.15.1...v1.15.2) (2022-07-21)
107 |
108 |
109 | ### Bug Fixes
110 |
111 | * bump for new version ([de2dc87](https://github.com/pengx17/logseq-plugin-tabs/commit/de2dc87ae02a870fda00b427ce28957c914fb48a))
112 |
113 | ## [1.15.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.15.0...v1.15.1) (2022-06-29)
114 |
115 |
116 | ### Bug Fixes
117 |
118 | * append the block id aggressively when opening a block ([0c898fa](https://github.com/pengx17/logseq-plugin-tabs/commit/0c898fa2d845d49335821569bb55cec1cdaf41a7))
119 |
120 | # [1.15.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.14.1...v1.15.0) (2022-06-17)
121 |
122 |
123 | ### Features
124 |
125 | * allow user custom tabs styles through custom.css ([61f23e8](https://github.com/pengx17/logseq-plugin-tabs/commit/61f23e80f41387f9f77d47d9145a7b510cb73def))
126 |
127 | ## [1.14.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.14.0...v1.14.1) (2022-06-09)
128 |
129 |
130 | ### Bug Fixes
131 |
132 | * add new tab via MOD+ENTER in search menu ([01d2a4a](https://github.com/pengx17/logseq-plugin-tabs/commit/01d2a4a1a7cd207c0b28b8953ad2a7fcea1ed511))
133 |
134 | # [1.14.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.13.1...v1.14.0) (2022-06-01)
135 |
136 |
137 | ### Features
138 |
139 | * open tabs via search menu ([f65bd53](https://github.com/pengx17/logseq-plugin-tabs/commit/f65bd538f4f8939f9a371ba65419e83bbabda5fb))
140 |
141 | ## [1.13.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.13.0...v1.13.1) (2022-05-19)
142 |
143 |
144 | ### Bug Fixes
145 |
146 | * add home page to show tab routes ([0a73716](https://github.com/pengx17/logseq-plugin-tabs/commit/0a73716e5cd15ccb7f6d0bfcdceb8297c9c58af7))
147 |
148 | # [1.13.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.12.6...v1.13.0) (2022-05-19)
149 |
150 |
151 | ### Features
152 |
153 | * add button for closing all tabs ([3022ff5](https://github.com/pengx17/logseq-plugin-tabs/commit/3022ff581bb2b6e7fd38cfb840923fa1b0eae502))
154 |
155 | ## [1.12.6](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.12.5...v1.12.6) (2022-05-17)
156 |
157 |
158 | ### Bug Fixes
159 |
160 | * only show tabs when needed ([061796f](https://github.com/pengx17/logseq-plugin-tabs/commit/061796f57546142b79be6b1d24126b0a745d9ab4))
161 |
162 | ## [1.12.5](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.12.4...v1.12.5) (2022-05-10)
163 |
164 |
165 | ### Bug Fixes
166 |
167 | * close other tabs not working properly ([25caa65](https://github.com/pengx17/logseq-plugin-tabs/commit/25caa65a9b402d538f590178e3bff97b3a019a0a))
168 |
169 | ## [1.12.4](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.12.3...v1.12.4) (2022-05-10)
170 |
171 |
172 | ### Bug Fixes
173 |
174 | * add close other tabs command ([991deab](https://github.com/pengx17/logseq-plugin-tabs/commit/991deab91dac4851adf81956da453b39ff0e6270))
175 |
176 | ## [1.12.3](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.12.2...v1.12.3) (2022-05-05)
177 |
178 |
179 | ### Bug Fixes
180 |
181 | * add a command to close all tabs ([8359d73](https://github.com/pengx17/logseq-plugin-tabs/commit/8359d733e4cfb537000f2bde53a192f94f7f6ae6))
182 |
183 | ## [1.12.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.12.1...v1.12.2) (2022-04-29)
184 |
185 |
186 | ### Bug Fixes
187 |
188 | * clean up get alias call ([22973fb](https://github.com/pengx17/logseq-plugin-tabs/commit/22973fbe5270112b31a60b90eace851f852b7696))
189 | * pinned tab issue ([b9d78ac](https://github.com/pengx17/logseq-plugin-tabs/commit/b9d78acd4fb1e1daf4ec29f0ce1f907bbd876fc8))
190 |
191 | ## [1.12.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.12.0...v1.12.1) (2022-04-26)
192 |
193 |
194 | ### Bug Fixes
195 |
196 | * allow the user give empty keybinding to revoke ([5fcd334](https://github.com/pengx17/logseq-plugin-tabs/commit/5fcd334c8b14801a188e05c5d7dcb66f8ed76118))
197 |
198 | # [1.12.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.11.0...v1.12.0) (2022-04-26)
199 |
200 |
201 | ### Features
202 |
203 | * support selecting tabs with mod+1 ~ 9 ([5a939e5](https://github.com/pengx17/logseq-plugin-tabs/commit/5a939e53ad4d9520a06b54590b1ab7c40b14268b))
204 |
205 | # [1.11.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.10.0...v1.11.0) (2022-04-24)
206 |
207 |
208 | ### Features
209 |
210 | * allow users to customize shortcuts ([041f048](https://github.com/pengx17/logseq-plugin-tabs/commit/041f048f5b9396a4ccfc12ce3c47baa3b0c5b2ba))
211 |
212 | # [1.10.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.9.2...v1.10.0) (2022-04-18)
213 |
214 |
215 | ### Features
216 |
217 | * add CTRL+TAB & CTRL+SHIFT+TAB commands ([34f2e3d](https://github.com/pengx17/logseq-plugin-tabs/commit/34f2e3d3d2e18821bbbf417a47d3573f35cc691b))
218 |
219 | ## [1.9.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.9.1...v1.9.2) (2022-04-06)
220 |
221 |
222 | ### Bug Fixes
223 |
224 | * add a padding top to make tabs & content fit nicer ([04e4bdf](https://github.com/pengx17/logseq-plugin-tabs/commit/04e4bdf23b419be117bda38023dfabe1e31ccb4b))
225 |
226 | ## [1.9.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.9.0...v1.9.1) (2022-04-06)
227 |
228 |
229 | ### Bug Fixes
230 |
231 | * keyboard binding sometimes does not work ([465c5a7](https://github.com/pengx17/logseq-plugin-tabs/commit/465c5a750c54b11c95930626e6acbead6e3ae732))
232 |
233 | # [1.9.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.8.9...v1.9.0) (2022-03-11)
234 |
235 |
236 | ### Bug Fixes
237 |
238 | * cursor blink issue ([0947f31](https://github.com/pengx17/logseq-plugin-tabs/commit/0947f31d5f99122b4dffa9c2c3f2a647d5f04b17))
239 |
240 |
241 | ### Features
242 |
243 | * add two keyboard shortcuts ([5bffa80](https://github.com/pengx17/logseq-plugin-tabs/commit/5bffa80be9eb01d22cd4106d896a8e892e764e43))
244 |
245 | ## [1.8.9](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.8.8...v1.8.9) (2022-02-21)
246 |
247 |
248 | ### Bug Fixes
249 |
250 | * bump version for [#18](https://github.com/pengx17/logseq-plugin-tabs/issues/18) ([eecd6af](https://github.com/pengx17/logseq-plugin-tabs/commit/eecd6af18776b4a66ca99d2641e4512be0983a25))
251 |
252 | ## [1.8.8](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.8.7...v1.8.8) (2022-02-17)
253 |
254 |
255 | ### Bug Fixes
256 |
257 | * remove keyboard shortcuts for now (will refactor this later) ([24d9445](https://github.com/pengx17/logseq-plugin-tabs/commit/24d9445b51d707eea635bb9f3cb3373d5794f850))
258 | * show block content in tab ([9239369](https://github.com/pengx17/logseq-plugin-tabs/commit/92393696953d57ccfaa034d3dfc4824926907c4c)), closes [#16](https://github.com/pengx17/logseq-plugin-tabs/issues/16)
259 |
260 | ## [1.8.7](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.8.6...v1.8.7) (2021-12-01)
261 |
262 |
263 | ### Bug Fixes
264 |
265 | * disable context menu event for now ([4092b38](https://github.com/pengx17/logseq-plugin-tabs/commit/4092b3832952c594b786bcbac764486bf5d8ec27))
266 |
267 | ## [1.8.6](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.8.5...v1.8.6) (2021-11-29)
268 |
269 |
270 | ### Bug Fixes
271 |
272 | * add a border to tabs ([46cdd8c](https://github.com/pengx17/logseq-plugin-tabs/commit/46cdd8c7e6dec0f872d3c75a580068eec0f181c9))
273 |
274 | ## [1.8.5](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.8.4...v1.8.5) (2021-11-28)
275 |
276 |
277 | ### Bug Fixes
278 |
279 | * allow open sidebar items into tabs ([69ab179](https://github.com/pengx17/logseq-plugin-tabs/commit/69ab1794c769e3a55866a6359eedd2a348c2eff4))
280 | * preserve focus when hovering tabs ([aeb1a23](https://github.com/pengx17/logseq-plugin-tabs/commit/aeb1a23fff666626e42d0e871eec93b69f8b1c57))
281 |
282 | ## [1.8.4](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.8.3...v1.8.4) (2021-11-19)
283 |
284 |
285 | ### Bug Fixes
286 |
287 | * minor ux ([9ce517e](https://github.com/pengx17/logseq-plugin-tabs/commit/9ce517e446631cd39a3feccbd7949579f84cb453))
288 |
289 | ## [1.8.3](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.8.2...v1.8.3) (2021-11-15)
290 |
291 |
292 | ### Bug Fixes
293 |
294 | * tabs scroll on active is not working ([41635e9](https://github.com/pengx17/logseq-plugin-tabs/commit/41635e90b7e9118dbc317f645eea093e785e6c36))
295 |
296 | ## [1.8.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.8.1...v1.8.2) (2021-11-12)
297 |
298 |
299 | ### Bug Fixes
300 |
301 | * should only pushState when change to different pages ([1ece34c](https://github.com/pengx17/logseq-plugin-tabs/commit/1ece34ce91e8f16c37b879add5920029879d347b))
302 |
303 | ## [1.8.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.8.0...v1.8.1) (2021-11-11)
304 |
305 |
306 | ### Bug Fixes
307 |
308 | * update tab icons ([0de928d](https://github.com/pengx17/logseq-plugin-tabs/commit/0de928dd2afff60301ea5c557b89d27627ae36fa))
309 |
310 | # [1.8.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.7.5...v1.8.0) (2021-11-08)
311 |
312 |
313 | ### Features
314 |
315 | * adapt page level properties as the prefix ([2f7aca9](https://github.com/pengx17/logseq-plugin-tabs/commit/2f7aca9fc1d60d7882d2fc71ef5e55a94313eb7c))
316 |
317 | ## [1.7.5](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.7.4...v1.7.5) (2021-10-12)
318 |
319 |
320 | ### Bug Fixes
321 |
322 | * tabs width when pdf is on ([6df1e41](https://github.com/pengx17/logseq-plugin-tabs/commit/6df1e41873cc2e3c31bf459c2ddf23554d463565))
323 |
324 | ## [1.7.4](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.7.3...v1.7.4) (2021-10-12)
325 |
326 |
327 | ### Bug Fixes
328 |
329 | * remove log ([cd5afcd](https://github.com/pengx17/logseq-plugin-tabs/commit/cd5afcdb899c92bc1e1130758d4074781b6e7370))
330 |
331 | ## [1.7.3](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.7.2...v1.7.3) (2021-10-12)
332 |
333 |
334 | ### Bug Fixes
335 |
336 | * adapt for logseq 0.4.3 ([abe7d95](https://github.com/pengx17/logseq-plugin-tabs/commit/abe7d9514f079efb63ae042120534b1ba44aa436))
337 |
338 | ## [1.7.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.7.1...v1.7.2) (2021-10-08)
339 |
340 |
341 | ### Bug Fixes
342 |
343 | * tabs sometimes prevent user from using shortcuts & tab name rename issue ([57c86d7](https://github.com/pengx17/logseq-plugin-tabs/commit/57c86d765e11b3cea9f094d5363e8903df51b703))
344 |
345 | ## [1.7.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.7.0...v1.7.1) (2021-09-26)
346 |
347 |
348 | ### Bug Fixes
349 |
350 | * build issue ([8d9c1bf](https://github.com/pengx17/logseq-plugin-tabs/commit/8d9c1bfcf6d11091a9b57a5f69873752b20ea682))
351 |
352 | # [1.7.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.6.0...v1.7.0) (2021-09-26)
353 |
354 |
355 | ### Features
356 |
357 | * allow use shift+cmd+click to open and visit new tab ([7ea20ad](https://github.com/pengx17/logseq-plugin-tabs/commit/7ea20ad1e24a03549ec7d59974295d4edacf2ad6))
358 |
359 | # [1.6.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.5.4...v1.6.0) (2021-09-07)
360 |
361 |
362 | ### Features
363 |
364 | * enable multi-graph support. ([d1b2224](https://github.com/pengx17/logseq-plugin-tabs/commit/d1b222466c8f6281194bfcff21356639256ac2a9))
365 |
366 | ## [1.5.4](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.5.3...v1.5.4) (2021-09-06)
367 |
368 |
369 | ### Bug Fixes
370 |
371 | * optimize animation ([6ba07f2](https://github.com/pengx17/logseq-plugin-tabs/commit/6ba07f2a7c6b799b5daf6c30ce6ce02cad76b028))
372 |
373 | ## [1.5.3](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.5.2...v1.5.3) (2021-09-02)
374 |
375 |
376 | ### Bug Fixes
377 |
378 | * some style enhancements ([277ee7c](https://github.com/pengx17/logseq-plugin-tabs/commit/277ee7c3c7ff56251240eaffcc82de0fec52971f))
379 |
380 | ## [1.5.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.5.1...v1.5.2) (2021-09-02)
381 |
382 |
383 | ### Bug Fixes
384 |
385 | * a issue when targeting block/page is not yet created ([5dae24e](https://github.com/pengx17/logseq-plugin-tabs/commit/5dae24e3a134660126712cd69f4d6b1e94e116f2))
386 |
387 | ## [1.5.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.5.0...v1.5.1) (2021-09-02)
388 |
389 |
390 | ### Bug Fixes
391 |
392 | * build ([7b57803](https://github.com/pengx17/logseq-plugin-tabs/commit/7b57803817af2f3a64477b6e496d88fc3df90d80))
393 |
394 | # [1.5.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.4.3...v1.5.0) (2021-08-31)
395 |
396 |
397 | ### Features
398 |
399 | * support opening block refs ([ccf3c7e](https://github.com/pengx17/logseq-plugin-tabs/commit/ccf3c7e5295aee97f9e3706e629ffb50d82f5bd6))
400 |
401 | ## [1.4.3](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.4.2...v1.4.3) (2021-08-31)
402 |
403 |
404 | ### Bug Fixes
405 |
406 | * some npe issues ([d0cdfa5](https://github.com/pengx17/logseq-plugin-tabs/commit/d0cdfa539c39063e0707b470f8facfc7fa07bda7))
407 |
408 | ## [1.4.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.4.1...v1.4.2) (2021-08-31)
409 |
410 |
411 | ### Bug Fixes
412 |
413 | * tabs position when there is pdf opening ([147bea5](https://github.com/pengx17/logseq-plugin-tabs/commit/147bea52b0e32bf1fcbc61d63e16aeeaacc610bb))
414 |
415 | ## [1.4.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.4.0...v1.4.1) (2021-08-31)
416 |
417 |
418 | ### Bug Fixes
419 |
420 | * sorted not working after using immer ([2cb184e](https://github.com/pengx17/logseq-plugin-tabs/commit/2cb184eb65c1b96500679495a3165f6630fd0672))
421 |
422 | # [1.4.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.3.8...v1.4.0) (2021-08-31)
423 |
424 |
425 | ### Features
426 |
427 | * restore tab scroll position ([fe5008c](https://github.com/pengx17/logseq-plugin-tabs/commit/fe5008c23e9d82d037772ff029a998c882f6ad98))
428 |
429 | ## [1.3.8](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.3.7...v1.3.8) (2021-08-31)
430 |
431 |
432 | ### Bug Fixes
433 |
434 | * fix polling issue ([191573c](https://github.com/pengx17/logseq-plugin-tabs/commit/191573ca680a3c34710bd11c2db4e3ca85c14e55))
435 |
436 | ## [1.3.7](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.3.6...v1.3.7) (2021-08-31)
437 |
438 |
439 | ### Bug Fixes
440 |
441 | * makes tabs more responsiveness ([18f269f](https://github.com/pengx17/logseq-plugin-tabs/commit/18f269f6ded39a747e2c968dcac4c5d497d86c83))
442 |
443 | ## [1.3.6](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.3.5...v1.3.6) (2021-08-30)
444 |
445 |
446 | ### Bug Fixes
447 |
448 | * do not use deprecated navigator.platform field ([0670d3e](https://github.com/pengx17/logseq-plugin-tabs/commit/0670d3e5d34115758e6ee5f903c0f9d270647d43))
449 |
450 | ## [1.3.5](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.3.4...v1.3.5) (2021-08-30)
451 |
452 |
453 | ### Bug Fixes
454 |
455 | * show tabs more aggresively ([5f1392e](https://github.com/pengx17/logseq-plugin-tabs/commit/5f1392ed7bca336f3ad96e8fb88cffcb18fd6717))
456 |
457 | ## [1.3.4](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.3.3...v1.3.4) (2021-08-30)
458 |
459 |
460 | ### Bug Fixes
461 |
462 | * sorting & animation ([381b2eb](https://github.com/pengx17/logseq-plugin-tabs/commit/381b2ebdbdc2722b6168d630987bcaaf4c474462))
463 |
464 | ## [1.3.3](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.3.2...v1.3.3) (2021-08-30)
465 |
466 |
467 | ### Bug Fixes
468 |
469 | * tabs background may cover other elements ([16a0ac4](https://github.com/pengx17/logseq-plugin-tabs/commit/16a0ac40aaf537d11f3a9026b974d6a328503a3f))
470 |
471 | ## [1.3.2](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.3.1...v1.3.2) (2021-08-30)
472 |
473 |
474 | ### Bug Fixes
475 |
476 | * animation fix ([7e4957f](https://github.com/pengx17/logseq-plugin-tabs/commit/7e4957f15e0dbab6f56c9013b51db35ab785962f))
477 | * optimize show/hide logic ([2e9a60d](https://github.com/pengx17/logseq-plugin-tabs/commit/2e9a60d5021a6e3296700218dfac232caea4834d))
478 |
479 | ## [1.3.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.3.0...v1.3.1) (2021-08-29)
480 |
481 | ### Bug Fixes
482 |
483 | - show view if there is a single pinned tab ([1b476d6](https://github.com/pengx17/logseq-plugin-tabs/commit/1b476d60b3902f2f2d790262dc0eb802d121c1f1))
484 |
485 | # [1.3.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.2.1...v1.3.0) (2021-08-29)
486 |
487 | ### Bug Fixes
488 |
489 | - do not hide label when pinned ([47013b4](https://github.com/pengx17/logseq-plugin-tabs/commit/47013b479284e18e3003662ccf9d6eda518ec0a9))
490 |
491 | ### Features
492 |
493 | - allow drag & drop tabs ([94587b7](https://github.com/pengx17/logseq-plugin-tabs/commit/94587b73c36f4930af94dd8176e5bf16c4a47821))
494 |
495 | ## [1.2.1](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.2.0...v1.2.1) (2021-08-29)
496 |
497 | ### Bug Fixes
498 |
499 | - ux fix ([ae63d8f](https://github.com/pengx17/logseq-plugin-tabs/commit/ae63d8fe032e7b342f1b6a4e7c27918afbd7132d))
500 |
501 | # [1.2.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.1.0...v1.2.0) (2021-08-28)
502 |
503 | ### Features
504 |
505 | - align browser behaviors for ux ([ef31004](https://github.com/pengx17/logseq-plugin-tabs/commit/ef3100430d32c1cd6a847f57d78828b079709cf3))
506 |
507 | # [1.1.0](https://github.com/pengx17/logseq-plugin-tabs/compare/v1.0.0...v1.1.0) (2021-08-28)
508 |
509 | ### Features
510 |
511 | - finish for new ux ([865105a](https://github.com/pengx17/logseq-plugin-tabs/commit/865105ad315a014c19c35f17735a0340d53fbf43))
512 |
513 | # 1.0.0 (2021-08-27)
514 |
515 | ### Bug Fixes
516 |
517 | - deps ([659bbe4](https://github.com/pengx17/logseq-plugin-tabs/commit/659bbe40ebe05b5a9765f8b2e16f7429128078f6))
518 | - release ([936092f](https://github.com/pengx17/logseq-plugin-tabs/commit/936092f34dcdf24af4c70ab18215f7c234f19857))
519 |
520 | ### Features
521 |
522 | - adapt for theme ([d7d9999](https://github.com/pengx17/logseq-plugin-tabs/commit/d7d9999743b0fd8363ab2ce0247497c60780a61a))
523 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Peng Xiao
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.
22 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengx17/logseq-plugin-tabs/430ec3da743cd838896a291670500602ad95b679/demo.gif
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Logseq Plugin
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/keybinding-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengx17/logseq-plugin-tabs/430ec3da743cd838896a291670500602ad95b679/keybinding-settings.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logseq-plugin-tabs",
3 | "version": "1.19.4",
4 | "schemaVersion": "1.0.0",
5 | "main": "dist/index.html",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preinstall": "npx only-allow pnpm"
10 | },
11 | "license": "MIT",
12 | "dependencies": {
13 | "@logseq/libs": "^0.0.15",
14 | "fast-deep-equal": "^3.1.3",
15 | "immer": "^9.0.16",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0",
18 | "react-use": "^17.4.0"
19 | },
20 | "devDependencies": {
21 | "@semantic-release/changelog": "6.0.1",
22 | "@semantic-release/exec": "^6.0.3",
23 | "@semantic-release/git": "10.0.1",
24 | "@semantic-release/npm": "9.0.1",
25 | "@types/react": "18.0.24",
26 | "@types/react-dom": "18.0.8",
27 | "@typescript-eslint/eslint-plugin": "^5.42.0",
28 | "@typescript-eslint/parser": "^5.42.0",
29 | "@vitejs/plugin-react": "^2.2.0",
30 | "conventional-changelog-conventionalcommits": "5.0.0",
31 | "eslint": "^8.26.0",
32 | "eslint-plugin-react": "^7.31.10",
33 | "eslint-plugin-react-hooks": "^4.6.0",
34 | "semantic-release": "^19.0.5",
35 | "typescript": "4.8.4",
36 | "vite": "3.2.2",
37 | "vite-plugin-logseq": "^1.1.2",
38 | "vite-plugin-windicss": "1.8.8",
39 | "windicss": "3.5.6"
40 | },
41 | "logseq": {
42 | "id": "_pengx17-logseq-tabs"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Logseq Plugin Tabs
2 |
3 | ### 🔔 Looking for maintainers! 🔔
4 |
5 | [](https://github.com/pengx17/logseq-plugin-tabs/releases)
6 |
7 | A plugin that let's you to manage your working pages with tabs.
8 |
9 | UX is mainly brought from modern browsers:
10 |
11 | - normally, if a new page is visited, the current tab will be replaced by the new page
12 | - if you click a page link or a block ref while holding CTRL (or CMD on Mac) key, a new tab will be created, but it is not visited yet
13 | - you can click the remove icon or middle click a tab to close tabs
14 | - you can double-click a tab to pin it. A pinned tab will not be replaced or be removed.
15 | - you can drag & drop to reorder tabs
16 | - tabs info will be persisted in your local storage, so that your tabs will recover even if you re-open the app
17 |
18 | 
19 |
20 | ## Keyboard shortcuts
21 |
22 | - Pin/unpin a tab: CTRL + P (macOS: CMD + P)
23 | - Close a tab: SHIFT + CTRL + W (macOS: SHIFT + CMD + W)
24 | - Change to next tab: CTRL + TAB
25 | - Change to nth tab: CTRL + 1 ~ 9 (this is not configurable yet)
26 |
27 | Hint: you can change them in the Settings. After change, you need to restart the app.
28 |
29 | 
30 |
31 | ## Contributing
32 |
33 | - Please follow [Logseq's guidelines](https://github.com/logseq/logseq/blob/master/CONTRIBUTING.md) for contributions and Pull Requests.
34 | - See the [logseq-plugin-template-react readme](https://github.com/pengx17/logseq-plugin-template-react?tab=readme-ov-file#how-to-get-started) for steps on how to build and test this plugin.
35 |
--------------------------------------------------------------------------------
/release.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | branches: ["master"],
3 | plugins: [
4 | [
5 | "@semantic-release/commit-analyzer",
6 | {
7 | preset: "conventionalcommits",
8 | },
9 | ],
10 | "@semantic-release/release-notes-generator",
11 | "@semantic-release/changelog",
12 | [
13 | "@semantic-release/npm",
14 | {
15 | npmPublish: false,
16 | },
17 | ],
18 | "@semantic-release/git",
19 | [
20 | "@semantic-release/exec",
21 | {
22 | prepareCmd:
23 | "zip -qq -r logseq-plugin-tabs-${nextRelease.version}.zip dist readme.md LICENSE package.json",
24 | },
25 | ],
26 | [
27 | "@semantic-release/github",
28 | {
29 | assets: "logseq-plugin-tabs-*.zip",
30 | },
31 | ],
32 | ],
33 | };
34 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "packageRules": [
4 | {
5 | "matchUpdateTypes": ["minor", "patch"],
6 | "automerge": true,
7 | "requiredStatusChecks": null
8 | },
9 | {
10 | "matchPackageNames": ["@logseq/libs"],
11 | "ignoreUnstable": false,
12 | "automerge": true,
13 | "requiredStatusChecks": null
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { PageTabs } from "./PageTabs";
3 | import { usePreventFocus, useThemeMode } from "./utils";
4 |
5 | function App(): JSX.Element {
6 | const themeMode = useThemeMode();
7 | usePreventFocus();
8 | return (
9 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default App;
22 |
--------------------------------------------------------------------------------
/src/PageTabs.css:
--------------------------------------------------------------------------------
1 | .logseq-tab {
2 | @apply cursor-pointer font-sans select-none text-xs h-6 transition-all duration-100
3 | flex items-center rounded mx-0.5 border border-1 light:border-gray-200 dark:border-gray-900
4 | px-2 light:text-black dark:text-white;
5 | }
6 |
7 | .logseq-tab[data-active="false"] {
8 | @apply light:(bg-cool-gray-100 text-gray-400 hover:text-black)
9 | dark:(bg-cool-gray-800 text-gray-400 hover:text-white);
10 | }
11 |
12 | .logseq-tab[data-active="true"] {
13 | @apply light:bg-cool-gray-300 dark:bg-cool-gray-900;
14 | }
15 |
16 | .logseq-tab[data-dragging="true"] {
17 | @apply ring-1 ring-red-500 mx-6;
18 | }
19 |
20 | .logseq-tab-title {
21 | @apply overflow-ellipsis max-w-80 px-0.5 overflow-hidden whitespace-nowrap inline transition-all delay-75 duration-100 ease-in-out;
22 | }
23 |
24 | .logseq-tab[data-active="false"] .logseq-tab-title {
25 | @apply max-w-40;
26 | }
27 |
28 | .logseq-tab[data-active="true"] .logseq-tab-title {
29 | @apply max-w-80;
30 | transition-property: none;
31 | }
32 |
33 | .logseq-tab[data-active="false"] button:hover {
34 | visibility: hidden;
35 | }
36 |
37 | .logseq-tab[data-active="false"]:hover button {
38 | visibility: visible;
39 | }
40 |
41 | [data-dragging="false"]
42 | .logseq-tab[data-active="false"][data-active="false"]:hover
43 | .logseq-tab-title {
44 | @apply max-w-80;
45 | transition-delay: 1s;
46 | transition-property: max-width;
47 | }
48 |
49 | .logseq-tab .close-button {
50 | @apply text-10px p-1 opacity-60 hover:opacity-100 ml-1 rounded;
51 | }
52 |
53 | .logseq-tab button:hover {
54 | @apply light:(bg-cool-gray-400)
55 | dark:(bg-cool-gray-600);
56 | }
57 |
58 | .close-all {
59 | opacity: 0;
60 | }
61 |
62 | .logseq-tab-wrapper:hover .close-all {
63 | opacity: 1;
64 | }
--------------------------------------------------------------------------------
/src/PageTabs.tsx:
--------------------------------------------------------------------------------
1 | import type {
2 | BlockEntity,
3 | SimpleCommandKeybinding,
4 | } from "@logseq/libs/dist/LSPlugin";
5 | import produce from "immer";
6 | import React from "react";
7 | import { useDeepCompareEffect, useLatest } from "react-use";
8 | import "./PageTabs.css";
9 | import { keyBindings } from "./settings";
10 | import { ITabInfo } from "./types";
11 | import {
12 | delay,
13 | getSourcePage,
14 | isBlock,
15 | isMac,
16 | mainContainerScroll,
17 | useAdaptMainUIStyle,
18 | useDebounceFn,
19 | useEventCallback,
20 | useScrollWidth,
21 | useStoreTabs,
22 | } from "./utils";
23 |
24 | const CloseSVG = () => (
25 |
35 | );
36 |
37 | function isTabEqual(
38 | tab: ITabInfo | null | undefined,
39 | anotherTab: ITabInfo | null | undefined
40 | ) {
41 | function isEqual(a?: string, b?: string) {
42 | return a != null && b != null && a.toLowerCase() === b.toLowerCase();
43 | }
44 | if (tab?.name == getJournalsString() && !anotherTab?.uuid) { // not possible to check 'anotherTab.name == undefined' for Journal, because a tab for a block also has no name
45 | return true;
46 | }
47 |
48 | if (tab?.page || anotherTab?.page) {
49 | return isEqual(tab?.uuid, anotherTab?.uuid);
50 | }
51 | return Boolean(
52 | isEqual(tab?.originalName, anotherTab?.originalName) ||
53 | isEqual(tab?.name, anotherTab?.name) ||
54 | // isEqual(tab?.uuid, anotherTab?.uuid) ||
55 | // @ts-expect-error
56 | tab?.alias?.includes(anotherTab?.id)
57 | );
58 | }
59 |
60 | interface TabsProps {
61 | tabs: ITabInfo[];
62 | closeButtonLeft: boolean;
63 | hideCloseAllButton: boolean;
64 | showSingleTab: boolean;
65 | activeTab: ITabInfo | null | undefined;
66 | onClickTab: (tab: ITabInfo, isShiftKeyPressed: boolean) => void;
67 | onCloseTab: (tab: ITabInfo, force?: boolean) => void;
68 | onCloseAllTabs: (excludeActive: boolean) => void;
69 | onPinTab: (tab: ITabInfo) => void;
70 | onSwapTab: (tab: ITabInfo, anotherTab: ITabInfo) => void;
71 | }
72 |
73 | const Tabs = React.forwardRef(
74 | (
75 | {
76 | activeTab,
77 | onClickTab,
78 | tabs,
79 | closeButtonLeft,
80 | hideCloseAllButton,
81 | showSingleTab,
82 | onCloseTab,
83 | onCloseAllTabs,
84 | onPinTab,
85 | onSwapTab,
86 | },
87 | ref
88 | ) => {
89 | const [draggingTab, setDraggingTab] = React.useState();
90 |
91 | React.useEffect(() => {
92 | const dragEndListener = () => {
93 | setDraggingTab(undefined);
94 | };
95 | document.addEventListener("dragend", dragEndListener);
96 | return () => {
97 | document.removeEventListener("dragend", dragEndListener);
98 | };
99 | }, []);
100 |
101 | // Clean out link markup and internal block metadata from text shown in tab titles.
102 | const cleanBlockTitle = (text: string): string => (
103 | text
104 | .replace(/\[([^\]]+)\]\([^\)]+\)/, "$1")
105 | .replace(/\s+(id|background-color|collapsed):: .+/g, "")
106 | )
107 |
108 | const debouncedSwap = useDebounceFn(onSwapTab, 0);
109 | const showTabs =
110 | showSingleTab ||
111 | 0 < tabs.filter((tab) => tab.pinned).length ||
112 | 1 < tabs.length;
113 |
114 | if (!showTabs) {
115 | return null;
116 | }
117 | return (
118 | {
126 | if (e.button === 1) e.preventDefault();
127 | }}
128 | >
129 | {tabs.map((tab) => {
130 | const isActive = isTabEqual(tab, activeTab);
131 | const onClose: React.MouseEventHandler = (e) => {
132 | e.stopPropagation();
133 | onCloseTab(tab);
134 | };
135 | const onDragOver: React.DragEventHandler = (e) => {
136 | if (draggingTab) {
137 | // Prevent drag fly back animation
138 | e.preventDefault();
139 | e.dataTransfer.dropEffect = "move";
140 | debouncedSwap(tab, draggingTab);
141 | }
142 | };
143 | const onDragStart: React.DragEventHandler = (e) => {
144 | e.dataTransfer.effectAllowed = "move";
145 | setDraggingTab(tab);
146 | e.stopPropagation();
147 | };
148 | const prefix = tab.properties?.icon
149 | ? tab.properties?.icon
150 | : isBlock(tab)
151 | ? "B"
152 | : tab.uuid == undefined // is Journal
153 | ? "J"
154 | : "P";
155 | return (
156 |
onClickTab(tab, e.shiftKey)}
158 | onDoubleClick={() => onPinTab(tab)}
159 | onContextMenu={(e) => {
160 | e.preventDefault();
161 | console.log(e);
162 | // onAuxClick={/*onClose*/}
163 | // TODO: show the same context menu like right-clicking the title?
164 | console.log("Not implemented yet");
165 | }}
166 | key={[tab.originalName, tab.uuid].join("-")}
167 | data-active={isActive}
168 | data-pinned={tab.pinned}
169 | data-dragging={draggingTab === tab}
170 | draggable={true}
171 | onDragOver={onDragOver}
172 | onDragStart={onDragStart}
173 | className="logseq-tab group"
174 | >
175 |
176 | {prefix}
177 |
178 | {closeButtonLeft && tab.pinned ? (
179 |
📌
180 | ) : (
181 | closeButtonLeft && (
182 |
185 | )
186 | )}
187 |
188 | {tab.originalName ?? tab.name}{" "}
189 | {isBlock(tab) && (
190 |
191 | •
192 | {cleanBlockTitle(tab.content)}
193 |
194 | )}
195 |
196 | {!closeButtonLeft && tab.pinned ? (
197 |
📌
198 | ) : (
199 | !closeButtonLeft && (
200 |
203 | )
204 | )}
205 |
206 | );
207 | })}
208 | {!hideCloseAllButton && (
209 |
onCloseAllTabs(true)}
211 | key={"Close All"}
212 | draggable={false}
213 | className="logseq-tab close-all group"
214 | >
215 | Close All
216 |
217 | )}
218 |
219 | );
220 | }
221 | );
222 |
223 | function getPageRef(element: HTMLElement) {
224 | const el = element as HTMLAnchorElement;
225 | return (
226 | getBlockContentPageRef(el) ??
227 | getSidebarPageRef(el) ??
228 | getReferencesPageRef(el) ??
229 | getSearchMenuPageRef(el) ??
230 | getFavoritesOrRecentPageRef(el)
231 | );
232 | }
233 |
234 | function getBlockContentPageRef(element: HTMLElement) {
235 | const el = element as HTMLAnchorElement;
236 | if (
237 | el.tagName === "A" &&
238 | el.hasAttribute("data-ref") &&
239 | (el.className.includes("page-ref") || el.className.includes("tag"))
240 | ) {
241 | return element.getAttribute("data-ref");
242 | }
243 | }
244 |
245 | function getSidebarPageRef(element: HTMLElement) {
246 | const el = element as HTMLAnchorElement;
247 | if (el.tagName === "A" && el.querySelector(".page-icon")) {
248 | return Array.from(element.childNodes)
249 | .find((n) => n.nodeName === "#text")
250 | ?.textContent?.trim();
251 | }
252 | }
253 |
254 | function getReferencesPageRef(element: HTMLElement) {
255 | const el = element as HTMLAnchorElement;
256 | // if it is a page ref link in the references section
257 | if (
258 | el.tagName === "A" &&
259 | el.closest(".references") &&
260 | el.getAttribute("href")?.startsWith("#/page/")
261 | ) {
262 | return Array.from(element.childNodes)
263 | .find((n) => n.nodeName === "#text")
264 | ?.textContent?.trim();
265 | }
266 | }
267 |
268 | function getFavoritesOrRecentPageRef(element: HTMLElement) {
269 | const el = element as HTMLAnchorElement;
270 | if (el.tagName === "SPAN" && el.classList.contains("page-title")) {
271 | const parentListItem = el.closest("li");
272 |
273 | if (parentListItem?.classList.contains("favorite-item") || parentListItem?.classList.contains("recent-item")) {
274 | return parentListItem.getAttribute("data-ref");
275 | }
276 | }
277 | }
278 |
279 | function getClosestSearchMenuLink(element: HTMLElement): HTMLElement | null {
280 | return element.closest(".search-results-wrap .menu-link");
281 | }
282 |
283 | function getSearchMenuPageRef(element: HTMLElement) {
284 | const refEl = getClosestSearchMenuLink(element);
285 | return refEl?.querySelector("[data-page-ref]")?.dataset.pageRef;
286 | }
287 |
288 | function getSearchMenuBlockRef(element: HTMLElement) {
289 | const refEl = getClosestSearchMenuLink(element);
290 | return refEl?.querySelector("[data-block-ref]")?.dataset
291 | .blockRef;
292 | }
293 |
294 | function getBlockUUID(element: HTMLElement) {
295 | return (
296 | element.getAttribute("blockid") ??
297 | element.querySelector("[blockid]")?.getAttribute("blockid") ??
298 | getSearchMenuBlockRef(element)
299 | );
300 | }
301 |
302 | function getJournalsString(): string {
303 | let storedJournalsString = localStorage.getItem("journalsString");
304 |
305 | if (!storedJournalsString) {
306 | let readJournalsString = top?.document.querySelector(".journals-nav")?.firstChild?.children[1]?.textContent;
307 |
308 | // Be careful if we read "gj": it's likely because we selected the wrong element.
309 | if (readJournalsString && readJournalsString == "gj") {
310 | readJournalsString = top?.document.querySelector(".journals-nav")?.firstChild?.lastChild?.textContent;
311 | }
312 |
313 | if (readJournalsString) {
314 | storedJournalsString = readJournalsString;
315 | } else {
316 | storedJournalsString = "Journals" // fallback
317 | }
318 |
319 | localStorage.setItem("journalsString", storedJournalsString);
320 | }
321 |
322 | return storedJournalsString;
323 | }
324 |
325 | function getIsJournalLink(element: HTMLElement) {
326 | return null !== element.closest(".journals-nav");
327 | }
328 |
329 | function stop(e: Event) {
330 | e.stopPropagation();
331 | e.stopImmediatePropagation();
332 | e.preventDefault(); // prevent scrolling from middle mouse button click
333 | }
334 |
335 | /**
336 | * Captures user CTRL Click a page link.
337 | */
338 | function useCaptureAddTabAction(cb: (e: ITabInfo, open: boolean) => void) {
339 | const handleAddTab = React.useCallback(
340 | async (e: Event, target: HTMLElement) => {
341 | let newTab: ITabInfo | null = null;
342 | if (getPageRef(target)) {
343 | stop(e);
344 | const p = await getSourcePage(getPageRef(target));
345 | if (p) {
346 | newTab = p;
347 | }
348 | } else if (getBlockUUID(target)) {
349 | stop(e);
350 | const blockId = getBlockUUID(target);
351 | if (blockId) {
352 | const block = await logseq.Editor.getBlock(blockId);
353 | if (block) {
354 | const page = await logseq.Editor.getPage(block?.page.id);
355 | // also, write block id to the block properties if it is missing there ...
356 | setTimeout(async () => {
357 | if (!(await logseq.Editor.getBlockProperty(blockId, "id"))) {
358 | logseq.Editor.upsertBlockProperty(blockId, "id", blockId);
359 | }
360 | }, 100);
361 | if (page) {
362 | newTab = { ...page, ...block };
363 | }
364 | }
365 | }
366 | } else if (getIsJournalLink(target)) {
367 | newTab = { name: getJournalsString(), uuid: undefined }
368 | }
369 |
370 | if (newTab) {
371 | cb(newTab, false);
372 | }
373 | },
374 | [cb]
375 | );
376 |
377 | React.useEffect(() => {
378 | const listener = async (e: MouseEvent) => {
379 | const target = e.composedPath()[0] as HTMLElement;
380 | // If CtrlKey is pressed or Middle Mouse Button is clicked, always open a new tab
381 | const ctrlKey = isMac() ? e.metaKey : e.ctrlKey;
382 | const middleMouseClick = e.button == 1;
383 |
384 | if (ctrlKey || middleMouseClick) {
385 | handleAddTab(e, target);
386 | }
387 | };
388 | top?.document.addEventListener("mousedown", listener, true);
389 | return () => {
390 | top?.document.removeEventListener("mousedown", listener, true);
391 | };
392 | }, [handleAddTab]);
393 |
394 | // Capture MOD+ENTER on search results
395 | React.useEffect(() => {
396 | const listener = async (e: KeyboardEvent) => {
397 | const ctrlKey = isMac() ? e.metaKey : e.ctrlKey;
398 | if (e.key === "Enter" && ctrlKey) {
399 | // Find out chosen search item
400 | const chosenMenuItem = top?.document.querySelector(
401 | ".search-results-wrap .menu-link.chosen"
402 | );
403 | if (chosenMenuItem) {
404 | handleAddTab(e, chosenMenuItem);
405 | }
406 | }
407 | };
408 | top?.document.addEventListener("keydown", listener, true);
409 | return () => {
410 | top?.document.removeEventListener("keydown", listener, true);
411 | };
412 | }, [handleAddTab]);
413 | }
414 |
415 | /**
416 | * the active page is the page that is currently being viewed
417 | */
418 | export function useActiveTab(tabs: ITabInfo[]) {
419 | const [page, setPage] = React.useState(null);
420 | const pageRef = React.useRef(page);
421 | const setActivePage = useEventCallback(async () => {
422 | const p = await logseq.Editor.getCurrentPage();
423 | let tab: ITabInfo | null = null;
424 | tab = tabs.find((t) => isTabEqual(t, p)) ?? null;
425 | if (!tab) {
426 | if (p) {
427 | tab = await logseq.Editor.getPage(
428 | p.name ?? (p as BlockEntity)?.page.id
429 | );
430 | } else {
431 | tab = { name: getJournalsString(), uuid: undefined }
432 | }
433 | }
434 |
435 | tab = { ...tab, ...p };
436 | if (tab.scrollTop) {
437 | setTimeout(() => { mainContainerScroll({ top: tab.scrollTop }); }, 250);
438 | }
439 | pageRef.current = tab;
440 | setPage(tab);
441 | });
442 | React.useEffect(() => {
443 | return logseq.App.onRouteChanged(setActivePage);
444 | }, [setActivePage]);
445 | React.useEffect(() => {
446 | let stopped = false;
447 | async function poll() {
448 | await delay(1500);
449 | if (!pageRef.current && !stopped) {
450 | await setActivePage();
451 | await poll();
452 | }
453 | }
454 | poll();
455 | return () => {
456 | stopped = true;
457 | };
458 | }, [setActivePage]);
459 |
460 | return [page, setPage] as const;
461 | }
462 |
463 | const sortTabs = (tabs: ITabInfo[]) => {
464 | tabs.sort((a, b) => {
465 | if (a.pinned && !b.pinned) {
466 | return -1;
467 | } else if (!a.pinned && b.pinned) {
468 | return 1;
469 | } else {
470 | return 0;
471 | }
472 | });
473 | };
474 |
475 | // Avoid register issues during dev
476 | const registeredKeybindings = new Set();
477 |
478 | function registerKeybinding(
479 | setting: {
480 | key: string;
481 | label: string;
482 | keybinding?: SimpleCommandKeybinding | undefined;
483 | },
484 | cb: () => void
485 | ) {
486 | if (registeredKeybindings.has(setting.key)) {
487 | return;
488 | }
489 | registeredKeybindings.add(setting.key);
490 | logseq.App.registerCommandPalette(setting, cb);
491 | }
492 |
493 | const useRegisterKeybindings = (
494 | key: keyof typeof keyBindings,
495 | cb: () => void
496 | ) => {
497 | const cbRef = useEventCallback(cb);
498 |
499 | React.useEffect(() => {
500 | const userKeybinding: string = logseq.settings?.[key];
501 | if (userKeybinding.trim() !== "") {
502 | const setting = {
503 | key,
504 | label: keyBindings[key].label,
505 | keybinding: {
506 | binding: logseq.settings?.[key],
507 | mode: "global",
508 | } as SimpleCommandKeybinding,
509 | };
510 | registerKeybinding(setting, cbRef);
511 | }
512 | // eslint-disable-next-line react-hooks/exhaustive-deps
513 | }, []);
514 | };
515 |
516 | const useRegisterSelectNthTabKeybindings = (cb: (nth: number) => void) => {
517 | const cbRef = useEventCallback(cb);
518 |
519 | React.useEffect(() => {
520 | for (let i = 1; i <= 9; i++) {
521 | const key = `tabs-select-nth-tab-${i}`;
522 | const setting = {
523 | key,
524 | label: `Select tab ${i}`,
525 | keybinding: {
526 | binding: `mod+${i}`,
527 | mode: "non-editing",
528 | } as SimpleCommandKeybinding,
529 | };
530 | registerKeybinding(setting, () => {
531 | cbRef(i);
532 | });
533 | }
534 | // eslint-disable-next-line react-hooks/exhaustive-deps
535 | }, []);
536 | };
537 |
538 | const useRegisterCloseAllButPins = (cb: (b: boolean) => void) => {
539 | const cbRef = useEventCallback(cb);
540 |
541 | React.useEffect(() => {
542 | registerKeybinding(
543 | {
544 | key: `tabs-close-all`,
545 | label: `Close all tabs`,
546 | // no keybindings yet
547 | },
548 | () => {
549 | cbRef(false);
550 | }
551 | );
552 | // eslint-disable-next-line react-hooks/exhaustive-deps
553 | }, []);
554 |
555 | React.useEffect(() => {
556 | registerKeybinding(
557 | {
558 | key: `tabs-close-others`,
559 | label: `Close other tabs`,
560 | // no keybindings yet
561 | },
562 | () => {
563 | cbRef(true);
564 | }
565 | );
566 | // eslint-disable-next-line react-hooks/exhaustive-deps
567 | }, []);
568 | };
569 |
570 | export function PageTabs(): JSX.Element {
571 | const [tabs, setTabs] = useStoreTabs();
572 | const [activeTab, setActiveTab] = useActiveTab(tabs);
573 |
574 | const currActiveTabRef = React.useRef();
575 | const latestTabsRef = useLatest(tabs);
576 | const showSingleTab = !!logseq.settings?.["tabs:show-single-tab"];
577 | const closeButtonLeft = !!logseq.settings?.["tabs:close-button-left"];
578 | const hideCloseAllButton = !!logseq.settings?.["tabs:hide-close-all-button"];
579 |
580 | const onCloseTab = useEventCallback((tab: ITabInfo, force?: boolean) => {
581 | const idx = tabs.findIndex((t) => isTabEqual(t, tab));
582 |
583 | // Do not close pinned
584 | if (tabs[idx]?.pinned && !force) {
585 | return;
586 | }
587 | const newTabs = [...tabs];
588 | newTabs.splice(idx, 1);
589 | setTabs(newTabs);
590 |
591 | if (newTabs.length === 0) {
592 | logseq.App.pushState("home");
593 | } else if (isTabEqual(tab, activeTab)) {
594 | const newTab = newTabs[Math.min(newTabs.length - 1, idx)];
595 | setActiveTab(newTab);
596 | }
597 | });
598 |
599 | const getCurrentActiveIndex = () => {
600 | return tabs.findIndex((ct) => isTabEqual(ct, currActiveTabRef.current));
601 | };
602 |
603 | const onCloseAllTabs = useEventCallback((excludeActive: boolean) => {
604 | const newTabs = tabs.filter(
605 | (t) =>
606 | t.pinned || (excludeActive && isTabEqual(t, currActiveTabRef.current))
607 | );
608 | setTabs(newTabs);
609 | if (!excludeActive) {
610 | logseq.App.pushState("home");
611 | }
612 | });
613 |
614 | const onTabClick = useEventCallback(async (t: ITabInfo, isShiftKeyPressed: boolean) => {
615 | if (isBlock(t) && t.uuid) {
616 | const block = await logseq.Editor.getBlock(t.uuid);
617 | if (!block) {
618 | logseq.UI.showMsg(
619 | `The target block ${t.content} is not found!`,
620 | "error",
621 | {
622 | timeout: 1000,
623 | }
624 | );
625 | // force close it if it's not found
626 | onCloseTab(t, true);
627 | return;
628 | }
629 | }
630 |
631 | if (isShiftKeyPressed) {
632 | // TODO shift click on Journals tab is not displayed in right side bar (working when shift clicked on Journals link in left sidebar)
633 | logseq.Editor.openInRightSidebar(t.uuid as string);
634 | } else {
635 | onChangeTab(t);
636 | }
637 | });
638 |
639 | const onChangeTab = useEventCallback(async (t: ITabInfo) => {
640 | if (isBlock(t) && t.uuid) {
641 | const block = await logseq.Editor.getBlock(t.uuid);
642 | if (!block) {
643 | logseq.UI.showMsg(
644 | `The target block ${t.content} is not found!`,
645 | "error",
646 | {
647 | timeout: 1000,
648 | }
649 | );
650 | // force close it if it's not found
651 | onCloseTab(t, true);
652 | return;
653 | }
654 | }
655 |
656 | if (t.name == getJournalsString() && !t.uuid) {
657 | logseq.App.pushState("all-journals");
658 | }
659 |
660 | setActiveTab(t);
661 | const idx = getCurrentActiveIndex();
662 | // remember current page's scroll position
663 | if (idx !== -1) {
664 | const scrollTop =
665 | top?.document.querySelector("#main-content-container")?.scrollTop;
666 |
667 | setTabs(
668 | produce(tabs, (draft) => {
669 | draft[idx].scrollTop = scrollTop;
670 | })
671 | );
672 | }
673 | });
674 |
675 | const onNewTab = useEventCallback((t: ITabInfo | null, open = false) => {
676 | if (t) {
677 | const previous = tabs.find((_t) => isTabEqual(t, _t));
678 | if (!previous) {
679 | setTabs([...tabs, t]);
680 | } else {
681 | open = true;
682 | }
683 | if (open) {
684 | onChangeTab({ ...t, pinned: previous?.pinned });
685 | }
686 | }
687 | });
688 |
689 | useCaptureAddTabAction(onNewTab);
690 | useDeepCompareEffect(() => {
691 | let timer = 0;
692 | let newTabs = latestTabsRef.current;
693 | const prevTab = currActiveTabRef.current;
694 | // If a new ActiveTab is set, we will need to replace or insert the tab
695 | if (activeTab) {
696 | newTabs = produce(tabs, (draft) => {
697 | if (tabs.every((t) => !isTabEqual(t, activeTab))) {
698 | const currentIndex = draft.findIndex((t) => isTabEqual(t, prevTab));
699 | const currentPinned = draft[currentIndex]?.pinned;
700 | if (currentIndex === -1 || currentPinned) {
701 | draft.push(activeTab);
702 | } else {
703 | draft[currentIndex] = activeTab;
704 | }
705 | } else {
706 | // Update the data if it is already in the list (to update icons etc)
707 | const currentIndex = draft.findIndex((t) => isTabEqual(t, activeTab));
708 | draft[currentIndex] = activeTab;
709 | }
710 | });
711 | timer = setTimeout(async () => {
712 | const p = await logseq.Editor.getCurrentPage();
713 | if (!isTabEqual(activeTab, p)) {
714 | logseq.App.pushState("page", {
715 | name: isBlock(activeTab)
716 | ? activeTab.uuid
717 | : activeTab.originalName ?? activeTab.name,
718 | });
719 | }
720 | }, 200);
721 | }
722 | currActiveTabRef.current = activeTab;
723 | setTabs(newTabs);
724 | return () => {
725 | if (timer) {
726 | clearTimeout(timer);
727 | }
728 | };
729 | }, [activeTab ?? {}]);
730 |
731 | const onPinTab = useEventCallback((t) => {
732 | setTabs(
733 | produce(tabs, (draft) => {
734 | const idx = draft.findIndex((ct) => isTabEqual(ct, t));
735 | draft[idx].pinned = !draft[idx].pinned;
736 | sortTabs(draft);
737 | })
738 | );
739 | });
740 |
741 | const onSwapTab = (t0: ITabInfo, t1: ITabInfo) => {
742 | setTabs(
743 | produce(tabs, (draft) => {
744 | const i0 = draft.findIndex((t) => isTabEqual(t, t0));
745 | const i1 = draft.findIndex((t) => isTabEqual(t, t1));
746 | draft[i0] = t1;
747 | draft[i1] = t0;
748 | sortTabs(draft);
749 | })
750 | );
751 | };
752 |
753 | const ref = React.useRef(null);
754 | const scrollWidth = useScrollWidth(ref);
755 |
756 | useAdaptMainUIStyle(tabs.length > 0, scrollWidth);
757 |
758 | React.useEffect(() => {
759 | if (activeTab && ref) {
760 | setTimeout(() => {
761 | ref.current
762 | ?.querySelector(`[data-active="true"]`)
763 | ?.scrollIntoView({ behavior: "smooth" });
764 | }, 100);
765 | }
766 | }, [activeTab, ref]);
767 |
768 | useRegisterKeybindings("tabs:toggle-pin", () => {
769 | if (currActiveTabRef.current) {
770 | onPinTab(currActiveTabRef.current);
771 | }
772 | });
773 |
774 | useRegisterKeybindings("tabs:close", () => {
775 | if (currActiveTabRef.current) {
776 | onCloseTab(currActiveTabRef.current);
777 | }
778 | });
779 |
780 | useRegisterKeybindings("tabs:select-next", () => {
781 | let idx = getCurrentActiveIndex() ?? -1;
782 | idx = (idx + 1) % tabs.length;
783 | onChangeTab(tabs[idx]);
784 | });
785 |
786 | useRegisterKeybindings("tabs:select-prev", () => {
787 | let idx = getCurrentActiveIndex() ?? -1;
788 | idx = (idx - 1 + tabs.length) % tabs.length;
789 | onChangeTab(tabs[idx]);
790 | });
791 |
792 | useRegisterSelectNthTabKeybindings((idx) => {
793 | if (idx > 0 && idx <= tabs.length) {
794 | onChangeTab(tabs[idx - 1]);
795 | }
796 | });
797 |
798 | useRegisterCloseAllButPins(onCloseAllTabs);
799 |
800 | return (
801 |
814 | );
815 | }
816 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import "@logseq/libs";
2 | import React from "react";
3 | import * as ReactDOM from "react-dom/client";
4 | import "virtual:windi.css";
5 | import { settings } from "./settings";
6 |
7 | import App from "./App";
8 | import "./reset.css";
9 | import { isMac } from "./utils";
10 |
11 | function main() {
12 | const pluginId = logseq.baseInfo.id;
13 | console.info(`#${pluginId}: MAIN`);
14 | const mac = isMac();
15 | logseq.provideStyle(`
16 | [data-active-keystroke=${mac ? "Meta" : "Control"} i]
17 | :is(.block-ref,.page-ref,a.tag) {
18 | cursor: n-resize
19 | }
20 | `);
21 |
22 | const root = ReactDOM.createRoot(document.getElementById("app")!);
23 | root.render(
24 |
25 |
26 |
27 | );
28 |
29 | parent.document.body.classList.add('is-plugin-tabs-enabled');
30 | logseq.beforeunload(async () => {
31 | parent.document.body.classList.remove('is-plugin-tabs-enabled');
32 | });
33 |
34 | console.info(`#${pluginId}: MAIN DONE`);
35 | }
36 |
37 | logseq.useSettingsSchema(settings).ready(main).catch(console.error);
38 |
--------------------------------------------------------------------------------
/src/reset.css:
--------------------------------------------------------------------------------
1 | ::-webkit-scrollbar {
2 | display: none;
3 | }
4 |
5 | .drag-region {
6 | -webkit-app-region: drag;
7 | }
--------------------------------------------------------------------------------
/src/settings.ts:
--------------------------------------------------------------------------------
1 | import { SettingSchemaDesc } from "@logseq/libs/dist/LSPlugin.user";
2 |
3 | export const keyBindings = {
4 | "tabs:toggle-pin": {
5 | label: "Toggle Tab Pin Status",
6 | binding: "",
7 | },
8 | "tabs:close": {
9 | label: "Close Tab",
10 | binding: "mod+shift+w",
11 | },
12 | "tabs:select-next": {
13 | label: "Select Next Tab",
14 | binding: "ctrl+tab",
15 | },
16 | "tabs:select-prev": {
17 | label: "Select Previous Tab",
18 | binding: "ctrl+shift+tab",
19 | }
20 | };
21 |
22 | const keybindingSettings: SettingSchemaDesc[] = Object.entries(keyBindings).map(
23 | ([key, value]) => ({
24 | key,
25 | title: value.label,
26 | type: "string",
27 | default: value.binding,
28 | description:
29 | "Keybinding: " +
30 | value.label +
31 | ". Default: `" +
32 | value.binding +
33 | "`. You need to restart the app for the changes to take effect.",
34 | })
35 | );
36 |
37 | export const inheritCustomCSSSetting: SettingSchemaDesc = {
38 | key: "tabs:inherit-custom-css",
39 | title: "Advanced: inherit custom.css styles",
40 | default: false,
41 | description:
42 | "When turning this on, this plugin will also applies styles in custom.css. You need to restart the app for the changes to take effect.",
43 | type: "boolean",
44 | };
45 |
46 | export const showSingleTab: SettingSchemaDesc = {
47 | key: "tabs:show-single-tab",
48 | title: "Show single tab?",
49 | description: "When turned on the tab bar will only show if at least two tabs are open.",
50 | type: "boolean",
51 | default: true,
52 | }
53 |
54 | export const closeButtonLeft: SettingSchemaDesc = {
55 | key: "tabs:close-button-left",
56 | title: "Close tab button on left side?",
57 | description: "When turned on the close button will be on the left side of the tab.",
58 | type: "boolean",
59 | default: false,
60 | }
61 |
62 | export const hideCloseAllButton: SettingSchemaDesc = {
63 | key: "tabs:hide-close-all-button",
64 | title: "Hide 'Close All' button?",
65 | description: "When turned on 'Close All' button at the end of tabs list will be hidden.",
66 | type: "boolean",
67 | default: false,
68 | }
69 |
70 | export const settings: SettingSchemaDesc[] = [
71 | ...keybindingSettings,
72 | inheritCustomCSSSetting,
73 | showSingleTab,
74 | closeButtonLeft,
75 | hideCloseAllButton
76 | ];
77 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface ITabInfo {
2 | // Main attributes from page/block
3 | uuid?: string;
4 | name?: string;
5 | originalName?: string;
6 | content?: string;
7 | page?: {
8 | id: number;
9 | };
10 | properties?: {
11 | icon?: string;
12 | };
13 |
14 | // UI States:
15 | pinned?: boolean;
16 | scrollTop?: number;
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import type { PageEntity } from "@logseq/libs/dist/LSPlugin";
3 | import React, { useMemo, useState } from "react";
4 | import isEqual from "fast-deep-equal";
5 | import { useHoverDirty, useMountedState } from "react-use";
6 | import { schemaVersion } from "../package.json";
7 | import { ITabInfo } from "./types";
8 | import { inheritCustomCSSSetting } from "./settings";
9 |
10 | export const useAppVisible = () => {
11 | const [visible, setVisible] = useState(logseq.isMainUIVisible);
12 | const isMounted = useMountedState();
13 | React.useEffect(() => {
14 | const eventName = "ui:visible:changed";
15 | const handler = async ({ visible }: { visible: boolean }) => {
16 | if (isMounted()) {
17 | setVisible(visible);
18 | }
19 | };
20 | logseq.on(eventName, handler);
21 | return () => {
22 | logseq.off(eventName, handler);
23 | };
24 | }, [isMounted]);
25 | return visible;
26 | };
27 |
28 | export const useSidebarVisible = () => {
29 | const [visible, setVisible] = useState(false);
30 | const isMounted = useMountedState();
31 | React.useEffect(() => {
32 | logseq.App.onSidebarVisibleChanged(({ visible }) => {
33 | if (isMounted()) {
34 | setVisible(visible);
35 | }
36 | });
37 | }, [isMounted]);
38 | return visible;
39 | };
40 |
41 | export const useThemeMode = () => {
42 | const isMounted = useMountedState();
43 | const [mode, setMode] = React.useState<"dark" | "light">("light");
44 | React.useEffect(() => {
45 | setMode(
46 | (top!.document
47 | .querySelector("html")
48 | ?.getAttribute("data-theme") as typeof mode) ??
49 | (matchMedia("prefers-color-scheme: dark").matches ? "dark" : "light")
50 | );
51 | return logseq.App.onThemeModeChanged((s) => {
52 | if (isMounted()) {
53 | setMode(s.mode);
54 | }
55 | });
56 | }, [isMounted]);
57 |
58 | return mode;
59 | };
60 |
61 | export async function getSourcePage(
62 | pageName?: string | null
63 | ): Promise {
64 | if (!pageName) {
65 | return null;
66 | }
67 | const page = await logseq.Editor.getPage(pageName);
68 |
69 | // If the page contains alias and it has no property alias ...
70 | // @ts-expect-error
71 | if (page && page.alias?.length > 0 && !(page.properties?.alias.length > 0)) {
72 | // @ts-expect-error
73 | const pageId = page.alias[0]?.id;
74 | if (pageId) {
75 | return await logseq.Editor.getPage(pageId);
76 | }
77 | }
78 | return page;
79 | }
80 |
81 | export const delay = (ms: number) =>
82 | new Promise((resolve) => setTimeout(resolve, ms));
83 |
84 | function getKeyId(graph: string) {
85 | return "logseq-plugin-tabs:" + schemaVersion + "/" + graph;
86 | }
87 |
88 | const readFromLocalStorage = (graph: string): ITabInfo[] | null => {
89 | const str = localStorage.getItem(getKeyId(graph));
90 | if (str) {
91 | try {
92 | return JSON.parse(str);
93 | } catch {
94 | // no ops
95 | }
96 | }
97 | return null;
98 | };
99 |
100 | const persistToLocalStorage = (tabs: ITabInfo[], graph: string) => {
101 | localStorage.setItem(getKeyId(graph), JSON.stringify(tabs));
102 | };
103 |
104 | function useCurrentGraph() {
105 | const [graph, setGraph] = useState(null);
106 | const reset = async () => {
107 | const g = await logseq.App.getCurrentGraph();
108 | setGraph(g?.path ?? null);
109 | };
110 | React.useEffect(() => {
111 | reset();
112 | return logseq.App.onCurrentGraphChanged(() => {
113 | reset();
114 | });
115 | }, []);
116 | return graph;
117 | }
118 |
119 | export function useStoreTabs() {
120 | const [tabs, setTabs] = React.useState([]);
121 | const currentGraph = useCurrentGraph();
122 |
123 | React.useEffect(() => {
124 | if (currentGraph) {
125 | const tabs = readFromLocalStorage(currentGraph);
126 | setTabs(tabs ?? []);
127 | }
128 | }, [currentGraph]);
129 |
130 | const userSetTabs = (newTabs: ITabInfo[]) => {
131 | if (currentGraph && !isEqual(tabs, newTabs)) {
132 | persistToLocalStorage(newTabs, currentGraph);
133 | return setTabs(newTabs);
134 | }
135 | };
136 |
137 | return [tabs, userSetTabs] as const;
138 | }
139 |
140 | export function debounce any>(fn: T, ms: number) {
141 | let timeout: number | null = null;
142 | return (...args: any[]) => {
143 | if (timeout) {
144 | clearTimeout(timeout);
145 | }
146 | timeout = window.setTimeout(() => {
147 | fn(...args);
148 | timeout = null;
149 | }, ms);
150 | };
151 | }
152 |
153 | export function useDebounceFn any>(
154 | callback: T,
155 | timeout = 300
156 | ) {
157 | const safeCallback = useEventCallback(callback);
158 | return useMemo(
159 | () => debounce(safeCallback, timeout),
160 | [safeCallback, timeout]
161 | );
162 | }
163 |
164 | interface RouteState {
165 | template: string;
166 | path: string;
167 | }
168 |
169 | function useRouteState() {
170 | const [state, setState] = React.useState(
171 | // @ts-expect-error getStateFromStore sames not working properly
172 | top?.logseq.api.get_state_from_store("route-match")
173 | );
174 |
175 | React.useEffect(() => {
176 | return logseq.App.onRouteChanged(setState);
177 | }, []);
178 |
179 | return state;
180 | }
181 |
182 | function useSettingValue(key: string) {
183 | const [value, setValue] = React.useState(logseq.settings?.[key]);
184 | React.useEffect(() => {
185 | return logseq.onSettingsChanged(() => {
186 | setValue(logseq.settings?.[key]);
187 | });
188 | });
189 | return value;
190 | }
191 |
192 | export function useCustomCSS() {
193 | const enabled = useSettingValue(inheritCustomCSSSetting.key);
194 | React.useEffect(() => {
195 | const rootHead = top?.document.head;
196 | if (rootHead && enabled) {
197 | const applyCustomCSS = () => {
198 | let customCSSLink = document.getElementById(
199 | "logseq-custom-theme-id"
200 | ) as HTMLLinkElement;
201 | if (!customCSSLink) {
202 | customCSSLink = document.createElement("link");
203 | customCSSLink.id = "logseq-custom-theme-id";
204 | customCSSLink.rel = "stylesheet";
205 | customCSSLink.media = "all";
206 | document.head.append(customCSSLink);
207 | }
208 | const content = top?.document.querySelector(
209 | "#logseq-custom-theme-id"
210 | )?.href;
211 | if (content) {
212 | customCSSLink.href = content;
213 | }
214 | };
215 |
216 | const observer = new MutationObserver((mutations) => {
217 | for (const mutation of mutations) {
218 | for (const addedNode of mutation.addedNodes) {
219 | if (
220 | addedNode.nodeName === "LINK" &&
221 | (addedNode as HTMLLinkElement).id === "logseq-custom-theme-id"
222 | ) {
223 | applyCustomCSS();
224 | break;
225 | }
226 | }
227 | }
228 | });
229 | observer.observe(rootHead, {
230 | childList: true,
231 | });
232 | applyCustomCSS();
233 | return () => {
234 | observer.disconnect();
235 | };
236 | }
237 | if (!enabled) {
238 | document.getElementById("logseq-custom-theme-id")?.remove();
239 | }
240 | });
241 | }
242 |
243 | export function useAdaptMainUIStyle(show: boolean, tabsWidth?: number | null) {
244 | const route = useRouteState();
245 | useCustomCSS();
246 | const shouldShow =
247 | show &&
248 | (!route?.template ||
249 | ["/", "/all-journals", "/page/:name", "/file/:path"].includes(
250 | route?.template
251 | ));
252 | const docRef = React.useRef(document.documentElement);
253 | const isHovering = useHoverDirty(docRef);
254 | React.useEffect(() => {
255 | logseq.provideStyle({
256 | key: "tabs--top-padding",
257 | style: `
258 | .cp__sidebar-main-content {
259 | padding-top: ${shouldShow ? "64px" : ""};
260 | }`,
261 | });
262 |
263 | logseq.showMainUI({ autoFocus: false });
264 | const headerEl = top!.document.querySelector(
265 | "#head.cp__header"
266 | )! as HTMLElement;
267 |
268 | const mainContainer = top!.document.querySelector(
269 | "#main-content-container"
270 | ) as HTMLElement;
271 |
272 | if (!mainContainer) {
273 | return;
274 | }
275 |
276 | const listener = () => {
277 | const { left: leftOffset, width } = mainContainer.getBoundingClientRect();
278 | const maxWidth = width - 10;
279 | logseq.setMainUIInlineStyle({
280 | zIndex: 9,
281 | userSelect: "none",
282 | position: "fixed",
283 | left: `${leftOffset}px`,
284 | top: `${headerEl.offsetHeight + 2}px`,
285 | height: shouldShow ? "28px" : "0px",
286 | width: isHovering ? "100%" : tabsWidth + "px", // 10 is the width of the scrollbar
287 | maxWidth: maxWidth + "px",
288 | });
289 | };
290 | listener();
291 | const ob = new ResizeObserver(listener);
292 | ob.observe(mainContainer);
293 | return () => {
294 | ob.disconnect();
295 | };
296 | }, [shouldShow, tabsWidth, isHovering]);
297 | }
298 |
299 | export const isMac = () => {
300 | return navigator.userAgent.includes("Mac");
301 | };
302 |
303 | export function useEventCallback any>(fn: T): T {
304 | const ref: any = React.useRef();
305 |
306 | // we copy a ref to the callback scoped to the current state/props on each render
307 | React.useLayoutEffect(() => {
308 | ref.current = fn;
309 | });
310 |
311 | return React.useCallback(
312 | (...args: any[]) => ref.current.apply(void 0, args),
313 | []
314 | ) as T;
315 | }
316 |
317 | export const useScrollWidth = (
318 | ref: React.RefObject
319 | ) => {
320 | const [scrollWidth, setScrollWidth] = React.useState();
321 | React.useEffect(() => {
322 | const update = () => setScrollWidth(ref.current?.scrollWidth || 0);
323 | const mo = new MutationObserver(() => {
324 | // Run multiple times to take animation into account, hacky...
325 | update();
326 | setTimeout(update, 100);
327 | setTimeout(update, 200);
328 | setTimeout(update, 300);
329 | });
330 | if (ref.current) {
331 | setScrollWidth(ref.current.scrollWidth || 0);
332 | mo.observe(ref.current, {
333 | childList: true,
334 | subtree: true,
335 | attributes: true,
336 | });
337 | }
338 | return () => mo.disconnect();
339 | }, [ref]);
340 | return scrollWidth;
341 | };
342 |
343 | export const mainContainerScroll = (scrollOptions: ScrollToOptions) => {
344 | top?.document.querySelector("#main-content-container")?.scrollTo(scrollOptions);
345 | };
346 |
347 | export const isBlock = (t: ITabInfo) => {
348 | return Boolean(t.page);
349 | };
350 |
351 | // Makes sure the user will not lose focus (editing state) when previewing a link
352 | export const usePreventFocus = () => {
353 | const restoreFocus = useDebounceFn(
354 | useEventCallback(() => {
355 | if (window.document.hasFocus()) {
356 | (top as any).focus();
357 | logseq.Editor.restoreEditingCursor();
358 | }
359 | }),
360 | 10
361 | );
362 | React.useEffect(() => {
363 | let timer = 0;
364 | timer = setInterval(restoreFocus, 1000);
365 | window.addEventListener("focus", restoreFocus);
366 | return () => {
367 | window.removeEventListener("focus", restoreFocus);
368 | clearInterval(timer);
369 | };
370 | });
371 | };
372 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "types": ["vite/client"],
6 | "allowJs": false,
7 | "skipLibCheck": false,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react"
18 | },
19 | "include": ["./src"]
20 | }
21 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import reactRefresh from "@vitejs/plugin-react";
2 | import { defineConfig } from "vite";
3 | import WindiCSS from "vite-plugin-windicss";
4 |
5 | import logseqPlugin from "vite-plugin-logseq";
6 |
7 | const reactRefreshPlugin = reactRefresh({
8 | fastRefresh: false,
9 | });
10 |
11 | // https://vitejs.dev/config/
12 | export default defineConfig({
13 | plugins: [reactRefreshPlugin, WindiCSS(), logseqPlugin()],
14 | clearScreen: false,
15 | build: {
16 | target: "esnext",
17 | minify: "esbuild",
18 | },
19 | });
20 |
--------------------------------------------------------------------------------