From 4f17b37ce1642c8e879a62dafd37020785bfa3c9 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 28 Feb 2023 11:52:29 +0100 Subject: [PATCH] make groups work with material plugin Refs #25, https://github.com/jonas/paradox-material-theme/issues/11 --- build.sbt | 5 +- .../org/apache/pekko/PekkoParadoxPlugin.scala | 6 +- .../main/assets/assets/javascripts/groups.js | 170 ++++++++++++++ .../javascripts/paradox-material-theme.js | 221 ++++++++++++++++++ theme/src/main/assets/page.st | 171 ++++++++++++++ 5 files changed, 571 insertions(+), 2 deletions(-) create mode 100644 theme/src/main/assets/assets/javascripts/groups.js create mode 100644 theme/src/main/assets/assets/javascripts/paradox-material-theme.js create mode 100644 theme/src/main/assets/page.st diff --git a/build.sbt b/build.sbt index 6221088..c27c980 100644 --- a/build.sbt +++ b/build.sbt @@ -48,7 +48,10 @@ lazy val pekkoTheme = project .settings( organization := "org.apache.pekko", name := "pekko-theme-paradox", - libraryDependencies += "io.github.jonas" % "paradox-material-theme" % "0.6.0") + libraryDependencies ++= Seq( + "io.github.jonas" % "paradox-material-theme" % "0.6.0", + "org.webjars" % "foundation" % "6.2.4" % "provided" + )) .settings(publishSettings) lazy val pekkoPlugin = project diff --git a/plugin/src/main/scala/org/apache/pekko/PekkoParadoxPlugin.scala b/plugin/src/main/scala/org/apache/pekko/PekkoParadoxPlugin.scala index 9fa2fe5..2901f47 100644 --- a/plugin/src/main/scala/org/apache/pekko/PekkoParadoxPlugin.scala +++ b/plugin/src/main/scala/org/apache/pekko/PekkoParadoxPlugin.scala @@ -46,5 +46,9 @@ object PekkoParadoxPlugin extends AutoPlugin { def pekkoParadoxSettings(config: Configuration): Seq[Setting[_]] = pekkoParadoxGlobalSettings ++ inConfig(config)(Seq( paradoxTheme / managedSourceDirectories += - (Assets / WebKeys.webJarsDirectory).value / (Assets / WebKeys.webModulesLib).value / "paradox-material-theme")) + (Assets / WebKeys.webJarsDirectory).value / (Assets / WebKeys.webModulesLib).value / "paradox-material-theme", + // we override some files from paradox-material-theme, so we must solve the ambiguity where to take those duplicates from + paradoxTheme / WebKeys.deduplicators += { (files: Seq[File]) => + files.find(_.getPath.contains("pekko-theme-paradox")) + })) } diff --git a/theme/src/main/assets/assets/javascripts/groups.js b/theme/src/main/assets/assets/javascripts/groups.js new file mode 100644 index 0000000..145ceaf --- /dev/null +++ b/theme/src/main/assets/assets/javascripts/groups.js @@ -0,0 +1,170 @@ +/*! + Adopted from original paradox generic theme to work with paradox-material-theme + https://github.com/lightbend/paradox/blob/8e30c341f1f8351a19b71599219d2f636ba68eb4/themes/generic/src/main/assets/js/groups.js + licensed under Apache License 2.0. +*/ + +groupChangeListeners = []; + +window.groupChanged = function(callback) { + groupChangeListeners.push(callback); +} + +$(function() { + + // Groups (like 'java' and 'scala') represent groups of 'switchable' content, either in tabs or in regular text. + // The catalog of groups can be defined in the sbt parameters to initialize the group. + + var groupCookie = "paradoxGroups"; + var cookieTg = getCookie(groupCookie); + var currentGroups = {}; + + var catalog = {} + var supergroupByGroup = {}; + + if(cookieTg != "") + currentGroups = JSON.parse(cookieTg); + + // https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie + function setCookie(cookieName, cookieValue, daysToExpire) { + if (!daysToExpire) daysToExpire = 365; + const now = new Date(); + now.setDate(now.getDate() + daysToExpire); + // The lax value will send the cookie for all same-site + // requests and top-level navigation GET requests. This + // is sufficient for user tracking, but it will prevent + // many CSRF attacks. This is the default value in modern browsers. + document.cookie = `${cookieName}=${encodeURIComponent(cookieValue)};expires=${now.toUTCString()};path=/;samesite=lax`; + } + + // https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_2_Get_a_sample_cookie_named_test2 + function getCookie(cookieName) { + const cookieAttr = decodeURIComponent(document.cookie) + .split(";") + .find(row => row.trimStart().startsWith(cookieName)) + return cookieAttr ? cookieAttr.split("=")[1] : ""; + } + + $("dl").has("dt").each(function() { + var dl = $(this); + dl.addClass("tabbed"); + var dts = dl.find("dt"); + dts.each(function(i) { + var dt = $(this); + dt.html("" + dt.text() + ""); + }); + var dds = dl.find("dd"); + dds.each(function(i) { + var dd = $(this); + dd.hide(); + if (dd.find("blockquote").length) { + dd.addClass("has-note"); + } + }); + + // Default to the first tab, for grouped tabs switch again later + switchToTab(dts.first()); + + dts.first().addClass("first"); + dts.last().addClass("last"); + }); + + // Determine all supergroups, populate 'catalog' and 'supergroupByGroup' accordingly. + $(".supergroup").each(function() { + var supergroup = $(this).attr('name').toLowerCase(); + var groups = $(this).find(".group"); + + catalog[supergroup] = []; + + groups.each(function() { + var group = "group-" + $(this).text().toLowerCase(); + catalog[supergroup].push(group); + supergroupByGroup[group] = supergroup; + }); + + $(this).on("change", function() { + switchToGroup(supergroup, this.value); + }); + }); + + // Switch to the right initial groups + for (var supergroup in catalog) { + var current = queryParamGroup(supergroup) || currentGroups[supergroup] || catalog[supergroup][0]; + + switchToGroup(supergroup, current); + } + + $("dl dt.mdc-tab").click(function(e){ + e.preventDefault(); + var currentDt = $(this);//.parent("dt"); + var currentDl = currentDt.parent("dl"); + + var currentGroup = groupOf(currentDt); + + var supergroup = supergroupByGroup[currentGroup] + if (supergroup) { + switchToGroup(supergroup, currentGroup); + } else { + switchToTab(currentDt); + } + }); + + function queryParamGroup(supergroup) { + var value = new URLSearchParams(window.location.search).get(supergroup) + if (value) { + return "group-" + value.toLowerCase(); + } else { + return ""; + } + } + + function switchToGroup(supergroup, group) { + currentGroups[supergroup] = group; + setCookie(groupCookie, JSON.stringify(currentGroups)); + + // Dropdown switcher: + $("select") + .has("option[value=" + group +"]") + .val(group); + + // Inline snippets: + catalog[supergroup].forEach(peer => { + if (peer === group) { + $("." + group).show(); + } else { + $("." + peer).hide(); + } + }) + + // Tabbed snippets: + $("dl.tabbed").each(function() { + var dl = $(this); + dl.find("dt").each(function() { + var dt = $(this); + if(groupOf(dt) == group) { + switchToTab(dt); + } + }); + }); + + groupChangeListeners.forEach(listener => listener(group, supergroup, catalog)); + } + + function switchToTab(dt) { + // interplay with paradox-material-theme.js adding an activate function to tabs + if (dt[0].activate) dt[0].activate(); + } + + function groupOf(elem) { + const classAttribute = elem.next("dd").find("pre").attr("class"); + if (classAttribute) { + const currentClasses = classAttribute.split(' '); + const regex = new RegExp("^group-.*"); + const matchingClass = currentClasses.find(cc => regex.test(cc)); + if (matchingClass) return matchingClass; + } + + // No class found? Then use the tab title + return "group-" + elem.find('a').text().toLowerCase(); + } +}); diff --git a/theme/src/main/assets/assets/javascripts/paradox-material-theme.js b/theme/src/main/assets/assets/javascripts/paradox-material-theme.js new file mode 100644 index 0000000..550ae3b --- /dev/null +++ b/theme/src/main/assets/assets/javascripts/paradox-material-theme.js @@ -0,0 +1,221 @@ +/*! + Adopted from paradox-material-theme 0.6.0 to simplify switching to a tab. + + Paradox Material Theme + Copyright (c) 2017 Jonas Fonseca + License: MIT +*/ + +function initParadoxMaterialTheme() { + // callout -> ammonition + document.querySelectorAll('.callout').forEach(callout => { + callout.classList.add('admonition') + callout.querySelectorAll('.callout-title').forEach(title => { + title.classList.add('admonition-title') + }) + callout.style.visibility = 'visible'; + }) + + var headers = ['h2', 'h3', 'h4', 'h5', 'h6'] + headers.forEach(headerName => { + document.querySelectorAll(headerName).forEach(header => { + var link = header.querySelector('a') + if (link) { + header.id = link.name + link.name = '' + header.removeChild(link) + link.text = '¶' + link.title = 'Permanent link' + link.className = 'headerlink' + header.appendChild(link) + } + }) + }) + + document.querySelectorAll('nav.md-nav--primary > ul').forEach((root, rootIndex) => { + function createNavToggle(path, active) { + var input = document.createElement('input') + input.classList.add('md-toggle') + input.classList.add('md-nav__toggle') + input.type = 'checkbox' + input.id = path + input.checked = active || false + input.setAttribute('data-md-toggle', path) + return input + } + + function createNavLabel(path, active, contentNode) { + var label = document.createElement('label') + label.classList.add('md-nav__link') + if (active) + label.classList.add('md-nav__link--active') + label.setAttribute('for', path) + if (contentNode) + label.appendChild(contentNode) + return label + } + + function visitListItem(item, path, level) { + item.classList.add('md-nav__item') + + var link = item.querySelector(':scope > a') + if (link) { + link.classList.add('md-nav__link') + link.classList.remove('page') + if (link.classList.contains('active')) { + item.classList.add('md-nav__item--active') + link.classList.add('md-nav__link--active') + } + link.setAttribute('data-md-state', '') + } + + var nestedNav = null + var nestedRoot = item.querySelector(':scope > ul') + if (nestedRoot) { + var active = item.querySelector(':scope a.active') != null + item.classList.add('md-nav__item--nested') + var nestedNav = document.createElement('nav') + nestedNav.classList.add('md-nav') + nestedNav.setAttribute('data-md-component', 'collapsible') + nestedNav.setAttribute('data-md-level', level) + + var input = createNavToggle(path, active) + + var label = createNavLabel(path, false, link) + if (link) + link.classList.remove('md-nav__link') + + var labelInner = document.createElement('label') + labelInner.classList.add('md-nav__title') + labelInner.setAttribute('for', path) + labelInner.textContent = link ? link.textContent : '???' + + nestedNav.appendChild(labelInner) + nestedNav.appendChild(nestedRoot) + item.appendChild(input) + item.appendChild(label) + item.appendChild(nestedNav) + visitList(nestedRoot, path, level + 1) + } + + if (link && link.classList.contains('active')) { + var toc = document.querySelector('nav.md-nav--primary > .md-nav--secondary') + if (toc && toc.children.length > 0) { + var input = createNavToggle('__toc', false) + var labelText = nestedNav ? 'Table of contents' : link ? link.textContent : '???' + var label = createNavLabel('__toc', true, document.createTextNode(labelText)) + + if (nestedNav) { + var node = nestedNav.children[1] + nestedNav.insertBefore(input, node) + nestedNav.insertBefore(label, node) + nestedNav.appendChild(toc) + } else if (link) { + item.insertBefore(input, link) + item.insertBefore(label, link) + item.appendChild(toc) + } + } + } + } + + function visitList(list, path, level) { + list.classList.add('md-nav__list') + list.setAttribute('data-md-scrollfix', '') + list.querySelectorAll('li').forEach((item, itemIndex) => { + visitListItem(item, path + '-' + itemIndex, level) + }) + } + + visitList(root, 'nav-' + rootIndex, 1) + var projectVersion = document.getElementById("project.version") + if (projectVersion) { + root.appendChild(projectVersion) + } + root.parentNode.style.visibility = 'visible' + }) + + document.querySelectorAll('.md-sidebar--secondary .md-nav--secondary > ul').forEach(tocRoot => { + function visitListItem(item) { + item.classList.add('md-nav__item') + item.querySelectorAll(':scope> a').forEach(link => { + link.classList.add('md-nav__link') + link.setAttribute('data-md-state', '') + }) + item.querySelectorAll(':scope > ul').forEach(list => { + visitList(list) + }) + } + + function visitList(list) { + list.classList.add('md-nav__list') + list.querySelectorAll(':scope > li').forEach(item => { + visitListItem(item) + }) + } + + var parent = tocRoot.parentNode + parent.removeChild(tocRoot) + + tocRoot.querySelectorAll(':scope > li > ul').forEach(list => { + parent.append(list) + list.setAttribute('data-md-scrollfix', '') + visitList(list) + }) + + parent.style.visibility = 'visible'; + }) + + document.querySelectorAll('dl').forEach(dl => { + const tabContents = dl.querySelectorAll(':scope > dd > pre') + if (tabContents.length > 0) { + dl.classList.add('mdc-tab-bar') + var first = true + var contentContainer = document.createElement('div') + contentContainer.classList.add('mdc-tab-content-container') + + tabContents.forEach(pre => { + var dd = pre.parentNode + var dt = dd.previousSibling + while (dt.nodeType != dt.ELEMENT_NODE) { + dt = dt.previousSibling + } + + var tabContent = document.createElement('div') + tabContent.classList.add('mdc-tab-content') + contentContainer.appendChild(tabContent) + while (dd.childNodes.length > 0) { + tabContent.appendChild(dd.childNodes[0]); + } + dl.removeChild(dd) + + dt.classList.add('mdc-tab') + if (first) { + dt.classList.add('mdc-tab--active') + tabContent.classList.add('mdc-tab-content--active') + } + first = false + dt.activate = function() { + dl.querySelectorAll(':scope .mdc-tab--active').forEach(active => { + active.classList.remove('mdc-tab--active') + }) + contentContainer.querySelectorAll(':scope .mdc-tab-content--active').forEach(active => { + active.classList.remove('mdc-tab-content--active') + }) + dt.classList.add('mdc-tab--active') + tabContent.classList.add('mdc-tab-content--active') + } + dt.onclick = event => { + dt.activate() + } + }) + + if (dl.nextSibling) + dl.parentNode.insertBefore(contentContainer, dl.nextSibling) + else + dl.parentNode.appendChild(contentContainer) + } + }) +} + +initParadoxMaterialTheme() diff --git a/theme/src/main/assets/page.st b/theme/src/main/assets/page.st new file mode 100644 index 0000000..c319f21 --- /dev/null +++ b/theme/src/main/assets/page.st @@ -0,0 +1,171 @@ +$! + Adopted from paradox-material-theme version 0.6.0 to include more scripts. + + Copyright (c) 2016-2018 Martin Donath + Copyright (c) 2017-2018 Jonas Fonseca + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +!$ + + + + + + + + + + $partials/language()$ + + $ if (page.properties.("description")) $ + + $ elseif (page.properties.("project.description")) $ + + $ endif $ + $ if (page.properties.("material.canonical.url")) $ + + $ endif $ + $ if (page.properties.("author")) $ + + $ elseif (page.properties.("material.author")) $ + + $ endif $ + + $ if (page.properties.("title")) $ + $page.properties.("title")$ + $ elseif (page.title) $ + $page.title$$ if (!page.home.active) $ · $page.home.title$$ endif $ + $ else $ + $page.home.title$ + $ endif $ + + $ if (page.properties.("material.color.primary")) $ + + $ if (page.properties.("material.color.primary.theme")) $ + + $ endif $ + $ endif $ + + + + $ if (page.properties.("material.font.text")) $ + + + $ endif $ + + + + $ if (page.properties.("material.custom.stylesheet")) $ + + $ endif $ + + + + + + $! FIXME: Skip to content (0e1850280a1c25d3bc697419288d97582dacbbc3) !$ + $partials/header()$ +
+ $ if (page.properties.("material.hero")) $ + $partials/hero()$ + $ endif $ +
+
+
+
+
+ $partials/nav()$ +
+
+
+ $ if (page.subheaders) $ +
+
+
+ $partials/toc()$ +
+
+
+ $ endif $ +
+
+
+ $page.content$ +
+ $ if (page.source_url) $ + + $ endif $ + +
+
+
+
+ $partials/footer()$ +
+ + + $! + + The ending "." enables search to work without providing the site URL, since the + theme JavaScript fetches the index by concatenating the base URL to an absolute + path: "/search/search_index.json". + + It originally was "./" but this breaks previewSite because it leads to a double + slash when fetching the index, i.e. ".//search/...". To complete the workaround + all locations in the search index now start with a "/" so the JavaScript can + safely do: `url.base + doc.location`. + + !$ + + + + + + + + $ if (page.properties.("material.custom.javascript")) $ + + $ endif $ + $ if (page.properties.("material.google.analytics")) $ + $partials/integrations/analytics()$ + $ endif $ + +