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