Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions src/resources/filters/customnodes/panel-tabset.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
--[[
Create a Tab AST node (represented as a Lua table)
]]
---@param params { content:nil|pandoc.Blocks|string, title:pandoc.Inlines|string, active:nil|boolean }
---@param params { content:nil|pandoc.Blocks|string, title:pandoc.Inlines|string, active:nil|boolean, identifier:nil|string }
---@return quarto.Tab
quarto.Tab = function(params)
local content
Expand All @@ -21,19 +21,21 @@ quarto.Tab = function(params)
if type(params.active) == "boolean" then
active = params.active
end
local identifier = params.identifier or ""

return {
active = active,
content = content,
title = pandoc.Inlines(params.title)
title = pandoc.Inlines(params.title),
identifier = identifier
}
end

local function render_quarto_tab(tbl, tabset)
local content = quarto.utils.as_blocks(tbl.content)
local title = quarto.utils.as_inlines(tbl.title)
local inner_content = pandoc.List()
local attr = pandoc.Attr("", {}, {})
local attr = pandoc.Attr(tbl.identifier or "", {}, {})
if tbl.active then
attr.classes:insert("active")
end
Expand All @@ -52,9 +54,10 @@ function parse_tabset_contents(div)
for i=1,#div.content do
local el = div.content[i]
if el.t == "Header" and el.level == level then
tab = quarto.Tab({
title = el.content,
active = el.attr.classes:includes("active")
tab = quarto.Tab({
title = el.content,
active = el.attr.classes:includes("active"),
identifier = el.attr.identifier
})
tabs:insert(tab)
elseif tab ~= nil then
Expand All @@ -70,6 +73,9 @@ end
local tabsetidx = 1

function render_tabset(attr, tabs, renderer)
-- Track used IDs to detect conflicts
local usedIds = {}

-- create a unique id for the tabset
local tabsetid = "tabset-" .. tabsetidx
tabsetidx = tabsetidx + 1
Expand Down Expand Up @@ -108,8 +114,27 @@ function render_tabset(attr, tabs, renderer)
local heading = tab.content[1]
tab.content:remove(1)

-- tab id
local tabid = tabsetid .. "-" .. i
-- Use custom ID if provided, otherwise auto-generate
local customId = heading.attr.identifier
local tabid

if customId and customId ~= "" then
-- Validate custom ID
if usedIds[customId] then
warn("Duplicate tab ID '" .. customId .. "' in tabset. Using auto-generated ID instead.")
tabid = tabsetid .. "-" .. i
elseif customId:match("^tabset%-") then
warn("Tab ID '" .. customId .. "' conflicts with auto-generated pattern. Using auto-generated ID instead.")
tabid = tabsetid .. "-" .. i
else
tabid = customId
end
else
tabid = tabsetid .. "-" .. i
end

print("DEBUG: Tab " .. i .. " using ID: '" .. tabid .. "'")
usedIds[tabid] = true
local tablinkid = tabid .. "-tab" -- FIXME unused from before?

-- navigation
Expand Down Expand Up @@ -155,7 +180,8 @@ _quarto.ast.add_handler({
__quarto_custom_node = node,
level = params.level or 2,
attr = params.attr or pandoc.Attr("", {"panel-tabset"}),
actives = params.tabs:map(function(tab) return tab.active end)
actives = params.tabs:map(function(tab) return tab.active end),
identifiers = params.tabs:map(function(tab) return tab.identifier or "" end)
}
local outer_custom_data = custom_data

Expand All @@ -166,6 +192,7 @@ _quarto.ast.add_handler({
}
local result = {
active = outer_custom_data.actives[index],
identifier = outer_custom_data.identifiers[index],
}
setmetatable(result, _quarto.ast.create_proxy_metatable(
function(key) return forwarder[key] end,
Expand Down
69 changes: 66 additions & 3 deletions src/resources/formats/html/templates/quarto-html-after-body.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,77 @@
<% } %>

<% if (tabby) { %>

const tabsets = window.document.querySelectorAll(".panel-tabset-tabby")
tabsets.forEach(function(tabset) {
const tabby = new Tabby('#' + tabset.id);
});

<% } %>


// Activate Bootstrap tab based on URL hash
const activateTabFromHash = function() {
if (!window.location.hash) return;

// Check if hash corresponds to a tab pane
const hash = window.location.hash;
const pane = document.querySelector(hash + '.tab-pane');

if (pane && window.bootstrap) {
// Find the tab link that targets this pane
const tabLink = document.querySelector('[data-bs-toggle="tab"][data-bs-target="' + hash + '"]');
if (tabLink) {
const tab = new window.bootstrap.Tab(tabLink);
tab.show();

// Scroll to the entire tabset after a brief delay to ensure it's shown
setTimeout(function() {
const tabset = pane.closest('.panel-tabset');
if (tabset) {
tabset.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
}
}
};

// Activate on page load
activateTabFromHash();

// Activate on hash change (when clicking anchor links)
window.addEventListener('hashchange', activateTabFromHash);

// Handle clicks on links to tab panes (even if hash is already set)
document.addEventListener('click', function(event) {
const link = event.target.closest('a[href^="#"]');
if (!link) return;

const hash = link.getAttribute('href');
const pane = document.querySelector(hash + '.tab-pane');

if (pane && window.bootstrap) {
const tabLink = document.querySelector('[data-bs-toggle="tab"][data-bs-target="' + hash + '"]');
if (tabLink) {
event.preventDefault();
const tab = new window.bootstrap.Tab(tabLink);
tab.show();

// Update URL without triggering hashchange
if (window.location.hash !== hash) {
window.location.hash = hash;
}

// Scroll to the entire tabset
setTimeout(function() {
const tabset = pane.closest('.panel-tabset');
if (tabset) {
tabset.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
}
}
});

<% if (anchors) { %>

const icon = "<%= anchors === true ? '' : anchors %>";
Expand Down
Loading