Merge commit 'a024bc7d76fcc5e49e8210f9b0896db9ef21861a'

This commit is contained in:
Bjørn Erik Pedersen
2025-02-13 10:40:34 +01:00
817 changed files with 5301 additions and 14766 deletions

View 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;
},
});

View File

@@ -0,0 +1,3 @@
export * from './navbar';
export * from './search';
export * from './toc';

View 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;
},
},
});

View 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">&nbsp;>&nbsp;</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();
},
},
});

View 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();
}
},
});

View 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;
};
});
}

View File

@@ -0,0 +1 @@
export * from './helpers';

View File

@@ -0,0 +1 @@
export * from './nav.js';

View 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');
}
};