Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for tabs / tabset panels #571

Merged
merged 6 commits into from
Apr 5, 2024
Merged
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
238 changes: 236 additions & 2 deletions inst/rmarkdown/lua/lesson.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ local blocks = {
["discussion"] = "message-circle",
["testimonial"] = "heart",
["keypoints"] = "key",
["instructor"] = "edit-2"
["instructor"] = "edit-2",
["tab"] = "none",
["group-tab"] = "none"
}

local block_counts = {
Expand All @@ -36,7 +38,9 @@ local block_counts = {
["discussion"] = 0,
["testimonial"] = 0,
["keypoints"] = 0,
["instructor"] = 0
["instructor"] = 0,
["tab"] = 0,
["group-tab"] = 0
}

-- get the timing elements from the metadata and store them in a global var
Expand Down Expand Up @@ -192,6 +196,10 @@ local button_headings = {
<div class="note-square"><i aria-hidden="true" class="callout-icon" data-feather="eye"></i></div>
{{title}}
</h3>]],
["tab"] = [[
<h3 class="tab-header" id="nav-tab-heading-{{id}}">
{{title}}
</h3>]],
}

local accordion_titles = {
Expand Down Expand Up @@ -268,6 +276,214 @@ accordion = function(el, class)
return(main_div)
end

-- For a single tab block:
-- Store the current tab button number
local tab_button_num = 0
-- Store the tab button number a tabpanel
-- element thinks it is on
local tabpanel_tab_button_num = 1

-- Stores the tab nav buttons
local tab_buttons = {}
-- Stores the elements to form the tabpanel
-- content for the tab currently being processed
local this_tab_tabpanel = {}
-- Stores a tab blocks, tabpanel content
local tabpanels = {}

-- Are we processing a group-tab?
local group_tab = false
local group_tab_titles = {}

local tab_button = [[
<button class="nav-link" id="nav-tab-{{id}}" {{name}} data-bs-toggle="tab" data-bs-target="#nav-tabpanel-{{id}}" type="button" role="tab" aria-controls="nav-tabpanel-{{id}}" aria-selected="false">
{{heading}}
</button>]]

-- The first tab button is active
local tab_button_active = [[
<button class="nav-link active" id="nav-tab-{{id}}" {{name}} data-bs-toggle="tab" data-bs-target="#nav-tabpanel-{{id}}" type="button" role="tab" aria-controls="nav-tabpanel-{{id}}" aria-selected="true">
{{heading}}
</button>]]

add_to_tabpanel = function(el)
-- If the tabpanel_button_number is the same
-- as the nav tab_button_number this element
-- belongs with the current nav button so store
-- it for later
if tabpanel_tab_button_num == tab_button_num then
table.insert(this_tab_tabpanel, el)
-- Else we have hit the next tab button and should
-- wrap the tabpanel content we stored in the
-- this_tab_tabpanel table for the previous button
else
local id
if group_tab then
local tab_id = block_counts["group-tab"]
id = tab_id.."-"..group_tab_titles[tabpanel_tab_button_num]
else
local tab_id = block_counts["tab"]
id = tab_id.."-"..tabpanel_tab_button_num
end

-- Wrap the tabpanel contents in a div
local tabpanel_div = pandoc.Div(this_tab_tabpanel)
-- n.b. in pandoc 2.17, the attributes must be set after the classes
if tabpanel_tab_button_num == 1 then
tabpanel_div.classes = {"tab-pane show active"}
else
tabpanel_div.classes = {"tab-pane"}
end
tabpanel_div.identifier = "nav-tabpanel-"..id
tabpanel_div.attributes = {
['role'] = 'tabpanel',
['aria-labelledby'] = "nav-tab-"..id,
}

-- Store the div for the tab_block function
table.insert(tabpanels, tabpanel_div)

-- We move onto the next button having processed
-- the previous buttons tabpanel content
tabpanel_tab_button_num = tabpanel_tab_button_num + 1

-- The current element belongs to the new button
-- so empty out the this_tab_tabpanel table and store the el
this_tab_tabpanel = {}
table.insert(this_tab_tabpanel, el)
end
end

tab_filter = {
Header = function(el)
-- Level 3 headers mark the tab titles
-- all other headers in a tab block are ignored
if el.level == 3 then

-- Insert the title for the add_to_tabpanel to access
local title = pandoc.utils.stringify(el)

local id
local name
if group_tab then
local tab_id = block_counts["group-tab"]
local title_no_spaces = title:gsub("%s+", "")
id = tab_id.."-"..title_no_spaces
-- The JS for the group tabs selects buttons
-- to show based on the name attribute.
-- Here we set it to the button title.
name = 'name="'..title_no_spaces..'"'
-- Store the title so it can be used in the tabpanel id
table.insert(group_tab_titles, title_no_spaces)
else
local tab_id = block_counts["tab"]
id = tab_id.."-"..tab_button_num+1
-- Non group tabs don't need a name attribute.
name = ""
end

-- Found another button so increment the
-- current tab_button_num
tab_button_num = tab_button_num + 1

-- Create the button, if this is the first
-- button it needs to be active
local this_button
if tab_button_num == 1 then
this_button = tab_button_active
else
this_button = tab_button
end

-- Substitute in the button information
this_button = this_button:gsub("{{heading}}", button_headings["tab"])
this_button = this_button:gsub("{{title}}", title)
this_button = this_button:gsub("{{id}}", id)
this_button = this_button:gsub("{{name}}", name)

-- Convert the tab button to a raw block and store
local button = pandoc.RawBlock("html", this_button)
table.insert(tab_buttons, button)
end
end,
-- for all other elements process them using
-- the add_to_tabpanel function
Para = function(el)
_ = add_to_tabpanel(el)
end,
Div = function(el)
_ = add_to_tabpanel(el)
end,
Figure = function(el)
_ = add_to_tabpanel(el)
end,
CodeBlock = function(el)
_ = add_to_tabpanel(el)
end,
OrderedList = function(el)
_ = add_to_tabpanel(el)
end,
BulletList = function(el)
_ = add_to_tabpanel(el)
end
}

tab_block = function(el)

-- Increment the tab count
local count
if group_tab then
block_counts["group-tab"] = block_counts["group-tab"] + 1
count = block_counts["group-tab"]
else
block_counts["tab"] = block_counts["tab"] + 1
count = block_counts["tab"]
end

-- Walk the tab elements and process them
_ = pandoc.walk_block(el,tab_filter)

-- Wraps the tab buttons to create the tablist div
local button_div_id = "nav-tab-"..count
local button_div = pandoc.Div(tab_buttons)
button_div.identifier = button_div_id
button_div.classes = {"nav", "nav-tabs"}
button_div.attributes = {
['role'] = 'tablist',
}

-- The tab_filter uses the current tab number
-- to determine whether we have reached the next tab
-- This tricks the add_to_tabpanel function into thinking
-- it has hit the number of tabs + 1 so it wraps
-- the last tabpanel in a div
tab_button_num = tab_button_num + 1
_ = add_to_tabpanel(tabpanels)

-- Wraps the tabpanels
local tab_content_div = pandoc.Div(tabpanels)
local tab_content_div_id = "nav-tabContent-"..count
tab_content_div.identifier = tab_content_div_id
tab_content_div.classes = {"tab-content"}

-- Create the nav html tags
local nav_start = pandoc.RawBlock("html", "<nav>")
local nav_end = pandoc.RawBlock("html", "</nav>")

-- Put everything in a tabs div
local tabs = pandoc.Div({nav_start, button_div, nav_end, tab_content_div})
tabs.classes = {"tabs"}

-- Reset counters for the next tab block
tab_button_num = 0
tabpanel_tab_button_num = 1
tab_buttons = {}
this_tab_tabpanel = {}
tabpanels = {}

return tabs
end

callout_block = function(el)
local classes = el.classes:map(pandoc.utils.stringify)
local this_icon = blocks[classes[1]]
Expand Down Expand Up @@ -402,6 +618,24 @@ handle_our_divs = function(el)
return(challenge_block(el))
end

-- Tab blocks:
--
-- Toggleable Tab blocks.
v,i = el.classes:find("tab")
if i ~= nil then
group_tab = false
return(tab_block(el))
end

-- Group Tab blocks:
--
-- Toggleable Group Tab blocks.
v,i = el.classes:find("group-tab")
if i ~= nil then
group_tab = true
return(tab_block(el))
end

-- All other Div tags should have at most level 3 headers
level_head(el, 3)
return(callout_block(el))
Expand Down
Loading