├── 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 | ![20220312_NUC8_M49yriOEAH](https://user-images.githubusercontent.com/4605693/158709862-5eb0917f-8b84-4c0b-be9e-bf84eda4e042.gif) 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 | ![20220314_NUC8_cMu56YIkrd](https://user-images.githubusercontent.com/4605693/158709836-762e4274-6604-4df8-9d1f-3d0260c6545c.gif) 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 | ![Logseq_DASHBOARD_20220517_1586](https://user-images.githubusercontent.com/4605693/168820686-4af1e0b5-e638-4b00-ac23-0fce80427755.png) 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 | ![ss_Logseq_All_pages_20230213_V5ihMcrohP](https://user-images.githubusercontent.com/4605693/218562643-542a8455-1845-43df-ab90-d89d87cdb5cd.gif) 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 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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 | --------------------------------------------------------------------------------