├── LICENSE
├── README.md
├── custom.css
└── custom.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 cannibalox
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # logseq-custom-files
2 | **custom.js** and **custom.css** utilities for Logseq.
3 |
4 | ## Current Version v20240317
5 |
6 | ### **Query table view resizer** :
7 | Add handles on the query table headers to resize column width
8 |
9 | 
10 |
11 |
12 | ### **Namespace prefixes collapser** :
13 | Collapse namespace prefixes eg: [[prefix/page/test]] becomes [[../test]] (use the hover tootip to see the original name or enter edit mode)
14 |
15 | 
16 |
17 |
18 | ### **Twitter embeds** :
19 | Fetches and embeds tweets and timelines without using logseq's internal syntax `{{tweet https://twitter.com/username/status/id}}`. Instead, you can just write the tweet url inline.
20 |
21 | Benefits:
22 | - doesn't add extra markup to the source file
23 | - shows the timelines.
24 |
25 | A demo with Logtool's kanban css to display latest tweets :
26 |
27 | 
28 |
29 | ### **Better sidebar** :
30 | Enhance the right sidebar by replacing the vertical scroll with horizontal panes which are collapsible and resizable. Inspired by the sliding panes/matuschak mode with improved usability.
31 |
32 | This works in conjunction with a custom.js snippet. If you don't want to use this sidebar mod, you need to REMOVE the better-sidebar javascript (edit the custom.js and comment out or remove the lines)
33 |
34 | 
35 |
36 |
37 | ## How-to use/install
38 |
39 | ### No exisiting custom.css/custom.js
40 | If you are not using any custom.js[^1] or custom.css, copy the files into your `%graph-name%/logseq/` folder.
41 |
42 | ### Existing custom.css/custom.js
43 | Alternatively, if you don't want to overwrite your current files or are only interested in some of the utilites :
44 | 1. Open the desired file with a text editor/code editor
45 | 2. Copy-paste the relevant sections into your own custom.js/custom.css files. Some utilities require to copy sections from **both** custom.js **and** custom.css to work. Make sure to include the mutation observer declaration at the start of the custom.js)
46 | 3. Use the search function to find the relevant snippets delimited by comments with descriptive names. For custom.css, it's possible to add `@import url("https://cdn.jsdelivr.net/gh/cannibalox/logseq-custom-files@latest/custom.css");` at the beginning of your file.
47 |
48 | [^1]: - custom.js has been introduced in logseq on 2021-11-10, see details here https://github.com/logseq/logseq/pull/2943
49 | - The custom.js file is **not** created by the default installer; it has to be created manually in `/logseq`.
50 | - Before executing the code, the user will be asked for execution permission.
51 | - When the content of the custom.js file is modified, it needs to be restarted or refreshed to take effect.
52 |
53 | ## Help me improve the utilities
54 |
55 | - I'm glad to accept Pull Requests if you know how to improve or optimize the utilities.
56 | - If you find this useful, you can also buy me a coffee :)
57 | [](https://ko-fi.com/O5O1BN89Y)
58 |
59 | More js snippets and css customizations are coming soon, stay tuned
60 |
61 | ## changelog
62 |
63 | - **v20240317** : fix better-sidebar css to work with logseq 0.10.x - depreceting support for older logseq versions
64 | - **v20230709** : fix better-sidebar's arrow location for logseq 0.9.10
65 | - **v20230214** : new: add better-sidebar, fix: props data-refs (`bg-pic::`)
66 | - **v20220517** : new: add function to add properties to the data-refs attributes; new: add bg-pic attribute
67 | - **v20220517** : new: add function for tweet embed
68 | - **v20220331** : fix sorting : resizer handle was overlapping the table headers. moved style to custom.css
69 | - **v20220329** : fix for advanced queries
70 |
--------------------------------------------------------------------------------
/custom.css:
--------------------------------------------------------------------------------
1 | /* logseq table resizer v20220331 ======================= */
2 | /* add rules to custom.css =============================== */
3 | .table th {
4 | position: relative;
5 | }
6 | .query-table-resizer {
7 | position: relative;
8 | top: -20px;
9 | float: right;
10 | margin-bottom: -18px;
11 | cursor: col-resize;
12 | user-select: none;
13 | border-right: 1px solid var(--ls-border-color);
14 | width: 10px; /* hitbox width */
15 | height: 20px;
16 | }
17 |
18 | .query-table-resizer:active,
19 | .query-table-resizing {
20 | border-right: 2px solid rgb(255, 0, 0);
21 | }
22 |
23 | .custom-query table.table-auto {
24 | width: -webkit-fill-available;
25 | table-layout: fixed;
26 | }
27 | .custom-query .table-auto>thead>tr>th {
28 | border-bottom: 1px solid var(--ls-border-color);
29 | }
30 | .custom-query .table-auto>thead>tr>th {
31 | background-color: rgba(0, 0, 0, 0.1);
32 | padding: 3px 6px;
33 | }
34 | .custom-query .table-auto>tbody>tr>td.whitespace-nowrap {
35 | overflow-wrap: break-word;
36 | min-width: 20px;
37 | white-space: normal;
38 | font-weight: 300;
39 | font-size: 13px;
40 | }
41 | .custom-query .table-auto>tbody>tr>.whitespace-nowrap img {
42 | max-height: 120px;
43 | margin: 0;
44 | }
45 | /* ==================================== end of table resizer */
46 |
47 | /* ========== BETTER SIDEBAR - shrinking panes w/ horizontal scroll 20240317 =======================================*/
48 | :root {
49 | --sidebarItemMinWidth: 250px;
50 | --sidebarItemMaxWidth: 650px;
51 | }
52 |
53 | .cp__right-sidebar-inner.flex.flex-col.h-full {
54 | overflow: auto;
55 | padding-top: 50px;
56 | }
57 | .cp__right-sidebar.open {
58 | max-width: 80vw;
59 | background-color: var(--ls-secondary-background-color);
60 | height: 100%;
61 | }
62 | .sidebar-item-list {
63 | width: fit-content;
64 | height: 95vh !important;
65 | overflow-x: auto;
66 | overflow-y: hidden;
67 | display: flex;
68 | flex-direction: column;
69 | flex-wrap:wrap;
70 | align-content: flex-start;
71 | padding: 0 0px 0px 6px;
72 | }
73 | .cp__right-sidebar .sidebar-item {
74 | display: block;
75 | overflow-y: auto;
76 | min-width: var(--sidebarItemMinWidth) !important;
77 | max-width: var(--sidebarItemMaxWidth) !important;
78 | margin: 0px 3px;
79 | background-color: var(--ls-primary-background-color);
80 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.4);
81 | border: 1px solid black;
82 | border-radius: 6px;
83 | padding: 0.25rem 0;
84 | overflow-x: hidden;
85 | resize: horizontal;
86 | min-height: calc(95vh - 22px) !important;
87 | height: calc(95vh - 22px) !important;
88 | }
89 | .cp__right-sidebar .resizer {
90 | width: 6px;
91 | transition-delay: 0.1s;
92 | z-index: 10;
93 | }
94 | .cp__right-sidebar .resizer:hover {
95 | background-color: var(--ph-highlight-strong);
96 | }
97 |
98 | /* sliding panes */
99 | .sidebar-item.collapsed {
100 | display: inline-block;
101 | overflow: hidden;
102 | margin: 8px 3px;
103 | padding: 0.25px;
104 | resize: none;
105 | width: 38px !important;
106 | min-width: 38px !important;
107 | min-height: calc(95vh - 22px) !important;
108 | height: calc(95vh - 22px) !important;
109 | }
110 | .cp__right-sidebar .sidebar-item.collapsed .sidebar-item-header > button {
111 | width: 38px;
112 | align-items: start;
113 | margin: 12px 0;
114 | padding: 0.25rem;
115 | }
116 | .cp__right-sidebar .sidebar-item.collapsed .sidebar-item-header .ml-1.font-medium.overflow-hidden {
117 | margin-top: 1.5rem;
118 | }
119 | .sidebar-item.collapsed .rotating-arrow.collapsed {
120 | margin-top: 0px;
121 | }
122 | .sidebar-item.collapsed .rotating-arrow.collapsed svg {
123 | transform: rotate(180deg) translate(-2px, 5px);
124 | transition: none;
125 | }
126 | .sidebar-item.collapsed .ml-1.font-medium {
127 | margin-top: 2.5rem;
128 | transition: none;
129 | margin-left: -20px;
130 | }
131 | .cp__right-sidebar .sidebar-item .sidebar-item-header {
132 | position: sticky;
133 | background-color: var(--ls-secondary-background-color);
134 | z-index: 20;
135 | top:-6px;
136 | height: 36px;
137 | border-radius: 0;
138 | }
139 | .cp__right-sidebar .sidebar-item.collapsed .sidebar-item-header {
140 | position: static;
141 | height: auto;
142 | width:36px;
143 | }
144 | .cp__right-sidebar .sidebar-item.collapsed > .flex.flex-col.w-full.relative > .sidebar-item-header > button > div.ml-1 > div {
145 | writing-mode: vertical-lr !important;
146 | }
147 | .cp__right-sidebar .sidebar-item.collapsed > .flex.flex-col.w-full.relative > .sidebar-item-header > button > div.ml-1 > div > span {
148 | padding: 0.5rem 0 0 0;
149 | }
150 | .cp__right-sidebar .sidebar-item.collapsed .item-actions.flex.items-center {
151 | align-items: start;
152 | position: absolute;
153 | top: 84vh;
154 | }
155 |
156 | /* ======================================== end of BETTER-SIDEBAR =====*/
157 | /* ==== hide special props ==========================*/
158 | div[data-refs-self="bg-pic"] {
159 | display: none !important;
160 | }
161 |
--------------------------------------------------------------------------------
/custom.js:
--------------------------------------------------------------------------------
1 | // common =================================================================
2 | MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
3 | const watchTarget = document.getElementById("app-container");
4 | // throttle MutationObserver
5 | // from https://stackoverflow.com/a/52868150
6 | const throttle = (func, limit) => {
7 | let inThrottle;
8 | return (...args) => {
9 | if (!inThrottle) {
10 | func(...args);
11 | inThrottle = setTimeout(() => (inThrottle = false), limit);
12 | }
13 | };
14 | };
15 | // ================================== END COMMON
16 |
17 | // query table resizer ==============================================
18 | // source : https://htmldom.dev/resize-columns-of-a-table/
19 | console.log("========= query table resizer v20220312 ============");
20 |
21 | const createResizableColumn = function (col, resizer) {
22 | // Track the current position of mouse
23 | let x = 0;
24 | let w = 0;
25 |
26 | const mouseDownHandler = function (e) {
27 | // Get the current mouse position
28 | x = e.clientX;
29 |
30 | // Calculate the current width of column
31 | const styles = window.getComputedStyle(col);
32 | w = parseInt(styles.width, 10);
33 |
34 | // Attach listeners for document's events
35 | document.addEventListener("mousemove", mouseMoveHandler);
36 | document.addEventListener("mouseup", mouseUpHandler);
37 | };
38 |
39 | const mouseMoveHandler = function (e) {
40 | // Determine how far the mouse has been moved
41 | const dx = e.clientX - x;
42 | // Update the width of column
43 | col.style.width = `${w + dx}px`;
44 | };
45 |
46 | // When user releases the mouse, remove the existing event listeners
47 | const mouseUpHandler = function () {
48 | document.removeEventListener("mousemove", mouseMoveHandler);
49 | document.removeEventListener("mouseup", mouseUpHandler);
50 | };
51 | resizer.addEventListener("mousedown", mouseDownHandler);
52 | };
53 |
54 | const updateTables = function () {
55 | // Query the table
56 | const table = document.querySelectorAll(".table-auto:not(.table-resizable)");
57 | for (let i = 0; i < table.length; i++) {
58 | // Query all headers1
59 | const cols = table[i].querySelectorAll("thead tr > th.whitespace-nowrap");
60 | // Loop ver them
61 | Array.from(cols).forEach((col) => {
62 | // Create a resizer element
63 | const resizer = document.createElement("div");
64 | resizer.classList.add("query-table-resizer");
65 | table[i].classList.add("table-resizable");
66 | console.info("-- injected div.query-table-resizer --");
67 | // Add a resizer element to the column
68 | col.appendChild(resizer);
69 | createResizableColumn(col, resizer);
70 | });
71 | }
72 | };
73 |
74 | const updateTablesThrottled = throttle(updateTables, 1000);
75 | const obsTable = new MutationObserver(updateTablesThrottled);
76 | obsTable.observe(watchTarget, {
77 | subtree: true,
78 | childList: true,
79 | });
80 | // ====================================================== query table resizer
81 |
82 | // namespace prefixes collapser =============================================
83 | function hideNamespace() {
84 | console.info("====== LS HIDE NAMESPACE v20220314 =====");
85 | let nmsp = document.querySelectorAll(
86 | 'a.page-ref[data-ref*="/"]:not(.hidden-namespace)'
87 | );
88 | for (var i = 0; i < nmsp.length; i++) {
89 | if (nmsp[i].innerText.indexOf("/") !== -1) {
90 | nmsp[i].innerHTML =
91 | ".." +
92 | nmsp[i].innerText.substring(nmsp[i].innerText.lastIndexOf("/"));
93 | nmsp[i].classList.add("hidden-namespace");
94 | //console.info(" namespace off ==> " + nmsp[i].innerText);
95 | }
96 | }
97 | }
98 |
99 | const updateHideNamespace = throttle(hideNamespace, 1000);
100 | const obsNamespace = new MutationObserver(updateHideNamespace);
101 | obsNamespace.observe(watchTarget, {
102 | subtree: true,
103 | attributes: true,
104 | });
105 | //===================================== end of namespace prefixes collapser
106 |
107 | // property data-refs =====================================
108 | // injects [data-refs-self='property'] attributes to property divs
109 | // to be used in the next functions + custom.css
110 |
111 | console.log("========= property data-ref v20220715 ============");
112 | const addPropDataRef = function () {
113 | console.log("addPropDataRef running...");
114 | const propertiesBlocks = document.querySelectorAll(
115 | "#main-content-container .page.relative > .relative .block-properties:not(.datarefd)" //.page.relative > .relative => main container only
116 | );
117 | for (let i = 0; i < propertiesBlocks.length; i++) {
118 | const propertySpan = propertiesBlocks[i].children;
119 | Array.from(propertySpan).forEach((divProp) => {
120 | console.log(" divProp : ", divProp);
121 | let propName = divProp.firstChild.innerText;
122 | console.log(" property : ", propName);
123 | divProp.setAttribute("data-refs-self", propName);
124 | switch (propName) {
125 | case "cover-pic":
126 | document
127 | .querySelector(".page.relative > .relative .page-blocks-inner")
128 | .classList.add("has-coverPic");
129 | console.log(" .has-coverPic injected");
130 | break;
131 | case "cover-pic-height":
132 | console.log(" .has-coverPic injected");
133 | break; // TODO
134 | }
135 | });
136 | propertiesBlocks[i].classList.add("datarefd");
137 | }
138 | };
139 |
140 | const addPropDataRefThrottled = throttle(addPropDataRef, 1000);
141 | const obsProps = new MutationObserver(addPropDataRefThrottled);
142 | obsProps.observe(
143 | watchTarget,
144 | {
145 | subtree: true,
146 | childList: true,
147 | }
148 | );
149 |
150 | // =====================================end of property data-refs
151 |
152 | // add bg-pic =======================================
153 | console.log("========= bg-pic v20220327 ============");
154 | const addbgPic = function () {
155 | console.log("addbgPic running...");
156 | const bgPic = document.querySelectorAll(
157 | "[data-refs-self='bg-pic']"
158 | );
159 | console.log("has bgpic : ", bgPic.length);
160 | if (bgPic.length > 0) {
161 | const bgPica = Array.from(bgPic).filter(
162 | (item) => !item.closest(".references-blocks")
163 | );
164 | console.log("bg-pic exists : ", bgPica.length);
165 | console.log("bg-pic parent : ", bgPica);
166 | if (bgPica.length > 0) {
167 | const bgPicUrl = bgPica[0].getElementsByTagName("img")[0].src;
168 | console.log("bg-pic url : ", bgPicUrl);
169 | document.getElementById(
170 | "main-content-container"
171 | ).style.backgroundImage = "url(" + bgPicUrl + ")";
172 | }
173 | } else {
174 | document.getElementById("main-content-container").removeAttribute("style");
175 | };
176 | };
177 | const addbgPicThrottled = throttle(addbgPic, 1000);
178 | const addbg = new MutationObserver(addbgPicThrottled);
179 | addbg.observe(watchTarget, {
180 | subtree: true,
181 | childList: true,
182 | });
183 | // =====================================end of bg-pic
184 |
185 | // ============ BETTER-SIDEBAR rotate closed tabs in right sidebar=========
186 | // ============ remove if you don't use the better-sidebar.css=============
187 | console.log("========= rsidebar fold 90° ============");
188 | const foldTab = function () {
189 | let foldedTab = document.querySelectorAll(
190 | ".sidebar-item.content > .flex.flex-col > .flex.flex-row"
191 | );
192 | if (foldedTab.length > 0) {
193 | let foldedTabsArray = Array.from(foldedTab);
194 | console.log("sidebar tabs : ", foldedTabsArray.length);
195 | for (let i = 0; i < foldedTabsArray.length; i++) {
196 | if (foldedTabsArray[i].nextElementSibling.classList.contains("hidden")) {
197 | // console.log("fold detected: ", foldedTabsArray[i].nextElementSibling, " is folded.");
198 | let tab = foldedTabsArray[i].closest(".sidebar-item.content");
199 | tab.classList.add("folded");
200 | } else {
201 | if (
202 | foldedTabsArray[i].nextElementSibling.classList.contains("initial") &&
203 | foldedTabsArray[i]
204 | .closest(".sidebar-item.content")
205 | .classList.contains("folded")
206 | ) {
207 | //console.log("this one is unfolded !!!");
208 | let tab = foldedTabsArray[i].closest(".sidebar-item.content");
209 | tab.classList.remove("folded");
210 | }
211 | }
212 | }
213 | }
214 | };
215 | const foldTabthrottled = throttle(foldTab, 300);
216 | const foldTabs = new MutationObserver(foldTabthrottled);
217 | const sidebarTarget = document.querySelector(".sidebar-item-list");
218 | foldTabs.observe(watchTarget, {
219 | subtree: true,
220 | childList: true,
221 | attributes: true,
222 | });
223 | // =================== end of rotate closed tabs in right sidebar =========
224 |
225 | // ====== LS-TWITTER-EMBED =============================================
226 | console.info('====== LS-TWITTER-EMBED ======');
227 | // add twitter script and meta tags to head
228 | var s = document.createElement("script");
229 | s.type = "text/javascript";
230 | s.src = "https://platform.twitter.com/widgets.js";
231 | s.async = true;
232 | var m = document.createElement("meta");
233 | m.name = "twitter:widgets:theme";
234 | m.content = "dark";
235 | document.head.append(s, m);
236 |
237 | function embedTwitter() {
238 | let isTweet = document.querySelectorAll(
239 | "a.external-link[href^='https://twitter.com"
240 | );
241 | for (let i = 0; i < isTweet.length; i++) {
242 | if (isTweet[i].children[0] === undefined) {
243 | var requestUrl =
244 | "https://publish.twitter.com/oembed?omit_script=1&url=" +
245 | isTweet[i].href + "&limit=8&theme=dark&maxwidth=550&maxheight=600";
246 | var oReq = new XMLHttpRequest();
247 | oReq.onreadystatechange = function () {
248 | if (this.readyState == 4 && this.status == 200) {
249 | var data = JSON.parse(this.responseText);
250 | console.log(
251 | 'requestUrl : ', requestUrl,
252 | '\noReq.response : ', oReq.response,
253 | '\ndata : ', data,
254 | '\ndata.html : ', data.html
255 | );
256 | insertResponse(data, i);
257 | }
258 | }
259 | oReq.open("GET", requestUrl, true);
260 | oReq.send();
261 | }
262 | }
263 |
264 | function insertResponse(data, i) {
265 | var insTw = document.createElement("div");
266 | insTw.className = "twembed";
267 | insTw.innerHTML = data.html;
268 | isTweet[i].appendChild(insTw);
269 | console.log("embedding Tweets...");
270 | twttr.widgets.load();
271 | }
272 | };
273 | const embTwthrottled = throttle(embedTwitter, 1000);
274 | const embTw = new MutationObserver(embTwthrottled);
275 | const embTwTarget = document.getElementById('main-container');
276 | embTw.observe(embTwTarget, {
277 | subtree: true,
278 | childList: true,
279 | });
280 | // =============================================== end of twitter embed =
281 |
--------------------------------------------------------------------------------