Skip to content

Instantly share code, notes, and snippets.

@stephanschielke
Last active May 6, 2025 16:06
Show Gist options
  • Save stephanschielke/7a9fae189cdf138400376c7789b4ef37 to your computer and use it in GitHub Desktop.
Save stephanschielke/7a9fae189cdf138400376c7789b4ef37 to your computer and use it in GitHub Desktop.
Collapsible/expandable custom properties for `Logseq` `TODO` blocks with query by property values.
- TODO Important stuff [[Tue, 6th May 2025]]
priority:: 4
suggestions:: 🧩 Fit into next free slot, ⏰ Set a quick reminder, ⚡ Handle right away
urgency:: 🔴
impact:: 🟡
effort:: 🟢
pomodoro:: {{renderer :pomodoro_pevja,25}}
:LOGBOOK:
CLOCK: [2025-05-06 Tue 06:01:51]--[2025-05-06 Tue 06:01:52] => 00:00:01
:END:
- TODO Task [[Tue, 6th May 2025]]
priority:: 6
suggestions:: 🗂️ Skip or batch later, 📉 Tackle during downtime, 🕰️ Do when you get a minute
urgency:: 🟡
impact:: 🟢
effort:: 🟢
/* Header styling */
.ls-block .block-properties-header {
cursor: pointer;
display: flex;
align-items: center;
margin-top: 1px;
padding: 1px;
user-select: none;
font-size: 0.9em;
/* ensure it sits above block-content */
z-index: 1;
}
.ls-block .block-properties-header .arrow {
margin-left: 8px;
margin-right: 3px;
font-weight: lighter;
transition: transform 0.2s;
}
/* Hide ONLY todo properties by default (not page properties) */
.ls-block .block-properties.todo-properties,
.block-content-wrapper .block-properties.todo-properties {
display: none;
margin-left: 25px;
padding: 5px;
}
/* Show when expanded */
.ls-block .block-properties.todo-properties.expanded,
.block-content-wrapper .block-properties.todo-properties.expanded {
display: block;
}
/* Keep page properties visible */
.ls-block .block-properties.page-properties {
display: block;
}
console.log('custom.js loaded');
function initCollapsibleProperties() {
// Find all TODO and DONE blocks, including those in the new wrapper structure
document
.querySelectorAll('.ls-block .block-content .inline.todo, .ls-block .block-content .inline.done, .block-content-wrapper .block-content .inline.done')
.forEach(todoElement => {
// Find the block-content element, handling both direct and wrapped structures
const blockContent = todoElement.closest('.block-content');
if (!blockContent) return;
// Only target block-properties that are NOT page-properties
const propsElement = blockContent.querySelector('.block-properties:not(.page-properties)');
// Skip if no properties or already processed
if (!propsElement || blockContent.querySelector('.block-properties-header')) return;
// Add a special class to mark this as a todo-properties
propsElement.classList.add('todo-properties');
// Ensure it's hidden by default
propsElement.style.display = 'none';
// Create header
const header = document.createElement('div');
header.className = 'block-properties-header collapsed';
header.innerHTML = '<span class="arrow">⨁</span> Properties';
// Stop Logseq's edit-handler on pointerdown & mousedown
['pointerdown', 'mousedown'].forEach(evt =>
header.addEventListener(evt, e => {
e.stopImmediatePropagation();
}, { capture: true })
);
// Toggle on click (capture-phase) and prevent Logseq from seeing it
header.addEventListener('click', e => {
e.preventDefault();
e.stopImmediatePropagation();
const expanded = propsElement.classList.toggle('expanded');
header.classList.toggle('collapsed', !expanded);
header.classList.toggle('expanded', expanded);
header.querySelector('.arrow').textContent = expanded ? '⊖' : '⨁';
propsElement.style.display = expanded ? 'block' : 'none';
}, { capture: true });
// Insert header and start collapsed
propsElement.before(header);
propsElement.classList.remove('expanded');
});
}
// Function to run initialization after a short delay
function delayedInit() {
setTimeout(initCollapsibleProperties, 100);
}
// Run on load and when the DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', delayedInit);
} else {
delayedInit();
}
// Also run when Logseq's main content is loaded
document.addEventListener('logseq:main-loaded', delayedInit);
// Watch for changes to the entire document
new MutationObserver(mutations => {
let shouldUpdate = false;
mutations.forEach(mutation => {
// Check for added nodes
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if this is a TODO/DONE block with properties
const todoBlock = node.querySelector?.('.inline.todo, .inline.done');
if (todoBlock) {
const blockContent = todoBlock.closest('.block-content');
if (blockContent) {
const hasProperties = blockContent.querySelector('.block-properties:not(.page-properties)');
if (hasProperties && !blockContent.querySelector('.block-properties-header')) {
shouldUpdate = true;
}
}
}
}
});
// Check for attribute changes (for when TODO changes to DONE)
if (mutation.type === 'attributes' &&
(mutation.target.classList.contains('todo') || mutation.target.classList.contains('done'))) {
shouldUpdate = true;
}
});
if (shouldUpdate) {
delayedInit();
}
}).observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
// Watch for scroll events to handle lazy loading
let scrollTimeout;
window.addEventListener('scroll', () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(delayedInit, 100);
}, { passive: true });
// Watch for visibility changes
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
delayedInit();
}
});
}, {
root: null,
rootMargin: '50px',
threshold: 0.1
});
// Observe the main content area
const mainContent = document.querySelector('.main-content-container');
if (mainContent) {
observer.observe(mainContent);
}
// Also observe any new blocks that are added
new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const blocks = node.querySelectorAll('.ls-block');
blocks.forEach(block => observer.observe(block));
}
});
});
}).observe(document.body, {
childList: true,
subtree: true
});
query-table:: true
query-sort-by:: impact
query-sort-desc:: false
query-properties:: [:priority :pomodoro :block :urgency :impact :effort :suggestions :page]
#+BEGIN_QUERY
{:title [:h3 "All (with priorities)"]
:query
[:find (pull ?b [*])
:where
;; only TODO or DOING blocks
[?b :block/marker ?marker]
[(contains? #{"TODO" "DOING"} ?marker)]
;; grab the properties map
[?b :block/properties ?props]
;; extract numeric priority
[(get ?props :priority) ?p]
;; filter for 1 ≤ priority ≤ 7
[(>= ?p 1)]
[(<= ?p 7)]]}
#+END_QUERY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment