diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4f2da0e..c665f75 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -2,13 +2,12 @@ name: CD on: push: - branches: - - production - tags-ignore: - - "**" + branches: [production] + tags-ignore: ["**"] jobs: release: + if: ${{ ! startsWith(github.event.head_commit.message, 'chore(release)') }} permissions: contents: write issues: write diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50a158b..31b8786 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,25 @@ -name: "CI" +name: CI + on: push: branches: - - "master" - - "hotfix/**" + - master + - "hotfix/*" paths-ignore: - ".github/**" - "!.github/workflows/ci.yml" - - ".gitignore" + - .gitignore - "docs/**" - - "README.md" - - "LICENSE" + - README.md + - LICENSE pull_request: + paths-ignore: + - ".github/**" + - "!.github/workflows/ci.yml" + - .gitignore + - "docs/**" + - README.md + - LICENSE jobs: build: diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index c9c48c3..58f1a3f 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -1,5 +1,11 @@ name: Lint Commit Messages -on: pull_request + +on: + push: + branches: + - master + - "hotfix/*" + pull_request: jobs: commitlint: diff --git a/.github/workflows/pr-filter.yml b/.github/workflows/pr-filter.yml new file mode 100644 index 0000000..8e9a18b --- /dev/null +++ b/.github/workflows/pr-filter.yml @@ -0,0 +1,25 @@ +name: PR Filter + +on: + pull_request_target: + types: [opened, reopened] + +jobs: + check-template: + if: github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Check PR Content + id: intercept + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('.github/workflows/scripts/pr-filter.js'); + await script({ github, context, core }); diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 99114ea..b0f9713 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,6 +10,7 @@ on: required: true BUILDER: required: true + workflow_dispatch: jobs: launch: diff --git a/.github/workflows/scripts/pr-filter.js b/.github/workflows/scripts/pr-filter.js new file mode 100644 index 0000000..03f50dc --- /dev/null +++ b/.github/workflows/scripts/pr-filter.js @@ -0,0 +1,36 @@ +function hasTypes(markdown) { + return /## Type of change/.test(markdown) && /-\s\[x\]/i.test(markdown); +} + +function hasDescription(markdown) { + return ( + /## Description/.test(markdown) && + !/## Description\s*\n\s*(##|\s*$)/.test(markdown) + ); +} + +module.exports = async ({ github, context, core }) => { + const pr = context.payload.pull_request; + const body = pr.body === null ? '' : pr.body; + const markdown = body.replace(//g, ''); + const action = context.payload.action; + + const isValid = + markdown !== '' && hasTypes(markdown) && hasDescription(markdown); + + if (!isValid) { + await github.rest.pulls.update({ + ...context.repo, + pull_number: pr.number, + state: 'closed' + }); + + await github.rest.issues.createComment({ + ...context.repo, + issue_number: pr.number, + body: `Oops, it seems you've ${action} an invalid pull request. No worries, we'll close it for you.` + }); + + core.setFailed('PR content does not meet template requirements.'); + } +}; diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index bcf425a..4f6e91c 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,7 +9,7 @@ permissions: pull-requests: write env: - STALE_LABEL: stale + STALE_LABEL: inactive EXEMPT_LABELS: "pending,planning,in progress" MESSAGE: > This conversation has been automatically marked as stale because it has not had recent activity. diff --git a/.github/workflows/style-lint.yml b/.github/workflows/style-lint.yml deleted file mode 100644 index f84f3bc..0000000 --- a/.github/workflows/style-lint.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: "Style Lint" - -on: - push: - branches: ["master", "hotfix/**"] - paths: ["_sass/**/*.scss"] - pull_request: - paths: ["_sass/**/*.scss"] - -jobs: - stylelint: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: lts/* - - run: npm i - - run: npm test diff --git a/.gitignore b/.gitignore index 0082d90..7dd7cef 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,5 @@ package-lock.json !.vscode/tasks.json # Misc -_sass/dist +_sass/vendors assets/js/dist diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..b890290 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,31 @@ +{ + "ignoreFiles": ["_sass/vendors/**"], + "extends": "stylelint-config-standard-scss", + "rules": { + "no-descending-specificity": null, + "shorthand-property-no-redundant-values": null, + "at-rule-no-vendor-prefix": null, + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null, + "color-function-notation": "legacy", + "alpha-value-notation": "number", + "selector-not-notation": "simple", + "color-hex-length": "long", + "declaration-block-single-line-max-declarations": 3, + "scss/operator-no-newline-after": null, + "rule-empty-line-before": [ + "always", + { + "ignore": ["after-comment", "first-nested"] + } + ], + "value-keyword-case": [ + "lower", + { + "ignoreProperties": ["/^\\$/"] + } + ], + "media-feature-range-notation": "prefix" + } +} diff --git a/Gemfile b/Gemfile index 66f9337..e541574 100644 --- a/Gemfile +++ b/Gemfile @@ -11,4 +11,4 @@ platforms :mingw, :x64_mingw, :mswin, :jruby do gem "tzinfo-data" end -gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] +gem "wdm", "~> 0.2.0", :platforms => [:mingw, :x64_mingw, :mswin] diff --git a/_data/locales/hu-HU.yml b/_data/locales/hu-HU.yml index 53d88e9..be3a31b 100644 --- a/_data/locales/hu-HU.yml +++ b/_data/locales/hu-HU.yml @@ -14,24 +14,23 @@ tabs: categories: Kategóriák tags: Címkék archives: Archívum - about: Rólam + about: Bemutatkozás # the text displayed in the search bar & search results search: hint: keresés cancel: Mégse - no_results: Oops! Nincs találat a keresésre. + no_results: Hoppá! Nincs találat a keresésre. panel: lastmod: Legutóbb frissítve trending_tags: Népszerű Címkék toc: Tartalom - links: Blog linkek copyright: # Shown at the bottom of the post license: - template: A bejegyzés :LICENSE_NAME licenccel rendelkezik. + template: A bejegyzést a szerző :LICENSE_NAME licenc alatt engedélyezte. name: CC BY 4.0 link: https://creativecommons.org/licenses/by/4.0/ @@ -42,7 +41,7 @@ copyright: Creative Commons Attribution 4.0 International (CC BY 4.0) licenccel rendelkeznek, hacsak másképp nincs jelezve. -meta: Készítve :PLATFORM motorral :THEME témával +meta: Készítve :THEME témával a :PLATFORM platformra. not_found: statement: Sajnáljuk, az URL-t rosszul helyeztük el, vagy valami nem létezőre mutat. @@ -73,7 +72,21 @@ post: title: Link másolása succeed: Link sikeresen másolva! +# Date time format. +# See: , +df: + post: + strftime: "%Y. %B. %e." + dayjs: "YYYY. MMMM D." + archives: + strftime: "%B" + dayjs: "MMM" + # categories page categories: - category_measure: kategória - post_measure: bejegyzés + category_measure: + singular: kategória + plural: kategória + post_measure: + singular: bejegyzés + plural: bejegyzés diff --git a/_data/origin/cors.yml b/_data/origin/cors.yml index afdb3d9..e9bb4eb 100644 --- a/_data/origin/cors.yml +++ b/_data/origin/cors.yml @@ -20,17 +20,17 @@ webfonts: https://fonts.googleapis.com/css2?family=Lato:wght@300;400&family=Sour # Libraries toc: - css: https://cdn.jsdelivr.net/npm/tocbot@4.29.0/dist/tocbot.min.css - js: https://cdn.jsdelivr.net/npm/tocbot@4.29.0/dist/tocbot.min.js + css: https://cdn.jsdelivr.net/npm/tocbot@4.32.2/dist/tocbot.min.css + js: https://cdn.jsdelivr.net/npm/tocbot@4.32.2/dist/tocbot.min.js fontawesome: - css: https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.6.0/css/all.min.css + css: https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.7.1/css/all.min.css search: js: https://cdn.jsdelivr.net/npm/simple-jekyll-search@1.10.0/dest/simple-jekyll-search.min.js mermaid: - js: https://cdn.jsdelivr.net/npm/mermaid@11.0.2/dist/mermaid.min.js + js: https://cdn.jsdelivr.net/npm/mermaid@11.4.0/dist/mermaid.min.js dayjs: js: diff --git a/_includes/analytics/cloudflare.html b/_includes/analytics/cloudflare.html index 1eeb1a9..9faa11e 100644 --- a/_includes/analytics/cloudflare.html +++ b/_includes/analytics/cloudflare.html @@ -4,4 +4,3 @@ src="https://static.cloudflareinsights.com/beacon.min.js" data-cf-beacon='{"token": "{{ site.analytics.cloudflare.id }}"}' > - diff --git a/_includes/analytics/fathom.html b/_includes/analytics/fathom.html index 4b603d3..216bb14 100644 --- a/_includes/analytics/fathom.html +++ b/_includes/analytics/fathom.html @@ -2,6 +2,5 @@ - + defer +> diff --git a/_includes/analytics/google.html b/_includes/analytics/google.html index d0aac65..dfe4828 100644 --- a/_includes/analytics/google.html +++ b/_includes/analytics/google.html @@ -1,7 +1,7 @@ - diff --git a/_includes/comments.html b/_includes/comment.html similarity index 100% rename from _includes/comments.html rename to _includes/comment.html diff --git a/_includes/comments/disqus.html b/_includes/comments/disqus.html index 2b889a4..fd12a3c 100644 --- a/_includes/comments/disqus.html +++ b/_includes/comments/disqus.html @@ -1,38 +1,25 @@ - - -
-

Comments powered by Disqus.

-
- - diff --git a/_includes/comments/giscus.html b/_includes/comments/giscus.html index f9becfe..8058472 100644 --- a/_includes/comments/giscus.html +++ b/_includes/comments/giscus.html @@ -1,21 +1,8 @@ - - - diff --git a/_includes/head.html b/_includes/head.html index af3acdb..011187c 100644 --- a/_includes/head.html +++ b/_includes/head.html @@ -70,7 +70,7 @@ {% unless jekyll.environment == 'production' %} - + {% endunless %} @@ -97,11 +97,32 @@ {% endif %} - + {% unless site.theme_mode %} - {% include mode-toggle.html %} + {% endunless %} + {% include js-selector.html lang=lang %} + + {% if jekyll.environment == 'production' %} + + {% if site.pwa.enabled %} + + {% endif %} + + + {% for analytics in site.analytics %} + {% capture str %}{{ analytics }}{% endcapture %} + {% assign platform = str | split: '{' | first %} + {% if site.analytics[platform].id and site.analytics[platform].id != empty %} + {% include analytics/{{ platform }}.html %} + {% endif %} + {% endfor %} + {% endif %} + {% include metadata-hook.html %} diff --git a/_includes/js-selector.html b/_includes/js-selector.html index 4d77d06..fd4acca 100644 --- a/_includes/js-selector.html +++ b/_includes/js-selector.html @@ -62,12 +62,12 @@ {% capture script %}/assets/js/dist/{{ js }}.min.js{% endcapture %} - + {% if page.math %} - - + + {% endif %} @@ -84,26 +84,3 @@ {% endcase %} {% endif %} {% endif %} - -{% if page.mermaid %} - {% include mermaid.html %} -{% endif %} - -{% if jekyll.environment == 'production' %} - - {% if site.pwa.enabled %} - - {% endif %} - - - {% for analytics in site.analytics %} - {% capture str %}{{ analytics }}{% endcapture %} - {% assign type = str | split: '{' | first %} - {% if site.analytics[type].id and site.analytics[type].id != empty %} - {% include analytics/{{ type }}.html %} - {% endif %} - {% endfor %} -{% endif %} diff --git a/_includes/jsdelivr-combine.html b/_includes/jsdelivr-combine.html index cffa699..0611213 100644 --- a/_includes/jsdelivr-combine.html +++ b/_includes/jsdelivr-combine.html @@ -1,6 +1,6 @@ {% assign urls = include.urls | split: ',' %} -{% assign combined_urls = nil %} +{% assign combined_urls = null %} {% assign domain = 'https://cdn.jsdelivr.net/' %} @@ -15,12 +15,12 @@ {% endif %} {% elsif url contains '//' %} - + {% else %} - + {% endif %} {% endfor %} {% if combined_urls %} - + {% endif %} diff --git a/_includes/mermaid.html b/_includes/mermaid.html deleted file mode 100644 index a3a83ed..0000000 --- a/_includes/mermaid.html +++ /dev/null @@ -1,62 +0,0 @@ - - diff --git a/_includes/mode-toggle.html b/_includes/mode-toggle.html deleted file mode 100644 index 113ec37..0000000 --- a/_includes/mode-toggle.html +++ /dev/null @@ -1,116 +0,0 @@ - - - diff --git a/_includes/search-loader.html b/_includes/search-loader.html index 2582580..7fd065d 100644 --- a/_includes/search-loader.html +++ b/_includes/search-loader.html @@ -19,29 +19,31 @@ {% capture not_found %}

{{ site.data.locales[include.lang].search.no_results }}

{% endcapture %} diff --git a/_includes/sidebar.html b/_includes/sidebar.html index 4f0bb8c..569585f 100644 --- a/_includes/sidebar.html +++ b/_includes/sidebar.html @@ -11,9 +11,7 @@ {%- endif -%} -

- {{ site.title }} -

+ {{ site.title }}

{{ site.tagline }}

diff --git a/_includes/toc-status.html b/_includes/toc-status.html new file mode 100644 index 0000000..4b71cae --- /dev/null +++ b/_includes/toc-status.html @@ -0,0 +1,10 @@ +{% comment %} + Determine TOC state and return it through variable "enable_toc" +{% endcomment %} + +{% assign enable_toc = false %} +{% if site.toc and page.toc %} + {% if page.content contains ' -

{{- site.data.locales[include.lang].panel.toc -}}

+
+
+

{{- site.data.locales[include.lang].panel.toc -}}

{% endif %} diff --git a/_javascript/categories.js b/_javascript/categories.js index 15d8251..ce87d67 100644 --- a/_javascript/categories.js +++ b/_javascript/categories.js @@ -1,5 +1,5 @@ import { basic, initSidebar, initTopbar } from './modules/layouts'; -import { categoryCollapse } from './modules/plugins'; +import { categoryCollapse } from './modules/components'; basic(); initSidebar(); diff --git a/_javascript/home.js b/_javascript/home.js index ef22cb9..7f628a1 100644 --- a/_javascript/home.js +++ b/_javascript/home.js @@ -1,5 +1,5 @@ import { basic, initSidebar, initTopbar } from './modules/layouts'; -import { initLocaleDatetime, loadImg } from './modules/plugins'; +import { initLocaleDatetime, loadImg } from './modules/components'; loadImg(); initLocaleDatetime(); diff --git a/_javascript/misc.js b/_javascript/misc.js index 52b4043..37130da 100644 --- a/_javascript/misc.js +++ b/_javascript/misc.js @@ -1,5 +1,5 @@ import { basic, initSidebar, initTopbar } from './modules/layouts'; -import { initLocaleDatetime } from './modules/plugins'; +import { initLocaleDatetime } from './modules/components'; initSidebar(); initTopbar(); diff --git a/_javascript/modules/plugins.js b/_javascript/modules/components.js similarity index 52% rename from _javascript/modules/plugins.js rename to _javascript/modules/components.js index fb892e2..95791a6 100644 --- a/_javascript/modules/plugins.js +++ b/_javascript/modules/components.js @@ -3,4 +3,8 @@ export { initClipboard } from './components/clipboard'; export { loadImg } from './components/img-loading'; export { imgPopup } from './components/img-popup'; export { initLocaleDatetime } from './components/locale-datetime'; -export { toc } from './components/toc'; +export { initToc } from './components/toc'; +export { loadMermaid } from './components/mermaid'; +export { modeWatcher } from './components/mode-toggle'; +export { back2top } from './components/back-to-top'; +export { loadTooptip } from './components/tooltip-loader'; diff --git a/_javascript/modules/components/img-popup.js b/_javascript/modules/components/img-popup.js index ac12043..420a226 100644 --- a/_javascript/modules/components/img-popup.js +++ b/_javascript/modules/components/img-popup.js @@ -4,7 +4,6 @@ * Dependencies: https://github.com/biati-digital/glightbox */ -const html = document.documentElement; const lightImages = '.popup:not(.dark)'; const darkImages = '.popup:not(.light)'; let selector = lightImages; @@ -33,26 +32,17 @@ export function imgPopup() { document.querySelector('.popup.dark') === null ); - if ( - (html.hasAttribute('data-mode') && - html.getAttribute('data-mode') === 'dark') || - (!html.hasAttribute('data-mode') && - window.matchMedia('(prefers-color-scheme: dark)').matches) - ) { + if (Theme.visualState === Theme.DARK) { selector = darkImages; } let current = GLightbox({ selector: `${selector}` }); - if (hasDualImages && document.getElementById('mode-toggle')) { + if (hasDualImages && Theme.switchable) { let reverse = null; window.addEventListener('message', (event) => { - if ( - event.source === window && - event.data && - event.data.direction === ModeToggle.ID - ) { + if (event.source === window && event.data && event.data.id === Theme.ID) { updateImages(current, reverse); } }); diff --git a/_javascript/modules/components/mermaid.js b/_javascript/modules/components/mermaid.js new file mode 100644 index 0000000..2b4759f --- /dev/null +++ b/_javascript/modules/components/mermaid.js @@ -0,0 +1,60 @@ +/** + * Mermaid-js loader + */ + +const MERMAID = 'mermaid'; +const themeMapper = Theme.getThemeMapper('default', 'dark'); + +function refreshTheme(event) { + if (event.source === window && event.data && event.data.id === Theme.ID) { + // Re-render the SVG › + const mermaidList = document.getElementsByClassName(MERMAID); + + [...mermaidList].forEach((elem) => { + const svgCode = elem.previousSibling.children.item(0).innerHTML; + elem.textContent = svgCode; + elem.removeAttribute('data-processed'); + }); + + const newTheme = themeMapper[Theme.visualState]; + + mermaid.initialize({ theme: newTheme }); + mermaid.init(null, `.${MERMAID}`); + } +} + +function setNode(elem) { + const svgCode = elem.textContent; + const backup = elem.parentElement; + backup.classList.add('d-none'); + // Create mermaid node + const mermaid = document.createElement('pre'); + mermaid.classList.add(MERMAID); + const text = document.createTextNode(svgCode); + mermaid.appendChild(text); + backup.after(mermaid); +} + +export function loadMermaid() { + if ( + typeof mermaid === 'undefined' || + typeof mermaid.initialize !== 'function' + ) { + return; + } + + const initTheme = themeMapper[Theme.visualState]; + + let mermaidConf = { + theme: initTheme + }; + + const basicList = document.getElementsByClassName('language-mermaid'); + [...basicList].forEach(setNode); + + mermaid.initialize(mermaidConf); + + if (Theme.switchable) { + window.addEventListener('message', refreshTheme); + } +} diff --git a/_javascript/modules/components/mode-toggle.js b/_javascript/modules/components/mode-toggle.js new file mode 100644 index 0000000..455ff0a --- /dev/null +++ b/_javascript/modules/components/mode-toggle.js @@ -0,0 +1,15 @@ +/** + * Add listener for theme mode toggle + */ + +const $toggle = document.getElementById('mode-toggle'); + +export function modeWatcher() { + if (!$toggle) { + return; + } + + $toggle.addEventListener('click', () => { + Theme.flip(); + }); +} diff --git a/_javascript/modules/components/mode-watcher.js b/_javascript/modules/components/mode-watcher.js deleted file mode 100644 index 9eecd09..0000000 --- a/_javascript/modules/components/mode-watcher.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Add listener for theme mode toggle - */ -const toggle = document.getElementById('mode-toggle'); - -export function modeWatcher() { - if (!toggle) { - return; - } - - toggle.addEventListener('click', () => { - modeToggle.flipMode(); - }); -} diff --git a/_javascript/modules/components/sidebar.js b/_javascript/modules/components/sidebar.js deleted file mode 100644 index 6b562d8..0000000 --- a/_javascript/modules/components/sidebar.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Expand or close the sidebar in mobile screens. - */ - -const ATTR_DISPLAY = 'sidebar-display'; - -class SidebarUtil { - static isExpanded = false; - - static toggle() { - if (SidebarUtil.isExpanded === false) { - document.body.setAttribute(ATTR_DISPLAY, ''); - } else { - document.body.removeAttribute(ATTR_DISPLAY); - } - - SidebarUtil.isExpanded = !SidebarUtil.isExpanded; - } -} - -export function sidebarExpand() { - document - .getElementById('sidebar-trigger') - .addEventListener('click', SidebarUtil.toggle); - - document.getElementById('mask').addEventListener('click', SidebarUtil.toggle); -} diff --git a/_javascript/modules/components/toc.js b/_javascript/modules/components/toc.js index 56ce26f..e9086ee 100644 --- a/_javascript/modules/components/toc.js +++ b/_javascript/modules/components/toc.js @@ -1,15 +1,33 @@ -export function toc() { - if (document.querySelector('main h2, main h3')) { - // see: https://github.com/tscanlin/tocbot#usage - tocbot.init({ - tocSelector: '#toc', - contentSelector: '.content', - ignoreSelector: '[data-toc-skip]', - headingSelector: 'h2, h3, h4', - orderedList: false, - scrollSmooth: false - }); +import { TocMobile as mobile } from './toc/toc-mobile'; +import { TocDesktop as desktop } from './toc/toc-desktop'; - document.getElementById('toc-wrapper').classList.remove('d-none'); +const desktopMode = matchMedia('(min-width: 1200px)'); + +function refresh(e) { + if (e.matches) { + if (mobile.popupOpened) { + mobile.hidePopup(); + } + + desktop.refresh(); + } else { + mobile.refresh(); } } + +function init() { + if (document.querySelector('main>article[data-toc="true"]') === null) { + return; + } + + // Avoid create multiple instances of Tocbot. Ref: + if (desktopMode.matches) { + desktop.init(); + } else { + mobile.init(); + } + + desktopMode.onchange = refresh; +} + +export { init as initToc }; diff --git a/_javascript/modules/components/toc/toc-desktop.js b/_javascript/modules/components/toc/toc-desktop.js new file mode 100644 index 0000000..5021a72 --- /dev/null +++ b/_javascript/modules/components/toc/toc-desktop.js @@ -0,0 +1,22 @@ +export class TocDesktop { + /* Tocbot options Ref: https://github.com/tscanlin/tocbot#usage */ + static options = { + tocSelector: '#toc', + contentSelector: '.content', + ignoreSelector: '[data-toc-skip]', + headingSelector: 'h2, h3, h4', + orderedList: false, + scrollSmooth: false, + headingsOffset: 16 * 2 // 2rem + }; + + static refresh() { + tocbot.refresh(this.options); + } + + static init() { + if (document.getElementById('toc-wrapper')) { + tocbot.init(this.options); + } + } +} diff --git a/_javascript/modules/components/toc/toc-mobile.js b/_javascript/modules/components/toc/toc-mobile.js new file mode 100644 index 0000000..20e24a7 --- /dev/null +++ b/_javascript/modules/components/toc/toc-mobile.js @@ -0,0 +1,125 @@ +/** + * TOC button, topbar and popup for mobile devices + */ + +const $tocBar = document.getElementById('toc-bar'); +const $soloTrigger = document.getElementById('toc-solo-trigger'); +const $triggers = document.getElementsByClassName('toc-trigger'); +const $popup = document.getElementById('toc-popup'); +const $btnClose = document.getElementById('toc-popup-close'); + +const SCROLL_LOCK = 'overflow-hidden'; +const CLOSING = 'closing'; + +export class TocMobile { + static #invisible = true; + static #barHeight = 16 * 3; // 3rem + + static options = { + tocSelector: '#toc-popup-content', + contentSelector: '.content', + ignoreSelector: '[data-toc-skip]', + headingSelector: 'h2, h3, h4', + orderedList: false, + scrollSmooth: false, + collapseDepth: 4, + headingsOffset: this.#barHeight + }; + + static initBar() { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + $tocBar.classList.toggle('invisible', entry.isIntersecting); + }); + }, + { rootMargin: `-${this.#barHeight}px 0px 0px 0px` } + ); + + observer.observe($soloTrigger); + this.#invisible = false; + } + + static listenAnchors() { + const $anchors = document.getElementsByClassName('toc-link'); + [...$anchors].forEach((anchor) => { + anchor.onclick = () => this.hidePopup(); + }); + } + + static refresh() { + if (this.#invisible) { + this.initComponents(); + } + tocbot.refresh(this.options); + this.listenAnchors(); + } + + static get popupOpened() { + return $popup.open; + } + + static showPopup() { + this.lockScroll(true); + $popup.showModal(); + const activeItem = $popup.querySelector('li.is-active-li'); + activeItem.scrollIntoView({ block: 'center' }); + } + + static hidePopup() { + $popup.toggleAttribute(CLOSING); + + $popup.addEventListener( + 'animationend', + () => { + $popup.toggleAttribute(CLOSING); + $popup.close(); + }, + { once: true } + ); + + this.lockScroll(false); + } + + static lockScroll(enable) { + document.documentElement.classList.toggle(SCROLL_LOCK, enable); + document.body.classList.toggle(SCROLL_LOCK, enable); + } + + static clickBackdrop(event) { + if ($popup.hasAttribute(CLOSING)) { + return; + } + + const rect = event.target.getBoundingClientRect(); + if ( + event.clientX < rect.left || + event.clientX > rect.right || + event.clientY < rect.top || + event.clientY > rect.bottom + ) { + this.hidePopup(); + } + } + + static initComponents() { + this.initBar(); + + [...$triggers].forEach((trigger) => { + trigger.onclick = () => this.showPopup(); + }); + + $popup.onclick = (e) => this.clickBackdrop(e); + $btnClose.onclick = () => this.hidePopup(); + $popup.oncancel = (e) => { + e.preventDefault(); + this.hidePopup(); + }; + } + + static init() { + tocbot.init(this.options); + this.listenAnchors(); + this.initComponents(); + } +} diff --git a/_javascript/modules/layouts/basic.js b/_javascript/modules/layouts/basic.js index fb36a8b..b8eddf6 100644 --- a/_javascript/modules/layouts/basic.js +++ b/_javascript/modules/layouts/basic.js @@ -1,7 +1,7 @@ -import { back2top } from '../components/back-to-top'; -import { loadTooptip } from '../components/tooltip-loader'; +import { back2top, loadTooptip, modeWatcher } from '../components'; export function basic() { + modeWatcher(); back2top(); loadTooptip(); } diff --git a/_javascript/modules/layouts/sidebar.js b/_javascript/modules/layouts/sidebar.js index 8795693..bbf5e7d 100644 --- a/_javascript/modules/layouts/sidebar.js +++ b/_javascript/modules/layouts/sidebar.js @@ -1,7 +1,19 @@ -import { modeWatcher } from '../components/mode-watcher'; -import { sidebarExpand } from '../components/sidebar'; +const ATTR_DISPLAY = 'sidebar-display'; +const $sidebar = document.getElementById('sidebar'); +const $trigger = document.getElementById('sidebar-trigger'); +const $mask = document.getElementById('mask'); + +class SidebarUtil { + static #isExpanded = false; + + static toggle() { + this.#isExpanded = !this.#isExpanded; + document.body.toggleAttribute(ATTR_DISPLAY, this.#isExpanded); + $sidebar.classList.toggle('z-2', this.#isExpanded); + $mask.classList.toggle('d-none', !this.#isExpanded); + } +} export function initSidebar() { - modeWatcher(); - sidebarExpand(); + $trigger.onclick = $mask.onclick = () => SidebarUtil.toggle(); } diff --git a/_javascript/modules/theme.js b/_javascript/modules/theme.js new file mode 100644 index 0000000..f9ebf20 --- /dev/null +++ b/_javascript/modules/theme.js @@ -0,0 +1,135 @@ +/** + * Theme management class + * + * To reduce flickering during page load, this script should be loaded synchronously. + */ +class Theme { + static #modeKey = 'mode'; + static #modeAttr = 'data-mode'; + static #darkMedia = window.matchMedia('(prefers-color-scheme: dark)'); + static switchable = !document.documentElement.hasAttribute(this.#modeAttr); + + static get DARK() { + return 'dark'; + } + + static get LIGHT() { + return 'light'; + } + + /** + * @returns {string} Theme mode identifier + */ + static get ID() { + return 'theme-mode'; + } + + /** + * Gets the current visual state of the theme. + * + * @returns {string} The current visual state, either the mode if it exists, + * or the system dark mode state ('dark' or 'light'). + */ + static get visualState() { + if (this.#hasMode) { + return this.#mode; + } else { + return this.#sysDark ? this.DARK : this.LIGHT; + } + } + + static get #mode() { + return sessionStorage.getItem(this.#modeKey); + } + + static get #isDarkMode() { + return this.#mode === this.DARK; + } + + static get #hasMode() { + return this.#mode !== null; + } + + static get #sysDark() { + return this.#darkMedia.matches; + } + + /** + * Maps theme modes to provided values + * @param {string} light Value for light mode + * @param {string} dark Value for dark mode + * @returns {Object} Mapped values + */ + static getThemeMapper(light, dark) { + return { + [this.LIGHT]: light, + [this.DARK]: dark + }; + } + + /** + * Initializes the theme based on system preferences or stored mode + */ + static init() { + if (!this.switchable) { + return; + } + + this.#darkMedia.addEventListener('change', () => { + const lastMode = this.#mode; + this.#clearMode(); + + if (lastMode !== this.visualState) { + this.#notify(); + } + }); + + if (!this.#hasMode) { + return; + } + + if (this.#isDarkMode) { + this.#setDark(); + } else { + this.#setLight(); + } + } + + /** + * Flips the current theme mode + */ + static flip() { + if (this.#hasMode) { + this.#clearMode(); + } else { + this.#sysDark ? this.#setLight() : this.#setDark(); + } + this.#notify(); + } + + static #setDark() { + document.documentElement.setAttribute(this.#modeAttr, this.DARK); + sessionStorage.setItem(this.#modeKey, this.DARK); + } + + static #setLight() { + document.documentElement.setAttribute(this.#modeAttr, this.LIGHT); + sessionStorage.setItem(this.#modeKey, this.LIGHT); + } + + static #clearMode() { + document.documentElement.removeAttribute(this.#modeAttr); + sessionStorage.removeItem(this.#modeKey); + } + + /** + * Notifies other plugins that the theme mode has changed + */ + static #notify() { + window.postMessage({ id: this.ID }, '*'); + } +} + +Theme.init(); + +export default Theme; diff --git a/_javascript/page.js b/_javascript/page.js index 76e8ce9..4b03b79 100644 --- a/_javascript/page.js +++ b/_javascript/page.js @@ -1,9 +1,15 @@ import { basic, initSidebar, initTopbar } from './modules/layouts'; -import { loadImg, imgPopup, initClipboard } from './modules/plugins'; +import { + loadImg, + imgPopup, + initClipboard, + loadMermaid +} from './modules/components'; loadImg(); imgPopup(); initSidebar(); initTopbar(); initClipboard(); +loadMermaid(); basic(); diff --git a/_javascript/post.js b/_javascript/post.js index 9340f05..dc472b4 100644 --- a/_javascript/post.js +++ b/_javascript/post.js @@ -1,17 +1,20 @@ -import { basic, initSidebar, initTopbar } from './modules/layouts'; +import { basic, initTopbar, initSidebar } from './modules/layouts'; + import { loadImg, imgPopup, initLocaleDatetime, initClipboard, - toc -} from './modules/plugins'; + initToc, + loadMermaid +} from './modules/components'; loadImg(); -toc(); +initToc(); imgPopup(); initSidebar(); initLocaleDatetime(); initClipboard(); initTopbar(); +loadMermaid(); basic(); diff --git a/_layouts/default.html b/_layouts/default.html index ea438fe..a55bfef 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -33,7 +33,7 @@ layout: compress -