From 566a7a9c9868edb617b2f071367b4f0c5dccdbfc Mon Sep 17 00:00:00 2001 From: Gabriel Arazas Date: Thu, 23 Mar 2023 14:28:41 +0800 Subject: [PATCH] Add asciidoctor-tabs extension --- Gemfile | 1 + Gemfile.lock | 8 ++ assets/css/asciidoctor-tabs.css | 103 ++++++++++++++++++++++++ assets/js/asciidoctor-tabs.js | 118 ++++++++++++++++++++++++++++ config.toml | 3 +- gemset.nix | 15 ++++ layouts/partials/head_extended.html | 6 ++ 7 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 assets/css/asciidoctor-tabs.css create mode 100644 assets/js/asciidoctor-tabs.js diff --git a/Gemfile b/Gemfile index 3c8733f..f7ee8f7 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,7 @@ source 'https://rubygems.org' gem 'asciidoctor' gem 'asciidoctor-html5s' gem 'asciidoctor-rouge' +gem 'asciidoctor-tabs', github: 'asciidoctor/asciidoctor-tabs' gem 'rouge' gem 'rubocop' gem 'asciidoctor-foodogsquared-extensions', :path => './gems' diff --git a/Gemfile.lock b/Gemfile.lock index 90d74ad..9600fc4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +GIT + remote: https://github.com/asciidoctor/asciidoctor-tabs.git + revision: d5ee94b5253f4db75e6646f46511676af0be2cc3 + specs: + asciidoctor-tabs (1.0.0.beta.3) + asciidoctor (>= 2.0.0, < 3.0.0) + PATH remote: gems specs: @@ -56,6 +63,7 @@ DEPENDENCIES asciidoctor-foodogsquared-extensions! asciidoctor-html5s asciidoctor-rouge + asciidoctor-tabs! rouge rubocop ruby-lsp diff --git a/assets/css/asciidoctor-tabs.css b/assets/css/asciidoctor-tabs.css new file mode 100644 index 0000000..2f804f8 --- /dev/null +++ b/assets/css/asciidoctor-tabs.css @@ -0,0 +1,103 @@ +/*! Asciidoctor Tabs | Copyright (c) 2018-present Dan Allen | MIT License */ +.tabs { + margin-bottom: 1.25em; +} + +.tablist > ul { + display: flex; + flex-wrap: wrap; + list-style: none; + margin: 0; + padding: 0; +} + +.tablist > ul li { + align-items: center; + background-color: var(--base02); + cursor: pointer; + font-size: 0.8em; + display: flex; + padding: 0.5em; + position: relative; + + &:hover { + background: var(--base01); + } +} + +.tablist > ul li:focus-visible { + outline: none; +} + +.tablist.ulist, +.tablist.ulist > ul li { + margin: 0; +} + +.tablist.ulist > ul li + li { + margin-left: 0.25em; +} + +.tabs.is-loading .tablist li:not(:first-child), +.tabs:not(.is-loading) .tablist li:not(.is-selected) { + background-color: var(--base00); +} + +.tabs.is-loading .tablist li:first-child::after, +.tabs:not(.is-loading) .tablist li.is-selected::after { + background-color: inherit; + content: ""; + display: block; + height: 3px; /* Chrome doesn't always paint the line accurately, so add a little extra */ + position: absolute; + bottom: -1.5px; + left: 0; + right: 0; +} + +.tablist > ul p { + line-height: inherit; + margin: 0; +} + +.tabpanel { + background-color: var(--base00); + padding: 1.25em; +} + +.tablist > ul li, +.tabpanel { + border: var(--border-style); +} + +.tablist > ul li { + border-bottom: 0; +} + +.tabs.is-loading .tabpanel + .tabpanel, +.tabs:not(.is-loading) .tabpanel.is-hidden { + display: none; +} + +.tabpanel > :first-child { + margin-top: 0; +} + +/* #content is a signature of the Asciidoctor standalone HTML output */ +#content .tabpanel > :last-child, +#content .tabpanel > :last-child > :last-child, +#content .tabpanel > :last-child > :last-child > li:last-child > :last-child { + margin-bottom: 0; +} + +.tablecontainer { + overflow-x: auto; +} + +#content .tablecontainer { + margin-bottom: 1.25em; +} + +#content .tablecontainer > table.tableblock { + margin-bottom: 0; +} diff --git a/assets/js/asciidoctor-tabs.js b/assets/js/asciidoctor-tabs.js new file mode 100644 index 0000000..483d6bb --- /dev/null +++ b/assets/js/asciidoctor-tabs.js @@ -0,0 +1,118 @@ +;(function () { /*! Asciidoctor Tabs | Copyright (c) 2018-present Dan Allen | MIT License */ + 'use strict' + + var config = (document.currentScript || {}).dataset || {} + var forEach = Array.prototype.forEach + + init(document.querySelectorAll('.tabs')) + + function init (tabsBlocks) { + if (!tabsBlocks.length) return + forEach.call(tabsBlocks, function (tabs) { + var syncIds = tabs.classList.contains('is-sync') ? {} : undefined + var tablist = tabs.querySelector('.tablist ul') + tablist.setAttribute('role', 'tablist') + var initial + forEach.call(tablist.querySelectorAll('li'), function (tab, idx) { + tab.setAttribute('role', (tab.className = 'tab')) // NOTE converter may not have set class on li + var id, anchor, syncId + if (!(id = tab.id)) { + if (!(anchor = tab.querySelector('a[id]'))) return // invalid state + tab.id = id = anchor.parentNode.removeChild(anchor).id + } + var panel = tabs.querySelector('.tabpanel[aria-labelledby~="' + id + '"]') + if (!panel) return // invalid state + tab.tabIndex = -1 + syncIds && (((syncId = tab.textContent.trim()) in syncIds) ? (syncId = undefined) : true) && + (syncIds[(tab.dataset.syncId = syncId)] = tab) + idx || (initial = { tab: tab, panel: panel }) && syncIds ? toggleHidden(panel, true) : toggleSelected(tab, true) + tab.setAttribute('aria-controls', panel.id) + panel.setAttribute('role', 'tabpanel') + forEach.call(panel.querySelectorAll('table.tableblock'), function (table) { + var container = Object.assign(document.createElement('div'), { className: 'tablecontainer' }) + table.parentNode.insertBefore(container, table).appendChild(table) + }) + var onClick = syncId === undefined ? activateTab : activateTabSync + tab.addEventListener('click', onClick.bind({ tabs: tabs, tab: tab, panel: panel })) + }) + if (syncIds && initial) { + var syncGroupId + for (var i = 0, lst = tabs.classList, len = lst.length, className; i !== len; i++) { + if (!(className = lst.item(i)).startsWith('data-sync-group-id=')) continue + tabs.dataset.syncGroupId = syncGroupId = lst.remove(className) || className.slice(19).replace(/\u00a0/g, ' ') + break + } + if (syncGroupId === undefined) tabs.dataset.syncGroupId = syncGroupId = Object.keys(syncIds).sort().join('|') + var preferredSyncId = 'syncStorageKey' in config && + window[(config.syncStorageScope || 'local') + 'Storage'].getItem(config.syncStorageKey + '-' + syncGroupId) + var tab = preferredSyncId && syncIds[preferredSyncId] + tab && Object.assign(initial, { tab: tab, panel: document.getElementById(tab.getAttribute('aria-controls')) }) + toggleSelected(initial.tab, true) || toggleHidden(initial.panel, false) + } + }) + onHashChange() + toggleClassOnEach(tabsBlocks, 'is-loading', 'remove') + window.setTimeout(toggleClassOnEach.bind(null, tabsBlocks, 'is-loaded', 'add'), 0) + window.addEventListener('hashchange', onHashChange) + } + + function activateTab (e) { + var tab = this.tab + var tabs = this.tabs || (this.tabs = tab.closest('.tabs')) + var panel = this.panel || (this.panel = document.getElementById(tab.getAttribute('aria-controls'))) + forEach.call(tabs.querySelectorAll('.tablist .tab'), function (el) { + toggleSelected(el, el === tab) + }) + forEach.call(tabs.querySelectorAll('.tabpanel'), function (el) { + toggleHidden(el, el !== panel) + }) + if (!this.isSync && 'syncStorageKey' in config && 'syncGroupId' in tabs.dataset) { + var storageKey = config.syncStorageKey + '-' + tabs.dataset.syncGroupId + window[(config.syncStorageScope || 'local') + 'Storage'].setItem(storageKey, tab.dataset.syncId) + } + if (!e) return + var loc = window.location + var hashIdx = loc.hash ? loc.href.indexOf('#') : -1 + if (~hashIdx) window.history.replaceState(null, '', loc.href.slice(0, hashIdx)) + e.preventDefault() + } + + function activateTabSync (e) { + activateTab.call(this, e) + var thisTabs = this.tabs + var thisTab = this.tab + var initialY = thisTabs.getBoundingClientRect().y + forEach.call(document.querySelectorAll('.tabs'), function (tabs) { + if (tabs === thisTabs || tabs.dataset.syncGroupId !== thisTabs.dataset.syncGroupId) return + forEach.call(tabs.querySelectorAll('.tablist .tab'), function (tab) { + if (tab.dataset.syncId === thisTab.dataset.syncId) activateTab.call({ tabs: tabs, tab: tab, isSync: true }) + }) + }) + var shiftedBy = thisTabs.getBoundingClientRect().y - initialY + if (shiftedBy && (shiftedBy = Math.round(shiftedBy))) window.scrollBy({ top: shiftedBy, behavior: 'instant' }) + } + + function toggleClassOnEach (elements, className, method) { + forEach.call(elements, function (el) { + el.classList[method](className) + }) + } + + function toggleHidden (el, state) { + el.classList[(el.hidden = state) ? 'add' : 'remove']('is-hidden') + } + + function toggleSelected (el, state) { + el.setAttribute('aria-selected', '' + state) + el.classList[state ? 'add' : 'remove']('is-selected') + el.tabIndex = state ? 0 : -1 + } + + function onHashChange () { + var id = window.location.hash.slice(1) + if (!id) return + var tab = document.getElementById(~id.indexOf('%') ? decodeURIComponent(id) : id) + if (!(tab && tab.classList.contains('tab'))) return + 'syncId' in tab.dataset ? activateTabSync.call({ tab: tab }) : activateTab.call({ tab: tab }) + } +})() diff --git a/config.toml b/config.toml index 06fc6e4..1e358f3 100644 --- a/config.toml +++ b/config.toml @@ -15,7 +15,8 @@ path = "github.com/foo-dogsquared/hugo-mod-web-feeds" [markup.asciidocExt] extensions = [ - "asciidoctor-foodogsquared-extensions" + "asciidoctor-foodogsquared-extensions", + "asciidoctor-tabs", ] workingFolderCurrent = true diff --git a/gemset.nix b/gemset.nix index e1f7c0d..016c380 100644 --- a/gemset.nix +++ b/gemset.nix @@ -49,6 +49,21 @@ targets = [ ]; version = "0.4.0"; }; + asciidoctor-tabs = { + dependencies = ["asciidoctor"]; + groups = ["default"]; + platforms = []; + source = { + fetchSubmodules = false; + rev = "d5ee94b5253f4db75e6646f46511676af0be2cc3"; + sha256 = "1s2ds0f3v308vw4ic5dm6zh8d7dbi6lxxw6y7ajb8hmm27d0nksi"; + target = "ruby"; + type = "git"; + url = "https://github.com/asciidoctor/asciidoctor-tabs.git"; + }; + targets = []; + version = "1.0.0.beta.3"; + }; ast = { groups = [ "default" ]; platforms = [ ]; diff --git a/layouts/partials/head_extended.html b/layouts/partials/head_extended.html index a27ec72..f6e09e2 100644 --- a/layouts/partials/head_extended.html +++ b/layouts/partials/head_extended.html @@ -5,6 +5,12 @@ {{ end }} +{{ $asciidoctorTabs := resources.Get "css/asciidoctor-tabs.css" | resources.ToCSS }} + + +{{ $asciidoctorTabsJS := resources.Get "js/asciidoctor-tabs.js" }} + + {{- /* medium-zoom */ -}}