mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-13 20:24:00 +02:00
Merge commit 'a024bc7d76fcc5e49e8210f9b0896db9ef21861a'
This commit is contained in:
123
docs/assets/js/alpinejs/data/explorer.js
Normal file
123
docs/assets/js/alpinejs/data/explorer.js
Normal file
@@ -0,0 +1,123 @@
|
||||
var debug = 0 ? console.log.bind(console, '[explorer]') : function () {};
|
||||
|
||||
// This is cureently not used, but kept in case I change my mind.
|
||||
export const explorer = (Alpine) => ({
|
||||
uiState: {
|
||||
containerScrollTop: -1,
|
||||
lastActiveRef: '',
|
||||
},
|
||||
treeState: {
|
||||
// The href of the current page.
|
||||
currentNode: '',
|
||||
// The state of each node in the tree.
|
||||
nodes: {},
|
||||
|
||||
// We currently only list the sections, not regular pages, in the side bar.
|
||||
// This strikes me as the right balance. The pages gets listed on the section pages.
|
||||
// This array is sorted by length, so we can find the longest prefix of the current page
|
||||
// without having to iterate over all the keys.
|
||||
nodeRefsByLength: [],
|
||||
},
|
||||
async init() {
|
||||
let keys = Reflect.ownKeys(this.$refs);
|
||||
for (let key of keys) {
|
||||
let n = {
|
||||
open: false,
|
||||
active: false,
|
||||
};
|
||||
this.treeState.nodes[key] = n;
|
||||
this.treeState.nodeRefsByLength.push(key);
|
||||
}
|
||||
|
||||
this.treeState.nodeRefsByLength.sort((a, b) => b.length - a.length);
|
||||
|
||||
this.setCurrentActive();
|
||||
},
|
||||
|
||||
longestPrefix(ref) {
|
||||
let longestPrefix = '';
|
||||
for (let key of this.treeState.nodeRefsByLength) {
|
||||
if (ref.startsWith(key)) {
|
||||
longestPrefix = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return longestPrefix;
|
||||
},
|
||||
|
||||
setCurrentActive() {
|
||||
let ref = this.longestPrefix(window.location.pathname);
|
||||
let activeChanged = this.uiState.lastActiveRef !== ref;
|
||||
debug('setCurrentActive', this.uiState.lastActiveRef, window.location.pathname, '=>', ref, activeChanged);
|
||||
this.uiState.lastActiveRef = ref;
|
||||
if (this.uiState.containerScrollTop === -1 && activeChanged) {
|
||||
// Navigation outside of the explorer menu.
|
||||
let el = document.querySelector(`[x-ref="${ref}"]`);
|
||||
if (el) {
|
||||
this.$nextTick(() => {
|
||||
debug('scrolling to', ref);
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
}
|
||||
}
|
||||
this.treeState.currentNode = ref;
|
||||
for (let key in this.treeState.nodes) {
|
||||
let n = this.treeState.nodes[key];
|
||||
n.active = false;
|
||||
n.open = ref == key || ref.startsWith(key);
|
||||
if (n.open) {
|
||||
debug('open', key);
|
||||
}
|
||||
}
|
||||
|
||||
let n = this.treeState.nodes[this.longestPrefix(ref)];
|
||||
if (n) {
|
||||
n.active = true;
|
||||
}
|
||||
},
|
||||
|
||||
getScrollingContainer() {
|
||||
return document.getElementById('leftsidebar');
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
debug('onLoad', this.uiState.containerScrollTop);
|
||||
if (this.uiState.containerScrollTop >= 0) {
|
||||
debug('onLoad: scrolling to', this.uiState.containerScrollTop);
|
||||
this.getScrollingContainer().scrollTo(0, this.uiState.containerScrollTop);
|
||||
}
|
||||
this.uiState.containerScrollTop = -1;
|
||||
},
|
||||
|
||||
onBeforeRender() {
|
||||
debug('onBeforeRender', this.uiState.containerScrollTop);
|
||||
this.setCurrentActive();
|
||||
},
|
||||
|
||||
toggleNode(ref) {
|
||||
this.uiState.containerScrollTop = this.getScrollingContainer().scrollTop;
|
||||
this.uiState.lastActiveRef = '';
|
||||
debug('toggleNode', ref, this.uiState.containerScrollTop);
|
||||
|
||||
let node = this.treeState.nodes[ref];
|
||||
if (!node) {
|
||||
debug('node not found', ref);
|
||||
return;
|
||||
}
|
||||
let wasOpen = node.open;
|
||||
},
|
||||
|
||||
isCurrent(ref) {
|
||||
let n = this.treeState.nodes[ref];
|
||||
return n && n.active;
|
||||
},
|
||||
|
||||
isOpen(ref) {
|
||||
let node = this.treeState.nodes[ref];
|
||||
if (!node) return false;
|
||||
if (node.open) {
|
||||
debug('isOpen', ref);
|
||||
}
|
||||
return node.open;
|
||||
},
|
||||
});
|
3
docs/assets/js/alpinejs/data/index.js
Normal file
3
docs/assets/js/alpinejs/data/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './navbar';
|
||||
export * from './search';
|
||||
export * from './toc';
|
10
docs/assets/js/alpinejs/data/navbar.js
Normal file
10
docs/assets/js/alpinejs/data/navbar.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const navbar = (Alpine) => ({
|
||||
init: function () {
|
||||
Alpine.bind(this.$root, this.root);
|
||||
},
|
||||
root: {
|
||||
['@scroll.window.debounce.10ms'](event) {
|
||||
this.$store.nav.scroll.atTop = window.scrollY < 40 ? true : false;
|
||||
},
|
||||
},
|
||||
});
|
109
docs/assets/js/alpinejs/data/search.js
Normal file
109
docs/assets/js/alpinejs/data/search.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const designMode = false;
|
||||
|
||||
const groupByLvl0 = (array) => {
|
||||
if (!array) return [];
|
||||
return array.reduce((result, currentValue) => {
|
||||
(result[currentValue.hierarchy.lvl0] = result[currentValue.hierarchy.lvl0] || []).push(currentValue);
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const applyHelperFuncs = (array) => {
|
||||
if (!array) return [];
|
||||
return array.map((item) => {
|
||||
item.getHeadingHTML = function () {
|
||||
let lvl2 = this._highlightResult.hierarchy.lvl2;
|
||||
let lvl3 = this._highlightResult.hierarchy.lvl3;
|
||||
|
||||
if (!lvl3) {
|
||||
if (lvl2) {
|
||||
return lvl2.value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!lvl2) {
|
||||
return lvl3.value;
|
||||
}
|
||||
|
||||
return `${lvl2.value} <span class="text-gray-500"> > </span> ${lvl3.value}`;
|
||||
};
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
export const search = (Alpine, cfg) => ({
|
||||
query: designMode ? 'shortcodes' : '',
|
||||
open: designMode,
|
||||
result: {},
|
||||
|
||||
init() {
|
||||
Alpine.bind(this.$root, this.root);
|
||||
|
||||
this.checkOpen();
|
||||
return this.$nextTick(() => {
|
||||
this.$watch('query', () => {
|
||||
this.search();
|
||||
});
|
||||
});
|
||||
},
|
||||
toggleOpen: function () {
|
||||
this.open = !this.open;
|
||||
this.checkOpen();
|
||||
},
|
||||
checkOpen: function () {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
this.search();
|
||||
this.$nextTick(() => {
|
||||
this.$refs.input.focus();
|
||||
});
|
||||
},
|
||||
|
||||
search: function () {
|
||||
if (!this.query) {
|
||||
this.result = {};
|
||||
return;
|
||||
}
|
||||
var queries = {
|
||||
requests: [
|
||||
{
|
||||
indexName: cfg.index,
|
||||
params: `query=${encodeURIComponent(this.query)}`,
|
||||
attributesToHighlight: ['hierarchy', 'content'],
|
||||
attributesToRetrieve: ['hierarchy', 'url', 'content'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const host = `https://${cfg.app_id}-dsn.algolia.net`;
|
||||
const url = `${host}/1/indexes/*/queries`;
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Algolia-Application-Id': cfg.app_id,
|
||||
'X-Algolia-API-Key': cfg.api_key,
|
||||
},
|
||||
body: JSON.stringify(queries),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.result = groupByLvl0(applyHelperFuncs(data.results[0].hits));
|
||||
});
|
||||
},
|
||||
root: {
|
||||
['@click']() {
|
||||
if (!this.open) {
|
||||
this.toggleOpen();
|
||||
}
|
||||
},
|
||||
['@search-toggle.window']() {
|
||||
this.toggleOpen();
|
||||
},
|
||||
['@keydown.meta.k.window.prevent']() {
|
||||
this.toggleOpen();
|
||||
},
|
||||
},
|
||||
});
|
71
docs/assets/js/alpinejs/data/toc.js
Normal file
71
docs/assets/js/alpinejs/data/toc.js
Normal file
@@ -0,0 +1,71 @@
|
||||
var debug = 0 ? console.log.bind(console, '[toc]') : function () {};
|
||||
|
||||
export const toc = (Alpine) => ({
|
||||
contentScrollSpy: null,
|
||||
activeHeading: '',
|
||||
justClicked: false,
|
||||
|
||||
setActive(id) {
|
||||
debug('setActive', id);
|
||||
this.activeHeading = id;
|
||||
// Prevent the intersection observer from changing the active heading right away.
|
||||
this.justClicked = true;
|
||||
setTimeout(() => {
|
||||
this.justClicked = false;
|
||||
}, 200);
|
||||
},
|
||||
|
||||
init() {
|
||||
this.$watch('$store.nav.scroll.atTop', (value) => {
|
||||
if (!value) return;
|
||||
this.activeHeading = '';
|
||||
this.$root.scrollTop = 0;
|
||||
});
|
||||
|
||||
return this.$nextTick(() => {
|
||||
let contentEl = document.getElementById('content');
|
||||
if (contentEl) {
|
||||
const handleIntersect = (entries) => {
|
||||
if (this.justClicked) {
|
||||
return;
|
||||
}
|
||||
for (let entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
let id = entry.target.id;
|
||||
this.activeHeading = id;
|
||||
let liEl = this.$refs[id];
|
||||
if (liEl) {
|
||||
// If liEl is not in the viewport, scroll it into view.
|
||||
let bounding = liEl.getBoundingClientRect();
|
||||
if (bounding.top < 0 || bounding.bottom > window.innerHeight) {
|
||||
this.$root.scrollTop = liEl.offsetTop - 100;
|
||||
}
|
||||
}
|
||||
debug('intersecting', id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let opts = {
|
||||
rootMargin: '0px 0px -75%',
|
||||
threshold: 0.75,
|
||||
};
|
||||
|
||||
this.contentScrollSpy = new IntersectionObserver(handleIntersect, opts);
|
||||
// Observe all headings.
|
||||
let headings = contentEl.querySelectorAll('h2, h3, h4, h5, h6');
|
||||
for (let heading of headings) {
|
||||
this.contentScrollSpy.observe(heading);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (this.contentScrollSpy) {
|
||||
debug('disconnecting');
|
||||
this.contentScrollSpy.disconnect();
|
||||
}
|
||||
},
|
||||
});
|
36
docs/assets/js/alpinejs/magics/helpers.js
Normal file
36
docs/assets/js/alpinejs/magics/helpers.js
Normal file
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
export function registerMagics(Alpine) {
|
||||
Alpine.magic('copy', (currentEl) => {
|
||||
return function (el) {
|
||||
if (!el) {
|
||||
el = currentEl;
|
||||
}
|
||||
|
||||
// Select the element to copy.
|
||||
let range = document.createRange();
|
||||
range.selectNode(el);
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
|
||||
// Remove the selection after some time.
|
||||
setTimeout(() => {
|
||||
window.getSelection().removeAllRanges();
|
||||
}, 500);
|
||||
|
||||
// Trim whitespace.
|
||||
let text = el.textContent.trim();
|
||||
|
||||
navigator.clipboard.writeText(text);
|
||||
};
|
||||
});
|
||||
|
||||
Alpine.magic('isScrollX', (currentEl) => {
|
||||
return function (el) {
|
||||
if (!el) {
|
||||
el = currentEl;
|
||||
}
|
||||
return el.clientWidth < el.scrollWidth;
|
||||
};
|
||||
});
|
||||
}
|
1
docs/assets/js/alpinejs/magics/index.js
Normal file
1
docs/assets/js/alpinejs/magics/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './helpers';
|
1
docs/assets/js/alpinejs/stores/index.js
Normal file
1
docs/assets/js/alpinejs/stores/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './nav.js';
|
94
docs/assets/js/alpinejs/stores/nav.js
Normal file
94
docs/assets/js/alpinejs/stores/nav.js
Normal file
@@ -0,0 +1,94 @@
|
||||
var debug = 1 ? console.log.bind(console, '[navStore]') : function () {};
|
||||
|
||||
var ColorScheme = {
|
||||
System: 1,
|
||||
Light: 2,
|
||||
Dark: 3,
|
||||
};
|
||||
|
||||
const localStorageUserSettingsKey = 'hugoDocsUserSettings';
|
||||
|
||||
export const navStore = (Alpine) => ({
|
||||
init() {
|
||||
// There is no $watch available in Alpine stores,
|
||||
// but this has the same effect.
|
||||
this.userSettings.onColorSchemeChanged = Alpine.effect(() => {
|
||||
if (this.userSettings.settings.colorScheme) {
|
||||
this.userSettings.isDark = isDark(this.userSettings.settings.colorScheme);
|
||||
toggleDarkMode(this.userSettings.isDark);
|
||||
}
|
||||
});
|
||||
|
||||
// Also react to changes in system settings.
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
this.userSettings.setColorScheme(ColorScheme.System);
|
||||
});
|
||||
},
|
||||
|
||||
destroy() {},
|
||||
|
||||
scroll: {
|
||||
atTop: true,
|
||||
},
|
||||
|
||||
userSettings: {
|
||||
// settings gets persisted between page navigations.
|
||||
settings: Alpine.$persist({
|
||||
// light, dark or system mode.
|
||||
// If not set, we use the OS setting.
|
||||
colorScheme: ColorScheme.System,
|
||||
// Used to show the most relevant tab in config listings etc.
|
||||
configFileType: 'toml',
|
||||
}).as(localStorageUserSettingsKey),
|
||||
|
||||
isDark: false,
|
||||
|
||||
setColorScheme(colorScheme) {
|
||||
this.settings.colorScheme = colorScheme;
|
||||
this.isDark = isDark(colorScheme);
|
||||
},
|
||||
|
||||
toggleColorScheme() {
|
||||
let next = this.settings.colorScheme + 1;
|
||||
if (next > ColorScheme.Dark) {
|
||||
next = ColorScheme.System;
|
||||
}
|
||||
this.setColorScheme(next);
|
||||
},
|
||||
colorScheme() {
|
||||
return this.settings.colorScheme ? this.settings.colorScheme : ColorScheme.System;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function isMediaDark() {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
function isDark(colorScheme) {
|
||||
if (!colorScheme || colorScheme == ColorScheme.System) {
|
||||
return isMediaDark();
|
||||
}
|
||||
|
||||
return colorScheme == ColorScheme.Dark;
|
||||
}
|
||||
|
||||
export function initColorScheme() {
|
||||
// The AlpineJS store has not have been initialized yet, so access the
|
||||
// localStorage directly.
|
||||
let settingsJSON = localStorage[localStorageUserSettingsKey];
|
||||
if (settingsJSON) {
|
||||
let settings = JSON.parse(settingsJSON);
|
||||
toggleDarkMode(isDark(settings.colorScheme));
|
||||
return;
|
||||
}
|
||||
toggleDarkMode(isDark(null));
|
||||
}
|
||||
|
||||
const toggleDarkMode = function (dark) {
|
||||
if (dark) {
|
||||
document.body.classList.add('dark');
|
||||
} else {
|
||||
document.body.classList.remove('dark');
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user