fix(callout): Grid-based callout collapsible animation (#1944)

* Fixed broken nested callout maxHeight calculation

* Implemented grid-based callout collapsible
This commit is contained in:
Stephen Tse 2025-04-26 11:05:51 -07:00 committed by GitHub
parent 78e13bcb40
commit 4bd714b7be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 42 additions and 40 deletions

View file

@ -1,25 +1,10 @@
function toggleCallout(this: HTMLElement) {
const outerBlock = this.parentElement!
outerBlock.classList.toggle("is-collapsed")
const content = outerBlock.getElementsByClassName("callout-content")[0] as HTMLElement
if (!content) return
const collapsed = outerBlock.classList.contains("is-collapsed")
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
outerBlock.style.maxHeight = height + "px"
// walk and adjust height of all parents
let current = outerBlock
let parent = outerBlock.parentElement
while (parent) {
if (!parent.classList.contains("callout")) {
return
}
const collapsed = parent.classList.contains("is-collapsed")
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
parent.style.maxHeight = height + "px"
current = parent
parent = parent.parentElement
}
content.style.gridTemplateRows = collapsed ? "0fr" : "1fr"
}
function setupCallout() {
@ -27,15 +12,15 @@ function setupCallout() {
`callout is-collapsible`,
) as HTMLCollectionOf<HTMLElement>
for (const div of collapsible) {
const title = div.firstElementChild
if (!title) continue
const title = div.getElementsByClassName("callout-title")[0] as HTMLElement
const content = div.getElementsByClassName("callout-content")[0] as HTMLElement
if (!title || !content) continue
title.addEventListener("click", toggleCallout)
window.addCleanup(() => title.removeEventListener("click", toggleCallout))
const collapsed = div.classList.contains("is-collapsed")
const height = collapsed ? title.scrollHeight : div.scrollHeight
div.style.maxHeight = height + "px"
content.style.gridTemplateRows = collapsed ? "0fr" : "1fr"
}
}

View file

@ -464,6 +464,30 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
})
}
// For the rest of the MD callout elements other than the title, wrap them with
// two nested HTML <div>s (use some hacked mdhast component to achieve this) of
// class `callout-content` and `callout-content-inner` respectively for
// grid-based collapsible animation.
if (calloutContent.length > 0) {
node.children = [
node.children[0],
{
data: { hProperties: { className: ["callout-content"] }, hName: "div" },
type: "blockquote",
children: [
{
data: {
hProperties: { className: ["callout-content-inner"] },
hName: "div",
},
type: "blockquote",
children: [...calloutContent],
},
],
},
]
}
// replace first line of blockquote with title and rest of the paragraph text
node.children.splice(0, 1, ...blockquoteContent)
@ -485,21 +509,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
"data-callout-metadata": calloutMetaData,
},
}
// Add callout-content class to callout body if it has one.
if (calloutContent.length > 0) {
const contentData: BlockContent | DefinitionContent = {
data: {
hProperties: {
className: "callout-content",
},
hName: "div",
},
type: "blockquote",
children: [...calloutContent],
}
node.children = [node.children[0], contentData]
}
}
})
}

View file

@ -7,11 +7,19 @@
border-radius: 5px;
padding: 0 1rem;
overflow-y: hidden;
transition: max-height 0.3s ease;
box-sizing: border-box;
& > .callout-content > :first-child {
margin-top: 0;
& > .callout-content {
display: grid;
transition: grid-template-rows 0.3s ease;
& > .callout-content-inner {
overflow: hidden;
& > :first-child {
margin-top: 0;
}
}
}
--callout-icon-note: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="2" x2="22" y2="6"></line><path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path></svg>');