Skip to content

Commit

Permalink
support for tabs in BS4 (#1694)
Browse files Browse the repository at this point in the history
Co-authored-by: Hadley Wickham <[email protected]>
  • Loading branch information
maelle and hadley authored Jun 18, 2021
1 parent 9ce6af4 commit 51268b0
Show file tree
Hide file tree
Showing 6 changed files with 369 additions and 2 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# pkgdown (development version)

* pkgdown, for Bootstrap 4, supports tabsets in articles [as in R Markdown](https://bookdown.org/yihui/rmarkdown-cookbook/html-tabs.html) including [fading effect](https://bookdown.org/yihui/rmarkdown/html-document.html#tabbed-sections) (@JamesHWade, #1667).

* New template option `trailingslash_redirect` that allows adding a script to redirect `your-package-url.com` to `your-package-url.com/`. (#1439, @cderv, @apreshill)

* `build_reference()` now runs examples with two more local options `rlang_interactive = FALSE` (therefore ensuring non-interactive behavior even in interactive sessions -- see `rlang::is_interactive()`) and `cli.dynamic = FALSE`, `withr::local_envvar(RSTUDIO = NA)` and `withr::local_collate("C")`(#1693).
Expand Down
143 changes: 141 additions & 2 deletions R/html-tweak.R
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ tweak_class_prepend <- function(x, class) {
invisible()
}

has_class <- function(html, class) {
classes <- strsplit(xml2::xml_attr(html, "class"), " ")
purrr::map_lgl(classes, ~ class %in% .x)
}

# from https://github.com/rstudio/bookdown/blob/ed31991df3bb826b453f9f50fb43c66508822a2d/R/bs4_book.R#L307
tweak_footnotes <- function(html) {
container <- xml2::xml_find_all(html, ".//div[@class='footnotes']")
Expand All @@ -129,6 +134,133 @@ tweak_footnotes <- function(html) {
xml2::xml_remove(container)
}

# Tabsets tweaking: find Markdown recommended in https://bookdown.org/yihui/rmarkdown-cookbook/html-tabs.html
# and https://bookdown.org/yihui/rmarkdown/html-document.html#tabbed-sections
# i.e. "## Heading {.tabset}" or "## Heading {.tabset .tabset-pills}"
# no matter the heading level -- the headings one level down are the tabs
# and transform to tabsets HTML a la Bootstrap

tweak_tabsets <- function(html) {
tabsets <- xml2::xml_find_all(html, ".//div[contains(@class, 'tabset')]")
purrr::walk(tabsets, tweak_tabset)
invisible(html)
}

tweak_tabset <- function(html) {
id <- xml2::xml_attr(html, "id")

# Users can choose pills or tabs
nav_class <- if (has_class(html, "tabset-pills")) {
"nav-pills"
} else {
"nav-tabs"
}
# Users can choose to make content fade
fade <- has_class(html, "tabset-fade")

# Get tabs and remove them from original HTML
tabs <- xml2::xml_find_all(html, "div")
xml2::xml_remove(tabs)

# Add empty ul for nav and div for content
xml2::xml_add_child(
html,
"ul",
class = sprintf("nav %s nav-row", nav_class),
id = id,
role = "tablist"
)
xml2::xml_add_child(html, "div", class="tab-content")

# Fill the ul for nav and div for content
purrr::walk(tabs, tablist_item, html = html, parent_id = id)
purrr::walk(tabs, tablist_content, html = html, parent_id = id, fade = fade)

# activate first tab unless another one is already activated
# (by the attribute {.active} in the source Rmd)
nav_links <- xml2::xml_find_all(html, sprintf("//ul[@id='%s']/li/a", id))

if (!any(has_class(nav_links, "active"))) {
tweak_class_prepend(nav_links[1], "active")
}

content_div <- xml2::xml_find_first(html, sprintf("//div[@id='%s']/div", id))
if (!any(has_class(xml2::xml_children(content_div), "active"))) {
tweak_class_prepend(xml2::xml_child(content_div), "active")
if (fade) {
tweak_class_prepend(xml2::xml_child(content_div), "show")
}
}
}

# Add an item (tab) to the tablist
tablist_item <- function(tab, html, parent_id) {
id <- xml2::xml_attr(tab, "id")
text <- xml_text1(xml2::xml_child(tab))
ul_nav <- xml2::xml_find_first(html, sprintf("//ul[@id='%s']", parent_id))

# Activate (if there was "{.active}" in the source Rmd)
active <- has_class(tab, "active")
class <- if (active) {
"nav-link active"
} else {
"nav-link"
}

xml2::xml_add_child(
ul_nav,
"a",
text,
`data-toggle` = "tab",
href = paste0("#", id),
role = "tab",
`aria-controls` = id,
`aria-selected` = tolower(as.character(active)),
class = class
)

# tab a's need to be wrapped in li's
xml2::xml_add_parent(
xml2::xml_find_first(html, sprintf("//a[@href='%s']", paste0("#", id))),
"li",
role = "presentation",
class = "nav-item"
)
}

# Add content of a tab to a tabset
tablist_content <- function(tab, html, parent_id, fade) {
active <- has_class(tab, "active")

# remove first child, that is the header
xml2::xml_remove(xml2::xml_child(tab))

xml2::xml_attr(tab, "class") <- "tab-pane"
if (fade) {
tweak_class_prepend(tab, "fade")
}

# Activate (if there was "{.active}" in the source Rmd)
if (active) {
tweak_class_prepend(tab, "active")
if (fade) {
tweak_class_prepend(tab, "show")
}
}

xml2::xml_attr(tab, "role") <- "tabpanel"
xml2::xml_attr(tab, " aria-labelledby") <- xml2::xml_attr(tab, "id")

content_div <- xml2::xml_find_first(
html,
sprintf("//div[@id='%s']/div", parent_id)
)

xml2::xml_add_child(content_div, tab)
}



# File level tweaks --------------------------------------------

tweak_rmarkdown_html <- function(html, input_path, pkg = pkg) {
Expand All @@ -137,13 +269,20 @@ tweak_rmarkdown_html <- function(html, input_path, pkg = pkg) {
tweak_anchors(html, only_contents = FALSE)
tweak_md_links(html)
tweak_all_links(html, pkg = pkg)
if (pkg$bs_version > 3) tweak_footnotes(html)

if (pkg$bs_version > 3) {
# Tweak footnotes
tweak_footnotes(html)

# Tweak tabsets
tweak_tabsets(html)
}

# Tweak classes of navbar
toc <- xml2::xml_find_all(html, ".//div[@id='tocnav']//ul")
xml2::xml_attr(toc, "class") <- "nav nav-pills nav-stacked"

# Mame sure all images use relative paths
# Make sure all images use relative paths
img <- xml2::xml_find_all(html, "//img")
src <- xml2::xml_attr(img, "src")
abs_src <- is_absolute_path(src)
Expand Down
31 changes: 31 additions & 0 deletions inst/assets/BS4/pkgdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,34 @@ summary {
details p {
margin-top: -.5rem;
}

/* tabsets */
.nav-row {
flex-direction: row;
}

.tab-content {
padding: 1rem;
}

.tabset-pills .tab-content {
border: solid 1px #e5e5e5;
}

/* https://observablehq.com/@rkaravia/css-trick-tabs-with-consistent-height */
/* Make tab height consistent */

.tab-content {
display: flex;
}

.tab-content > .tab-pane {
display: block; /* undo "display: none;" */
visibility: hidden;
margin-right: -100%;
width: 100%;
}

.tab-content > .active {
visibility: visible;
}
78 changes: 78 additions & 0 deletions tests/testthat/_snaps/html-tweak.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,81 @@
[1] <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role= ...
[2] <div class="dropdown-menu" aria-labelledby="navbarDropdown">\n <a clas ...

# tweak_tabsets() default

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div id="results-in-tabset" class="section level2 tabset">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>


<ul class="nav nav-tabs nav-row" id="results-in-tabset" role="tablist">
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-1" role="tab" aria-controls="tab-1" aria-selected="false" class="active nav-link">Tab 1</a></li>
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-2" role="tab" aria-controls="tab-2" aria-selected="false" class="nav-link">Tab 2</a></li>
</ul>
<div class="tab-content">
<div id="tab-1" class="active tab-pane" role="tabpanel" aria-labelledby="tab-1">

<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="tab-pane" role="tabpanel" aria-labelledby="tab-2">

<p>blop</p>
</div>
</div>
</div></body></html>

# tweak_tabsets() with tab pills and second tab active

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div id="results-in-tabset" class="section level2 tabset tabset-pills">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>


<ul class="nav nav-pills nav-row" id="results-in-tabset" role="tablist">
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-1" role="tab" aria-controls="tab-1" aria-selected="false" class="nav-link">Tab 1</a></li>
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-2" role="tab" aria-controls="tab-2" aria-selected="true" class="nav-link active">Tab 2</a></li>
</ul>
<div class="tab-content">
<div id="tab-1" class="tab-pane" role="tabpanel" aria-labelledby="tab-1">

<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="active tab-pane" role="tabpanel" aria-labelledby="tab-2">

<p>blop</p>
</div>
</div>
</div></body></html>

# tweak_tabsets() with tab pills, fade and second tab active

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div id="results-in-tabset" class="section level2 tabset tabset-pills tabset-fade">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>


<ul class="nav nav-pills nav-row" id="results-in-tabset" role="tablist">
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-1" role="tab" aria-controls="tab-1" aria-selected="false" class="nav-link">Tab 1</a></li>
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-2" role="tab" aria-controls="tab-2" aria-selected="true" class="nav-link active">Tab 2</a></li>
</ul>
<div class="tab-content">
<div id="tab-1" class="fade tab-pane" role="tabpanel" aria-labelledby="tab-1">

<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="show active fade tab-pane" role="tabpanel" aria-labelledby="tab-2">

<p>blop</p>
</div>
</div>
</div></body></html>

66 changes: 66 additions & 0 deletions tests/testthat/test-html-tweak.R
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,69 @@ test_that("activate_navbar()", {
xml2::xml_find_first(navbar, ".//li[contains(@class, 'active')]")
)
})

# tabsets -------------------------------------------------------------

test_that("tweak_tabsets() default", {
html <- '<div id="results-in-tabset" class="section level2 tabset">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>
<div id="tab-1" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-1" class="anchor" aria-hidden="true"></a>Tab 1</h3>
<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-2" class="anchor" aria-hidden="true"></a>Tab 2</h3>
<p>blop</p>
</div>
</div>'
new_html <- tweak_tabsets(xml2::read_html(html))
expect_snapshot_output(cat(as.character(new_html)))
})

test_that("tweak_tabsets() with tab pills and second tab active", {
html <- '<div id="results-in-tabset" class="section level2 tabset tabset-pills">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>
<div id="tab-1" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-1" class="anchor" aria-hidden="true"></a>Tab 1</h3>
<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="section level3 active">
<h3 class="hasAnchor">
<a href="#tab-2" class="anchor" aria-hidden="true"></a>Tab 2</h3>
<p>blop</p>
</div>
</div>'
new_html <- tweak_tabsets(xml2::read_html(html))
expect_snapshot_output(cat(as.character(new_html)))
})


test_that("tweak_tabsets() with tab pills, fade and second tab active", {
html <- '<div id="results-in-tabset" class="section level2 tabset tabset-pills tabset-fade">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>
<div id="tab-1" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-1" class="anchor" aria-hidden="true"></a>Tab 1</h3>
<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="section level3 active">
<h3 class="hasAnchor">
<a href="#tab-2" class="anchor" aria-hidden="true"></a>Tab 2</h3>
<p>blop</p>
</div>
</div>'
new_html <- tweak_tabsets(xml2::read_html(html))
expect_snapshot_output(cat(as.character(new_html)))
})
Loading

0 comments on commit 51268b0

Please sign in to comment.