a new start
This commit is contained in:
40
_js/src/clap-button.js
Normal file
40
_js/src/clap-button.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2020 Florian Klampfer <https://qwtel.com/>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import "broadcastchannel-polyfill";
|
||||
import { webComponentsReady, stylesheetReady } from "./common";
|
||||
|
||||
(async () => {
|
||||
await Promise.all([
|
||||
...("customElements" in window
|
||||
? []
|
||||
: [
|
||||
import(
|
||||
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents"
|
||||
).then(
|
||||
() =>
|
||||
import(/* webpackChunkName: "shadydom" */ "./polyfills/shadydom"),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
await webComponentsReady;
|
||||
await stylesheetReady;
|
||||
|
||||
if (process.env.GET_CLAPS_API && !window.GET_CLAPS_API)
|
||||
window.GET_CLAPS_API = process.env.GET_CLAPS_API;
|
||||
|
||||
import(/* webpackMode: "eager" */ "@getclaps/button");
|
||||
})();
|
||||
202
_js/src/common.js
Normal file
202
_js/src/common.js
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Observable, of } from "rxjs";
|
||||
|
||||
// HACK: Temporary MS Edge fix
|
||||
// TODO: Move rx-element into separate file or module
|
||||
export {
|
||||
getScrollHeight,
|
||||
getScrollLeft,
|
||||
getScrollTop,
|
||||
} from "@hydecorp/component/lib/util";
|
||||
export { fromMediaQuery, fetchRx } from "@hydecorp/component/lib/creators";
|
||||
export { subscribeWhen, filterWhen } from "@hydecorp/component/lib/operators";
|
||||
export { createIntersectionObservable } from "@hydecorp/component/lib/observers";
|
||||
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
|
||||
export const BREAK_POINT_3 = `(min-width: ${style.getPropertyValue("--break-point-3")})`;
|
||||
export const BREAK_POINT_DYNAMIC = `(min-width: ${style.getPropertyValue("--break-point-dynamic")})`;
|
||||
export const CONTENT_WIDTH_5 = parseFloat(
|
||||
style.getPropertyValue("--content-width-5"),
|
||||
);
|
||||
export const CONTENT_MARGIN_5 = parseFloat(
|
||||
style.getPropertyValue("--content-margin-5"),
|
||||
);
|
||||
export const DRAWER_WIDTH = parseFloat(
|
||||
style.getPropertyValue("--sidebar-width"),
|
||||
);
|
||||
export const HALF_CONTENT = parseFloat(
|
||||
style.getPropertyValue("--half-content"),
|
||||
);
|
||||
|
||||
// Check the user agent for Safari and iOS Safari, to give them some special treatment...
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
export const isSafari = ua.indexOf("safari") > 0 && ua.indexOf("chrome") < 0;
|
||||
export const isMobile = ua.indexOf("mobile") > 0;
|
||||
export const isMobileSafari = isSafari && isMobile;
|
||||
export const isUCBrowser = ua.indexOf("ucbrowser") > 0;
|
||||
export const isFirefox = ua.indexOf("firefox") > 0;
|
||||
export const isFirefoxIOS = ua.indexOf("fxios") > 0 && ua.indexOf("safari") > 0;
|
||||
|
||||
export const hasCSSOM =
|
||||
"attributeStyleMap" in Element.prototype && "CSS" in window && CSS.number;
|
||||
|
||||
export const webComponentsReady = new Promise((res) => {
|
||||
if ("customElements" in window) res(true);
|
||||
else document.addEventListener("WebComponentsReady", res);
|
||||
});
|
||||
|
||||
// FIXME: Replace with something more robust!?
|
||||
export const stylesheetReady = new Promise(function checkCSS(
|
||||
res,
|
||||
rej,
|
||||
retries = 30,
|
||||
) {
|
||||
const drawerEl = document.querySelector("hy-drawer");
|
||||
if (!drawerEl) res(true);
|
||||
else if (getComputedStyle(drawerEl).getPropertyValue("--hy-drawer-width"))
|
||||
res(true);
|
||||
else if (retries <= 0) rej(Error("Stylesheet not loaded within 10 seconds"));
|
||||
else setTimeout(() => checkCSS(res, rej, retries - 1), 1000 / 3);
|
||||
});
|
||||
|
||||
export const once = (el, eventName) =>
|
||||
new Promise((res) => el.addEventListener(eventName, res, { once: true }));
|
||||
export const timeout = (t) => new Promise((res) => setTimeout(res, t));
|
||||
|
||||
// Takes an array of Modernizr feature tests and makes sure they all pass.
|
||||
export function hasFeatures(features) {
|
||||
if (!window.Modernizr) return true;
|
||||
return [...features].every((feature) => {
|
||||
const hasFeature = window.Modernizr[feature];
|
||||
if (!hasFeature && process.env.DEBUG)
|
||||
console.warn(`Feature '${feature}' missing!`);
|
||||
return hasFeature;
|
||||
});
|
||||
}
|
||||
|
||||
// Some functions to hide and show content.
|
||||
export function show() {
|
||||
this.style.display = "block";
|
||||
this.style.visibility = "visible";
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
this.style.display = "none";
|
||||
this.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
export function unshow() {
|
||||
this.style.display = "";
|
||||
this.style.visibility = "";
|
||||
}
|
||||
|
||||
export const unhide = unshow;
|
||||
|
||||
// Same as `el.innerHTML = ''`, but not quite so hacky.
|
||||
export function empty() {
|
||||
while (this?.firstChild) this.removeChild(this.firstChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* An observable wrapper for the WebAnimations API.
|
||||
* Will return an observable that emits once when the animation finishes.
|
||||
* @param {HTMLElement|null} el
|
||||
* @param {AnimationKeyFrame | AnimationKeyFrame[] | null} effect
|
||||
* @param {number|AnimationEffectTiming} timing
|
||||
* @returns {Observable<Event>}
|
||||
*/
|
||||
export function animate(el, effect, timing) {
|
||||
if (!el) return of(new CustomEvent("finish"));
|
||||
|
||||
return Observable.create((observer) => {
|
||||
const anim = el.animate(effect, timing);
|
||||
|
||||
anim.addEventListener("finish", (e) => {
|
||||
observer.next(e);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => observer.complete());
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (anim.playState !== "finished") anim.cancel();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} templateId
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
export function importTemplate(templateId) {
|
||||
const template = document.getElementById(templateId);
|
||||
return template && document.importNode(template.content, true);
|
||||
}
|
||||
|
||||
export const body = document.body || document.documentElement;
|
||||
export const rem = (units) =>
|
||||
units * parseFloat(getComputedStyle(body).fontSize);
|
||||
export const getViewWidth = () => window.innerWidth || body.clientWidth;
|
||||
export const getViewHeight = () => window.innerHeight || body.clientHeight;
|
||||
|
||||
/**
|
||||
* @template Q
|
||||
* @template S
|
||||
* @param {Worker} worker
|
||||
* @param {Q} message
|
||||
* @returns {Promise<S>}
|
||||
*/
|
||||
export function postMessage(worker, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageChannel = new MessageChannel();
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error);
|
||||
} else {
|
||||
resolve(event.data);
|
||||
}
|
||||
};
|
||||
worker.postMessage(message, [messageChannel.port2]);
|
||||
});
|
||||
}
|
||||
|
||||
const promisifyLoad = (loadFn) => (href) =>
|
||||
new Promise((r) => loadFn(href).addEventListener("load", r));
|
||||
|
||||
/** @type {(href: string) => Promise<Event>} */
|
||||
export const loadJS = promisifyLoad(window.loadJS);
|
||||
|
||||
/** @type {(href: string) => Promise<Event>} */
|
||||
export const loadCSS = promisifyLoad(window.loadCSS);
|
||||
|
||||
/**
|
||||
* @param {ArrayLike<Element>} els
|
||||
* @param {IntersectionObserverInit} [options]
|
||||
* @returns {Promise<IntersectionObserverEntry>}
|
||||
*/
|
||||
export function intersectOnce(els, options) {
|
||||
return new Promise((res) => {
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
if (entries.some((x) => x.isIntersecting)) {
|
||||
els.forEach((el) => io.unobserve(el));
|
||||
res(entries.find((x) => x.isIntersecting));
|
||||
}
|
||||
}, options);
|
||||
els.forEach((el) => io.observe(el));
|
||||
});
|
||||
}
|
||||
166
_js/src/cross-fader.js
Normal file
166
_js/src/cross-fader.js
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { EMPTY, of } from "rxjs";
|
||||
import { catchError, finalize, map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { animate, fetchRx } from "./common";
|
||||
|
||||
const RE_CSS_URL = /url\s*\(['"]?(([^'"\\]|\\.)*)['"]?\)/u;
|
||||
|
||||
/** @param {Document} doc */
|
||||
const calcHash = (doc) => {
|
||||
const sidebar = doc.getElementById("_sidebar");
|
||||
const sidebarBg = sidebar?.querySelector(".sidebar-bg");
|
||||
const pageStyle = doc.getElementById("_pageStyle");
|
||||
const source = doc.getElementById("source");
|
||||
// const rule = Array.from(pageStyle?.sheet?.rules ?? []).find(r => r.selectorText === 'html');
|
||||
// const accentColor = rule?.style.getPropertyValue('--accent-color') ?? '';
|
||||
// const themeColor = rule?.style.getPropertyValue('--theme-color') ?? '';
|
||||
return [
|
||||
pageStyle?.innerText?.trim(),
|
||||
sidebar?.classList,
|
||||
sidebarBg?.classList,
|
||||
sidebarBg?.style.backgroundImage,
|
||||
source?.src,
|
||||
].join("\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Consider a URL external if either the protocol, hostname or port is different.
|
||||
* @param {URL} param0
|
||||
* @param {Location=} location
|
||||
*/
|
||||
function isExternal({ protocol, host }, location = window.location) {
|
||||
return protocol !== location.protocol || host !== location.host;
|
||||
}
|
||||
|
||||
const objectURLs = new WeakMap();
|
||||
|
||||
export class CrossFader {
|
||||
/** @param {number} fadeDuration */
|
||||
constructor(fadeDuration) {
|
||||
this.sidebar = document.getElementById("_sidebar");
|
||||
this.fadeDuration = fadeDuration;
|
||||
this.prevHash = calcHash(document);
|
||||
this.themeColorEl = document.querySelector('meta[name="theme-color"]');
|
||||
}
|
||||
|
||||
/** @param {Document} newDocument */
|
||||
fetchImage2(newDocument) {
|
||||
const sidebarBg = newDocument.querySelector(".sidebar-bg");
|
||||
const video = sidebarBg?.querySelector("source");
|
||||
const { backgroundImage = "" } = sidebarBg?.style ?? {};
|
||||
const result = RE_CSS_URL.exec(backgroundImage);
|
||||
const videoUrl = video?.src;
|
||||
if (!result) {
|
||||
return of("");
|
||||
}
|
||||
|
||||
const url = new URL(result[1], window.location.origin);
|
||||
|
||||
return fetchRx(url.href, {
|
||||
method: "GET",
|
||||
headers: { Accept: "image/*" },
|
||||
...(isExternal(url) ? { mode: "cors" } : {}),
|
||||
}).pipe(
|
||||
switchMap((r) => r.blob()),
|
||||
map((blob) => [URL.createObjectURL(blob), videoUrl]),
|
||||
catchError(() => of(url.href)),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {Document} newDocument */
|
||||
fetchImage(newDocument) {
|
||||
const hash = calcHash(newDocument);
|
||||
if (hash === this.prevHash) return EMPTY;
|
||||
|
||||
return this.fetchImage2(newDocument).pipe(
|
||||
map(([objectUrl, videoUrl]) => {
|
||||
/** @type {HTMLDivElement} */
|
||||
const div =
|
||||
newDocument.querySelector(".sidebar-bg") ??
|
||||
document.createElement("div");
|
||||
|
||||
if (objectUrl) {
|
||||
div.style.backgroundImage = `url(${objectUrl})`;
|
||||
objectURLs.set(div, objectUrl);
|
||||
}
|
||||
|
||||
const video = div.querySelector("video");
|
||||
|
||||
if (video && videoUrl) {
|
||||
video.querySelector("source").src = videoUrl;
|
||||
}
|
||||
|
||||
return [div, hash, newDocument];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {Document} newDocument */
|
||||
updateStyle(newDocument) {
|
||||
const classList = newDocument.getElementById("_sidebar")?.classList;
|
||||
if (classList) this.sidebar.setAttribute("class", classList);
|
||||
|
||||
if (this.themeColorEl) {
|
||||
const themeColor = newDocument.head.querySelector(
|
||||
'meta[name="theme-color"]',
|
||||
)?.content;
|
||||
if (themeColor) {
|
||||
window.setTimeout(() => {
|
||||
if (this.themeColorEl) {
|
||||
this.themeColorEl.content = themeColor;
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const pageStyle = document.getElementById("_pageStyle");
|
||||
const newPageStyle = newDocument.getElementById("_pageStyle");
|
||||
if (!newPageStyle) return;
|
||||
pageStyle?.parentNode?.replaceChild(newPageStyle, pageStyle);
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[HTMLDivElement]} param0
|
||||
* @param {[HTMLDListElement, string, Document]} param1
|
||||
*/
|
||||
fade([prevDiv], [div, hash, newDocument]) {
|
||||
prevDiv?.parentNode?.insertBefore(div, prevDiv.nextElementSibling);
|
||||
|
||||
this.updateStyle(newDocument);
|
||||
|
||||
// Only update the prev hash after we're actually in the fade stage
|
||||
this.prevHash = hash;
|
||||
|
||||
return animate(div, [{ opacity: 0 }, { opacity: 1 }], {
|
||||
duration: this.fadeDuration,
|
||||
easing: "ease",
|
||||
}).pipe(
|
||||
finalize(() => {
|
||||
if (objectURLs.has(prevDiv))
|
||||
URL.revokeObjectURL(objectURLs.get(prevDiv));
|
||||
prevDiv?.parentNode?.removeChild(prevDiv);
|
||||
|
||||
div.querySelector("video").play();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
81
_js/src/dark-mode.js
Normal file
81
_js/src/dark-mode.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
|
||||
|
||||
import { importTemplate, stylesheetReady, once } from "./common";
|
||||
|
||||
const SEL_NAVBAR_BTN_BAR = "#_navbar > .content > .nav-btn-bar";
|
||||
|
||||
(async () => {
|
||||
await stylesheetReady;
|
||||
|
||||
const darkMode = importTemplate("_dark-mode-template");
|
||||
if (darkMode) {
|
||||
const navbarEl = document.querySelector(SEL_NAVBAR_BTN_BAR);
|
||||
navbarEl?.insertBefore(
|
||||
darkMode,
|
||||
navbarEl.querySelector(".nav-insert-marker"),
|
||||
);
|
||||
|
||||
const metaEl = document.querySelector('meta[name="color-scheme"]');
|
||||
let tId;
|
||||
|
||||
const navbarBtn = document.getElementById("_dark-mode");
|
||||
navbarBtn?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
clearTimeout(tId);
|
||||
const list = document.body.classList;
|
||||
if (
|
||||
list.contains("dark-mode") ||
|
||||
("_sunset" in window &&
|
||||
!list.contains("light-mode") &&
|
||||
matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
list.remove("dark-mode");
|
||||
list.add("light-mode");
|
||||
tId = setTimeout(() => {
|
||||
if (metaEl) metaEl.content = "light";
|
||||
document.documentElement.style.colorScheme = "light";
|
||||
}, 250);
|
||||
navbarBtn.dispatchEvent(
|
||||
new CustomEvent("pivoine-dark-mode-toggle", {
|
||||
detail: false,
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
list.remove("light-mode");
|
||||
list.add("dark-mode");
|
||||
tId = setTimeout(() => {
|
||||
if (metaEl) metaEl.content = "dark";
|
||||
document.documentElement.style.colorScheme = "dark";
|
||||
}, 250);
|
||||
navbarBtn.dispatchEvent(
|
||||
new CustomEvent("pivoine-dark-mode-toggle", {
|
||||
detail: true,
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await once(document, "click");
|
||||
|
||||
const styleSheets = Array.from(document.styleSheets);
|
||||
const inlineSheet = styleSheets.find(
|
||||
(s) => s.ownerNode?.id === "_styleInline",
|
||||
);
|
||||
const linkSheet = styleSheets.find(
|
||||
(s) => s.ownerNode?.id === "_stylePreload",
|
||||
);
|
||||
const setRule = (sheet) => {
|
||||
if (!sheet) return;
|
||||
const rule = Array.from(sheet.rules).find((rule) =>
|
||||
rule.selectorText.startsWith(".color-transition"),
|
||||
);
|
||||
if (rule)
|
||||
rule.style.transition =
|
||||
"background-color 1s ease, border-color 1s ease";
|
||||
};
|
||||
setRule(inlineSheet);
|
||||
setRule(linkSheet);
|
||||
}
|
||||
})();
|
||||
275
_js/src/drawer.js
Normal file
275
_js/src/drawer.js
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { fromEvent, merge, NEVER, combineLatest } from "rxjs";
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
filter,
|
||||
startWith,
|
||||
switchMap,
|
||||
tap,
|
||||
throttleTime,
|
||||
withLatestFrom,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
BREAK_POINT_3,
|
||||
BREAK_POINT_DYNAMIC,
|
||||
isSafari,
|
||||
isMobile,
|
||||
isMobileSafari,
|
||||
hasCSSOM,
|
||||
webComponentsReady,
|
||||
stylesheetReady,
|
||||
getScrollTop,
|
||||
getViewWidth,
|
||||
fromMediaQuery,
|
||||
} from "./common";
|
||||
|
||||
(async () => {
|
||||
await Promise.all([
|
||||
...("customElements" in window
|
||||
? []
|
||||
: [
|
||||
import(
|
||||
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents"
|
||||
).then(
|
||||
() =>
|
||||
import(/* webpackChunkName: "shadydom" */ "./polyfills/shadydom"),
|
||||
),
|
||||
]),
|
||||
...("ResizeObserver" in window
|
||||
? []
|
||||
: [
|
||||
import(
|
||||
/* webpackChunkName: "resize-observer" */ "./polyfills/resize-observer"
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
await webComponentsReady;
|
||||
await stylesheetReady;
|
||||
|
||||
const MOBILE = 1;
|
||||
const DESKTOP = 2;
|
||||
const LARGE_DESKTOP = 3;
|
||||
|
||||
const subscribeWhen = (p$) => (source) => {
|
||||
if (process.env.DEBUG && !p$) throw Error();
|
||||
return p$.pipe(switchMap((p) => (p ? source : NEVER)));
|
||||
};
|
||||
|
||||
// Determines the range from which to draw the drawer in pixels, counted from the left edge.
|
||||
// It depends on the browser, e.g. Safari has a native gesture when sliding form the side,
|
||||
// so we ignore the first 35 pixels (roughly the range for the native gesture),
|
||||
// to avoid triggering both gestures.
|
||||
function getRange(drawerWidth, size) {
|
||||
if (size >= DESKTOP) return [0, drawerWidth];
|
||||
if (isMobileSafari) return [35, 150];
|
||||
return [0, 150];
|
||||
}
|
||||
|
||||
// The functions below add an svg graphic to the sidebar
|
||||
// that indicate that the sidebar can be drawn using touch gestures.
|
||||
function setupIcon(drawerEl) {
|
||||
const img = document.getElementById("_hrefSwipeSVG");
|
||||
if (img) {
|
||||
const svg = document.createElement("img");
|
||||
svg.id = "_swipe";
|
||||
svg.src = img.href;
|
||||
svg.alt = "Swipe image";
|
||||
svg.addEventListener("click", () => drawerEl.close());
|
||||
document.getElementById("_sidebar")?.appendChild(svg);
|
||||
}
|
||||
}
|
||||
|
||||
function removeIcon() {
|
||||
const svg = document.getElementById("_swipe");
|
||||
svg?.parentNode?.removeChild(svg);
|
||||
}
|
||||
|
||||
const detectSize = () =>
|
||||
window.matchMedia(BREAK_POINT_DYNAMIC).matches
|
||||
? LARGE_DESKTOP
|
||||
: window.matchMedia(BREAK_POINT_3).matches
|
||||
? DESKTOP
|
||||
: MOBILE;
|
||||
|
||||
// First we get hold of some DOM elements.
|
||||
const drawerEl = document.getElementById("_drawer");
|
||||
const sidebarEl = document.getElementById("_sidebar");
|
||||
const contentEl = sidebarEl?.querySelector(".sidebar-sticky");
|
||||
if (!drawerEl || !sidebarEl || !contentEl) return;
|
||||
|
||||
document.getElementById("_menu")?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
drawerEl.toggle();
|
||||
});
|
||||
|
||||
sidebarEl
|
||||
.querySelectorAll('a[href^="/"]:not(.external)')
|
||||
.forEach((el) => el.addEventListener("click", () => drawerEl.close()));
|
||||
|
||||
if (isSafari) drawerEl.setAttribute("threshold", "0");
|
||||
if (!isMobile) drawerEl.setAttribute("mouseevents", "");
|
||||
|
||||
const [tValue, oValue] = hasCSSOM
|
||||
? [
|
||||
new CSSTransformValue([new CSSTranslate(CSS.px(0), CSS.px(0))]),
|
||||
CSS.number(1),
|
||||
]
|
||||
: [null, null];
|
||||
|
||||
const updateSidebar = (t, size, distance) => {
|
||||
const value = distance * t;
|
||||
const opacity = size >= DESKTOP ? 1 : 1 - t;
|
||||
if (hasCSSOM) {
|
||||
tValue[0].x.value = value;
|
||||
oValue.value = opacity;
|
||||
sidebarEl.attributeStyleMap.set("transform", tValue);
|
||||
contentEl.attributeStyleMap.set("opacity", oValue);
|
||||
} else {
|
||||
sidebarEl.style.transform = `translateX(${value}px)`;
|
||||
contentEl.style.opacity = opacity;
|
||||
}
|
||||
};
|
||||
|
||||
// A flag for the 3 major viewport sizes we support
|
||||
const size$ = merge(
|
||||
fromMediaQuery(window.matchMedia(BREAK_POINT_3)),
|
||||
fromMediaQuery(window.matchMedia(BREAK_POINT_DYNAMIC)),
|
||||
).pipe(startWith({}), map(detectSize));
|
||||
|
||||
// An observable keeping track of the drawer (peek) width.
|
||||
const peekWidth$ = fromEvent(drawerEl, "peek-width-change").pipe(
|
||||
map((e) => e.detail),
|
||||
);
|
||||
|
||||
// An observable keeping track the viewport width
|
||||
const viewWidth$ = fromEvent(window, "resize", { passive: true }).pipe(
|
||||
startWith({}),
|
||||
map(getViewWidth),
|
||||
);
|
||||
|
||||
// An observable keeping track of the distance between
|
||||
// the middle point of the screen and the middle point of the drawer.
|
||||
const distance$ = combineLatest(peekWidth$, viewWidth$).pipe(
|
||||
map(([drawerWidth, viewWidth]) => viewWidth / 2 - drawerWidth / 2),
|
||||
);
|
||||
|
||||
const t$ = merge(
|
||||
distance$.pipe(
|
||||
map(() =>
|
||||
drawerEl.opacity !== undefined ? 1 - drawerEl.opacity : opened ? 0 : 1,
|
||||
),
|
||||
),
|
||||
fromEvent(drawerEl, "hy-drawer-move").pipe(
|
||||
map(({ detail: { opacity } }) => {
|
||||
return 1 - opacity;
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
drawerEl.addEventListener("hy-drawer-prepare", () => {
|
||||
sidebarEl.style.willChange = "transform";
|
||||
contentEl.style.willChange = "opacity";
|
||||
});
|
||||
|
||||
drawerEl.addEventListener("hy-drawer-transitioned", () => {
|
||||
sidebarEl.style.willChange = "";
|
||||
contentEl.style.willChange = "";
|
||||
});
|
||||
|
||||
// Save scroll position before the drawer gets initialized.
|
||||
const scrollTop = getScrollTop();
|
||||
|
||||
// Start the drawer in `opened` state when the cover class is present,
|
||||
// and the user hasn't started scrolling already.
|
||||
const opened =
|
||||
drawerEl.classList.contains("cover") &&
|
||||
scrollTop <= 0 &&
|
||||
!(history.state && history.state.closedOnce);
|
||||
|
||||
if (!opened) {
|
||||
if (!history.state) history.replaceState({}, document.title);
|
||||
history.state.closedOnce = true;
|
||||
drawerEl.removeAttribute("opened");
|
||||
}
|
||||
|
||||
const opened$ = fromEvent(drawerEl, "hy-drawer-transitioned").pipe(
|
||||
map((e) => e.detail),
|
||||
distinctUntilChanged(),
|
||||
tap((opened) => {
|
||||
if (!opened) {
|
||||
removeIcon();
|
||||
if (!history.state) history.replaceState({}, document.title);
|
||||
history.state.closedOnce = true;
|
||||
}
|
||||
}),
|
||||
startWith(opened),
|
||||
);
|
||||
|
||||
// We need the height of the drawer in case we need to reset the scroll position
|
||||
const drawerHeight = opened ? null : drawerEl.getBoundingClientRect().height;
|
||||
|
||||
drawerEl.addEventListener(
|
||||
"hy-drawer-init",
|
||||
() => {
|
||||
drawerEl.classList.add("loaded");
|
||||
|
||||
setupIcon(drawerEl);
|
||||
|
||||
if (drawerHeight && scrollTop >= drawerHeight) {
|
||||
window.scrollTo(0, scrollTop - drawerHeight);
|
||||
}
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
await import(/* webpackMode: "eager" */ "@hydecorp/drawer");
|
||||
|
||||
window._drawer = drawerEl;
|
||||
|
||||
t$.pipe(
|
||||
withLatestFrom(size$, distance$),
|
||||
tap((args) => updateSidebar(...args)),
|
||||
).subscribe();
|
||||
|
||||
// Keeping the drawer updated.
|
||||
peekWidth$
|
||||
.pipe(
|
||||
withLatestFrom(size$),
|
||||
map((args) => getRange(...args)),
|
||||
tap((range) => {
|
||||
drawerEl.range = range;
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Hacky way of letting the cover page close when scrolling
|
||||
fromEvent(document, "wheel", { passive: false })
|
||||
.pipe(
|
||||
subscribeWhen(opened$),
|
||||
filter((e) => e.deltaY > 0),
|
||||
tap((e) => {
|
||||
if (drawerEl.translateX > 0) e.preventDefault();
|
||||
}),
|
||||
throttleTime(500),
|
||||
tap(() => drawerEl.close()),
|
||||
)
|
||||
.subscribe();
|
||||
})();
|
||||
78
_js/src/entry.js
Normal file
78
_js/src/entry.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import "@babel/polyfill";
|
||||
|
||||
import "../lib/version";
|
||||
import "../lib/modernizr-custom";
|
||||
import { hasFeatures } from "./common";
|
||||
|
||||
__webpack_public_path__ = window._publicPath;
|
||||
|
||||
const BASELINE = ["classlist", "eventlistener", "queryselector", "template"];
|
||||
const DARK_MODE_FEATURES = ["customproperties"];
|
||||
const DRAWER_FEATURES = [
|
||||
"customproperties",
|
||||
"history",
|
||||
"matchmedia",
|
||||
"opacity",
|
||||
];
|
||||
const PUSH_STATE_FEATURES = [
|
||||
"history",
|
||||
"matchmedia",
|
||||
"opacity",
|
||||
"cssanimations",
|
||||
"cssremunit",
|
||||
"documentfragment",
|
||||
];
|
||||
const CLAP_BUTTON_FEATURES = [
|
||||
"customproperties",
|
||||
"cssanimations",
|
||||
"cssremunit",
|
||||
];
|
||||
const TOC_FEATURES = ["matchmedia", "cssremunit"];
|
||||
|
||||
if (hasFeatures(BASELINE)) {
|
||||
import(/* webpackMode: "eager" */ "./upgrades");
|
||||
|
||||
if (!window._noNavbar) import(/* webpackChunkName: "navbar" */ "./navbar");
|
||||
if (!window._noLightbox)
|
||||
import(/* webpackChunkName: "lightbox" */ "./lightbox");
|
||||
if (!window._noSound) import(/* webpackChunkName: "sound" */ "./sound");
|
||||
|
||||
if (hasFeatures(DARK_MODE_FEATURES)) {
|
||||
// import(/* webpackMode: "eager" */ './pro/cookies-banner');
|
||||
import(/* webpackMode: "eager" */ "./dark-mode");
|
||||
}
|
||||
|
||||
if (!window._noSearch) import(/* webpackChunkName: "search" */ "./search");
|
||||
|
||||
if (window._clapButton && hasFeatures(CLAP_BUTTON_FEATURES)) {
|
||||
import(/* webpackChunkName: "clap-button" */ "./clap-button");
|
||||
}
|
||||
|
||||
// A list of Modernizr tests that are required for the drawer to work.
|
||||
if (!window._noDrawer && hasFeatures(DRAWER_FEATURES)) {
|
||||
import(/* webpackChunkName: "drawer" */ "./drawer");
|
||||
}
|
||||
|
||||
if (!window._noPushState && hasFeatures(PUSH_STATE_FEATURES)) {
|
||||
import(/* webpackChunkName: "push-state" */ "./push-state");
|
||||
}
|
||||
|
||||
// if (!window._noToc && hasFeatures(TOC_FEATURES)) {
|
||||
// import(/* webpackChunkName: "toc" */ './pro/toc');
|
||||
// }
|
||||
}
|
||||
29
_js/src/flip/index.js
Normal file
29
_js/src/flip/index.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { merge } from "rxjs";
|
||||
import { filter } from "rxjs/operators";
|
||||
|
||||
import { setupFLIPTitle } from "./title";
|
||||
|
||||
const FLIP_TYPES = ["title"];
|
||||
|
||||
export function setupFLIP(start$, ready$, fadeIn$, options) {
|
||||
const other$ = start$.pipe(
|
||||
filter(({ flipType }) => !FLIP_TYPES.includes(flipType)),
|
||||
);
|
||||
|
||||
return merge(setupFLIPTitle(start$, ready$, fadeIn$, options), other$);
|
||||
}
|
||||
116
_js/src/flip/title.js
Normal file
116
_js/src/flip/title.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Observable, of, zip } from "rxjs";
|
||||
import { tap, finalize, filter, map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { animate, empty } from "../common";
|
||||
|
||||
const TITLE_SELECTOR = ".page-title, .post-title";
|
||||
|
||||
/**
|
||||
* @param {Observable<any>} start$
|
||||
* @param {Observable<any>} ready$
|
||||
* @param {Observable<any>} fadeIn$
|
||||
* @param {any} opts
|
||||
*/
|
||||
export function setupFLIPTitle(
|
||||
start$,
|
||||
ready$,
|
||||
fadeIn$,
|
||||
{ animationMain, settings },
|
||||
) {
|
||||
if (!animationMain) return start$;
|
||||
|
||||
const flip$ = start$.pipe(
|
||||
filter(({ flipType }) => flipType === "title"),
|
||||
switchMap(({ anchor }) => {
|
||||
if (!anchor) return of({});
|
||||
|
||||
const title = document.createElement("h1");
|
||||
|
||||
title.classList.add("page-title");
|
||||
title.textContent = anchor.textContent;
|
||||
title.style.transformOrigin = "left top";
|
||||
|
||||
const page = animationMain.querySelector(".page");
|
||||
if (!page) return of({});
|
||||
|
||||
empty.call(page);
|
||||
page.appendChild(title);
|
||||
|
||||
animationMain.style.position = "fixed";
|
||||
animationMain.style.opacity = 1;
|
||||
|
||||
const first = anchor.getBoundingClientRect();
|
||||
const last = title.getBoundingClientRect();
|
||||
const firstFontSize = parseInt(getComputedStyle(anchor).fontSize, 10);
|
||||
const lastFontSize = parseInt(getComputedStyle(title).fontSize, 10);
|
||||
|
||||
const invertX = first.left - last.left;
|
||||
const invertY = first.top - last.top;
|
||||
const invertScale = firstFontSize / lastFontSize;
|
||||
|
||||
anchor.style.opacity = 0;
|
||||
|
||||
const transform = [
|
||||
{
|
||||
transform: `translate3d(${invertX}px, ${invertY}px, 0) scale(${invertScale})`,
|
||||
},
|
||||
{ transform: "translate3d(0, 0, 0) scale(1)" },
|
||||
];
|
||||
|
||||
return animate(title, transform, settings).pipe(
|
||||
tap({
|
||||
complete() {
|
||||
animationMain.style.position = "absolute";
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
start$
|
||||
.pipe(
|
||||
switchMap(({ flipType }) =>
|
||||
zip(
|
||||
ready$.pipe(
|
||||
filter(() => flipType === "title"),
|
||||
map(({ replaceEls: [main] }) => {
|
||||
const title = main.querySelector(TITLE_SELECTOR);
|
||||
if (title) title.style.opacity = 0;
|
||||
return title;
|
||||
}),
|
||||
),
|
||||
fadeIn$,
|
||||
).pipe(
|
||||
map(([x]) => x),
|
||||
tap((title) => {
|
||||
if (title) title.style.opacity = 1;
|
||||
animationMain.style.opacity = 0;
|
||||
}),
|
||||
finalize(() => {
|
||||
animationMain.style.opacity = 0;
|
||||
|
||||
const page = animationMain.querySelector(".page");
|
||||
empty.call(page);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return flip$;
|
||||
}
|
||||
337
_js/src/languages.json
Normal file
337
_js/src/languages.json
Normal file
@@ -0,0 +1,337 @@
|
||||
{
|
||||
"abap": "ABAP",
|
||||
"actionscript": "ActionScript",
|
||||
"as": "ActionScript",
|
||||
"as3": "ActionScript",
|
||||
"ada": "Ada",
|
||||
"apache": "Apache",
|
||||
"apex": "Apex",
|
||||
"apiblueprint": "API Blueprint",
|
||||
"apib": "API Blueprint",
|
||||
"applescript": "AppleScript",
|
||||
"armasm": "ArmAsm",
|
||||
"augeas": "Augeas",
|
||||
"aug": "Augeas",
|
||||
"awk": "Awk",
|
||||
"batchfile": "Batchfile",
|
||||
"bat": "Batchfile",
|
||||
"batch": "Batchfile",
|
||||
"dosbatch": "Batchfile",
|
||||
"winbatch": "Batchfile",
|
||||
"bbcbasic": "BBCBASIC",
|
||||
"bibtex": "BibTeX",
|
||||
"bib": "BibTeX",
|
||||
"biml": "BIML",
|
||||
"bpf": "BPF",
|
||||
"brainfuck": "Brainfuck",
|
||||
"bsl": "1C (BSL)",
|
||||
"c": "C",
|
||||
"ceylon": "Ceylon",
|
||||
"cfscript": "CFScript",
|
||||
"cfc": "CFScript",
|
||||
"clean": "Clean",
|
||||
"clojure": "Clojure",
|
||||
"clj": "Clojure",
|
||||
"cljs": "Clojure",
|
||||
"cmake": "CMake",
|
||||
"cmhg": "CMHG",
|
||||
"coffeescript": "CoffeeScript",
|
||||
"coffee": "CoffeeScript",
|
||||
"coffee-script": "CoffeeScript",
|
||||
"common_lisp": "Common Lisp",
|
||||
"cl": "Common Lisp",
|
||||
"common-lisp": "Common Lisp",
|
||||
"elisp": "Common Lisp",
|
||||
"emacs-lisp": "Common Lisp",
|
||||
"lisp": "Common Lisp",
|
||||
"conf": "Config File",
|
||||
"config": "Config File",
|
||||
"configuration": "Config File",
|
||||
"console": "Console",
|
||||
"terminal": "Console",
|
||||
"shell_session": "Console",
|
||||
"shell-session": "Console",
|
||||
"coq": "Coq",
|
||||
"cpp": "C++",
|
||||
"c++": "C++",
|
||||
"crystal": "Crystal",
|
||||
"cr": "Crystal",
|
||||
"csharp": "C#",
|
||||
"c#": "C#",
|
||||
"cs": "C#",
|
||||
"css": "CSS",
|
||||
"csvs": "csvs",
|
||||
"cuda": "CUDA",
|
||||
"cypher": "Cypher",
|
||||
"cython": "Cython",
|
||||
"pyx": "Cython",
|
||||
"pyrex": "Cython",
|
||||
"d": "D",
|
||||
"dlang": "D",
|
||||
"dart": "Dart",
|
||||
"datastudio": "Datastudio",
|
||||
"diff": "diff",
|
||||
"patch": "diff",
|
||||
"udiff": "diff",
|
||||
"digdag": "digdag",
|
||||
"docker": "Docker",
|
||||
"dockerfile": "Docker",
|
||||
"dot": "DOT",
|
||||
"ecl": "ECL",
|
||||
"eex": "EEX",
|
||||
"leex": "EEX",
|
||||
"eiffel": "Eiffel",
|
||||
"elixir": "Elixir",
|
||||
"exs": "Elixir",
|
||||
"elm": "Elm",
|
||||
"epp": "EPP",
|
||||
"erb": "ERB",
|
||||
"eruby": "ERB",
|
||||
"rhtml": "ERB",
|
||||
"erlang": "Erlang",
|
||||
"erl": "Erlang",
|
||||
"escape": "Escape",
|
||||
"esc": "Escape",
|
||||
"factor": "Factor",
|
||||
"fortran": "Fortran",
|
||||
"freefem": "FreeFEM",
|
||||
"ff": "FreeFEM",
|
||||
"fsharp": "FSharp",
|
||||
"gdscript": "GDScript",
|
||||
"gd": "GDScript",
|
||||
"ghc-cmm": "GHC Cmm (C--)",
|
||||
"cmm": "GHC Cmm (C--)",
|
||||
"ghc-core": "GHC Core",
|
||||
"gherkin": "Gherkin",
|
||||
"cucumber": "Gherkin",
|
||||
"behat": "Gherkin",
|
||||
"glsl": "GLSL",
|
||||
"go": "Go",
|
||||
"golang": "Go",
|
||||
"gradle": "Gradle",
|
||||
"graphql": "Graphql",
|
||||
"groovy": "Groovy",
|
||||
"hack": "Hack",
|
||||
"hh": "Hack",
|
||||
"haml": "Haml",
|
||||
"HAML": "Haml",
|
||||
"handlebars": "Handlebars",
|
||||
"hbs": "Handlebars",
|
||||
"mustache": "Handlebars",
|
||||
"haskell": "Haskell",
|
||||
"hs": "Haskell",
|
||||
"haxe": "Haxe",
|
||||
"hx": "Haxe",
|
||||
"hcl": "Hashicorp Configuration Language",
|
||||
"hlsl": "HLSL",
|
||||
"hocon": "HOCON",
|
||||
"hql": "HQL",
|
||||
"html": "HTML",
|
||||
"http": "HTTP",
|
||||
"hylang": "HyLang",
|
||||
"hy": "HyLang",
|
||||
"idlang": "IDL",
|
||||
"igorpro": "IgorPro",
|
||||
"ini": "INI",
|
||||
"io": "Io",
|
||||
"irb": "Irb",
|
||||
"pry": "Irb",
|
||||
"irb_output": "Irb_output",
|
||||
"isbl": "ISBL",
|
||||
"java": "Java",
|
||||
"javascript": "JavaScript",
|
||||
"js": "JavaScript",
|
||||
"jinja": "Jinja",
|
||||
"django": "Jinja",
|
||||
"jsl": "JSL",
|
||||
"json": "JSON",
|
||||
"json-doc": "Json-doc",
|
||||
"jsonc": "Json-doc",
|
||||
"jsonnet": "Jsonnet",
|
||||
"jsp": "Jsp",
|
||||
"jsx": "JSX",
|
||||
"react": "JSX",
|
||||
"julia": "Julia",
|
||||
"jl": "Julia",
|
||||
"kotlin": "Kotlin",
|
||||
"lasso": "Lasso",
|
||||
"lassoscript": "Lasso",
|
||||
"liquid": "Liquid",
|
||||
"literate_coffeescript": "Literate CoffeeScript",
|
||||
"litcoffee": "Literate CoffeeScript",
|
||||
"literate_haskell": "Literate Haskell",
|
||||
"lithaskell": "Literate Haskell",
|
||||
"lhaskell": "Literate Haskell",
|
||||
"lhs": "Literate Haskell",
|
||||
"livescript": "LiveScript",
|
||||
"ls": "LiveScript",
|
||||
"llvm": "LLVM",
|
||||
"lua": "Lua",
|
||||
"lustre": "Lustre",
|
||||
"lutin": "Lutin",
|
||||
"m68k": "M68k",
|
||||
"magik": "Magik",
|
||||
"make": "Make",
|
||||
"makefile": "Make",
|
||||
"mf": "Make",
|
||||
"gnumake": "Make",
|
||||
"bsdmake": "Make",
|
||||
"markdown": "Markdown",
|
||||
"md": "Markdown",
|
||||
"mkd": "Markdown",
|
||||
"mason": "Mason",
|
||||
"mathematica": "Mathematica",
|
||||
"wl": "Mathematica",
|
||||
"matlab": "MATLAB",
|
||||
"m": "MATLAB",
|
||||
"minizinc": "MiniZinc",
|
||||
"moonscript": "MoonScript",
|
||||
"moon": "MoonScript",
|
||||
"mosel": "Mosel",
|
||||
"msgtrans": "MessageTrans",
|
||||
"mxml": "MXML",
|
||||
"nasm": "Nasm",
|
||||
"nesasm": "NesAsm",
|
||||
"nes": "NesAsm",
|
||||
"nginx": "nginx",
|
||||
"nim": "Nim",
|
||||
"nimrod": "Nim",
|
||||
"nix": "Nix",
|
||||
"nixos": "Nix",
|
||||
"objective_c": "Objective-C",
|
||||
"objc": "Objective-C",
|
||||
"obj-c": "Objective-C",
|
||||
"obj_c": "Objective-C",
|
||||
"objectivec": "Objective-C",
|
||||
"objective_cpp": "Objective-C++",
|
||||
"objcpp": "Objective-C++",
|
||||
"obj-cpp": "Objective-C++",
|
||||
"obj_cpp": "Objective-C++",
|
||||
"objectivecpp": "Objective-C++",
|
||||
"objc++": "Objective-C++",
|
||||
"obj-c++": "Objective-C++",
|
||||
"obj_c++": "Objective-C++",
|
||||
"objectivec++": "Objective-C++",
|
||||
"ocaml": "OCaml",
|
||||
"openedge": "OpenEdge ABL",
|
||||
"opentype_feature_file": "OpenType Feature File",
|
||||
"fea": "OpenType Feature File",
|
||||
"opentype": "OpenType Feature File",
|
||||
"opentypefeature": "OpenType Feature File",
|
||||
"pascal": "Pascal",
|
||||
"perl": "Perl",
|
||||
"pl": "Perl",
|
||||
"php": "PHP",
|
||||
"php3": "PHP",
|
||||
"php4": "PHP",
|
||||
"php5": "PHP",
|
||||
"plaintext": "Plain Text",
|
||||
"text": "Plain Text",
|
||||
"plist": "Plist",
|
||||
"pony": "Pony",
|
||||
"powershell": "powershell",
|
||||
"posh": "powershell",
|
||||
"microsoftshell": "powershell",
|
||||
"msshell": "powershell",
|
||||
"praat": "Praat",
|
||||
"prolog": "Prolog",
|
||||
"prometheus": "Prometheus",
|
||||
"properties": ".properties",
|
||||
"protobuf": "Protobuf",
|
||||
"proto": "Protobuf",
|
||||
"puppet": "Puppet",
|
||||
"pp": "Puppet",
|
||||
"python": "Python",
|
||||
"py": "Python",
|
||||
"q": "Q",
|
||||
"kdb+": "Q",
|
||||
"qml": "QML",
|
||||
"r": "R",
|
||||
"R": "R",
|
||||
"s": "R",
|
||||
"S": "R",
|
||||
"racket": "Racket",
|
||||
"reasonml": "ReasonML",
|
||||
"rego": "Rego",
|
||||
"robot_framework": "Robot Framework",
|
||||
"robot": "Robot Framework",
|
||||
"robot-framework": "Robot Framework",
|
||||
"ruby": "Ruby",
|
||||
"rb": "Ruby",
|
||||
"rust": "Rust",
|
||||
"rs": "Rust",
|
||||
"rust,no_run": "Rust",
|
||||
"rs,no_run": "Rust",
|
||||
"rust,ignore": "Rust",
|
||||
"rs,ignore": "Rust",
|
||||
"rust,should_panic": "Rust",
|
||||
"rs,should_panic": "Rust",
|
||||
"sas": "SAS",
|
||||
"sass": "Sass",
|
||||
"scala": "Scala",
|
||||
"scheme": "Scheme",
|
||||
"scss": "SCSS",
|
||||
"sed": "sed",
|
||||
"shell": "shell",
|
||||
"bash": "shell",
|
||||
"zsh": "shell",
|
||||
"ksh": "shell",
|
||||
"sh": "shell",
|
||||
"sieve": "Sieve",
|
||||
"slice": "Slice",
|
||||
"slim": "Slim",
|
||||
"smalltalk": "Smalltalk",
|
||||
"st": "Smalltalk",
|
||||
"squeak": "Smalltalk",
|
||||
"smarty": "Smarty",
|
||||
"sml": "SML",
|
||||
"ml": "SML",
|
||||
"solidity": "Solidity",
|
||||
"sparql": "SPARQL",
|
||||
"sqf": "SQF",
|
||||
"sql": "SQL",
|
||||
"supercollider": "SuperCollider",
|
||||
"swift": "Swift",
|
||||
"tap": "TAP",
|
||||
"tcl": "Tcl",
|
||||
"terraform": "Terraform",
|
||||
"tf": "Terraform",
|
||||
"tex": "TeX",
|
||||
"TeX": "TeX",
|
||||
"LaTeX": "TeX",
|
||||
"latex": "TeX",
|
||||
"toml": "TOML",
|
||||
"tsx": "TSX",
|
||||
"ttcn3": "TTCN3",
|
||||
"tulip": "Tulip",
|
||||
"turtle": "Turtle/TriG",
|
||||
"twig": "Twig",
|
||||
"typescript": "TypeScript",
|
||||
"ts": "TypeScript",
|
||||
"vala": "Vala",
|
||||
"vb": "Visual Basic",
|
||||
"visualbasic": "Visual Basic",
|
||||
"vcl": "VCL: Varnish Configuration Language",
|
||||
"varnishconf": "VCL: Varnish Configuration Language",
|
||||
"varnish": "VCL: Varnish Configuration Language",
|
||||
"velocity": "Velocity",
|
||||
"verilog": "Verilog and System Verilog",
|
||||
"vhdl": "VHDL 2008",
|
||||
"viml": "VimL",
|
||||
"vim": "VimL",
|
||||
"vimscript": "VimL",
|
||||
"ex": "VimL",
|
||||
"vue": "Vue",
|
||||
"vuejs": "Vue",
|
||||
"wollok": "Wollok",
|
||||
"xml": "XML",
|
||||
"xojo": "Xojo",
|
||||
"realbasic": "Xojo",
|
||||
"xpath": "XPath",
|
||||
"xquery": "XQuery",
|
||||
"yaml": "YAML",
|
||||
"yml": "YAML",
|
||||
"yang": "YANG",
|
||||
"zig": "Zig",
|
||||
"zir": "Zig"
|
||||
}
|
||||
31
_js/src/lightbox.js
Normal file
31
_js/src/lightbox.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { fromEvent } from "rxjs";
|
||||
import { webComponentsReady, stylesheetReady } from "./common.js";
|
||||
|
||||
(async () => {
|
||||
await Promise.all([
|
||||
...("customElements" in window
|
||||
? []
|
||||
: [
|
||||
import(
|
||||
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents.js"
|
||||
).then(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "shadydom" */ "./polyfills/shadydom.js"
|
||||
),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
await webComponentsReady;
|
||||
await stylesheetReady;
|
||||
|
||||
await import(/* webpackMode: "eager" */ "fslightbox");
|
||||
|
||||
const pushStateEl = document.querySelector("hy-push-state");
|
||||
const after$ = fromEvent(pushStateEl, "hy-push-state-after");
|
||||
|
||||
after$.subscribe(() => {
|
||||
refreshFsLightbox();
|
||||
});
|
||||
})();
|
||||
98
_js/src/navbar.js
Normal file
98
_js/src/navbar.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2018 Florian Klampfer <https://qwtel.com/>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { fromEvent, timer, merge } from "rxjs";
|
||||
import {
|
||||
map,
|
||||
filter,
|
||||
pairwise,
|
||||
mergeWith,
|
||||
mapTo,
|
||||
tap,
|
||||
switchMap,
|
||||
startWith,
|
||||
share,
|
||||
debounceTime,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import { hasCSSOM, getScrollTop, stylesheetReady, filterWhen } from "./common";
|
||||
|
||||
(async () => {
|
||||
await stylesheetReady;
|
||||
|
||||
const navbarEl = document.getElementById("_navbar");
|
||||
if (!navbarEl) return;
|
||||
|
||||
// FIXME: update when size changes
|
||||
const height = navbarEl.clientHeight;
|
||||
|
||||
let offset = 0;
|
||||
|
||||
const tv = hasCSSOM
|
||||
? new CSSTransformValue([new CSSTranslate(CSS.px(0), CSS.px(0))])
|
||||
: null;
|
||||
|
||||
const checkNavbarInactive = () =>
|
||||
!document.activeElement?.classList.contains("nav-btn");
|
||||
|
||||
const hashchange$ = fromEvent(window, "hashchange").pipe(
|
||||
map((e) => new URL(e.newURL).hash),
|
||||
filter((hash) => hash !== "" && hash !== "#_search-input"),
|
||||
share(),
|
||||
);
|
||||
|
||||
// To disable the navbar while the "scroll into view" animation is active.
|
||||
// Wait for 50ms after scrolling has stopped before unlocking the navbar.
|
||||
const notScrollIntoView$ = hashchange$.pipe(
|
||||
switchMap(() =>
|
||||
fromEvent(document, "scroll").pipe(
|
||||
debounceTime(50),
|
||||
mapTo(true),
|
||||
startWith(false),
|
||||
),
|
||||
),
|
||||
startWith(true),
|
||||
);
|
||||
|
||||
// Certain events should make the navbar "jump" to a new position.
|
||||
const jump$ = merge(
|
||||
// Focusing any navbar element should show the navbar to enable keyboard-only interaction.
|
||||
fromEvent(navbarEl, "focus", { capture: true }).pipe(mapTo(2 * height)),
|
||||
// Scrolling to a certain headline should hide the navbar to prevent hiding it.
|
||||
hashchange$.pipe(mapTo(-2 * height)),
|
||||
);
|
||||
|
||||
fromEvent(document, "scroll", { passive: true })
|
||||
.pipe(
|
||||
filterWhen(notScrollIntoView$),
|
||||
map(getScrollTop),
|
||||
filter((x) => x >= 0),
|
||||
pairwise(),
|
||||
map(([prev, curr]) => prev - curr),
|
||||
filter(checkNavbarInactive),
|
||||
mergeWith(jump$),
|
||||
tap((x) => {
|
||||
offset += x;
|
||||
offset = Math.max(-height, Math.min(0, offset));
|
||||
if (hasCSSOM) {
|
||||
tv[0].y.value = offset;
|
||||
navbarEl.attributeStyleMap.set("transform", tv);
|
||||
} else {
|
||||
navbarEl.style.transform = `translateY(${offset}px)`;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
})();
|
||||
2
_js/src/polyfills/fetch.js
Normal file
2
_js/src/polyfills/fetch.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import "whatwg-fetch";
|
||||
import "abortcontroller-polyfill/dist/polyfill-patch-fetch";
|
||||
2
_js/src/polyfills/resize-observer.js
Normal file
2
_js/src/polyfills/resize-observer.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
window.ResizeObserver = window.ResizeObserver || ResizeObserver;
|
||||
2
_js/src/polyfills/shadydom.js
Normal file
2
_js/src/polyfills/shadydom.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import "@webcomponents/shadydom";
|
||||
import "@webcomponents/shadycss/entrypoints/scoping-shim";
|
||||
41
_js/src/polyfills/webcomponents.js
Normal file
41
_js/src/polyfills/webcomponents.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import "@webcomponents/webcomponents-platform";
|
||||
import "@webcomponents/url";
|
||||
import "@webcomponents/template";
|
||||
import "@webcomponents/custom-elements";
|
||||
|
||||
const { customElements } = window;
|
||||
|
||||
let shouldFlush = false;
|
||||
/** @type {?function()} */
|
||||
let flusher = null;
|
||||
|
||||
if (customElements["polyfillWrapFlushCallback"]) {
|
||||
customElements["polyfillWrapFlushCallback"]((flush) => {
|
||||
flusher = flush;
|
||||
if (shouldFlush) {
|
||||
flush();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function flushAndFire() {
|
||||
if (window.HTMLTemplateElement.bootstrap) {
|
||||
window.HTMLTemplateElement.bootstrap(window.document);
|
||||
}
|
||||
flusher && flusher();
|
||||
shouldFlush = true;
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("WebComponentsReady", { bubbles: true }),
|
||||
);
|
||||
}
|
||||
|
||||
if (document.readyState !== "complete") {
|
||||
// this script may come between DCL and load, so listen for both, and cancel load listener if DCL fires
|
||||
window.addEventListener("load", flushAndFire);
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
window.removeEventListener("load", flushAndFire);
|
||||
flushAndFire();
|
||||
});
|
||||
} else {
|
||||
flushAndFire();
|
||||
}
|
||||
232
_js/src/push-state.js
Normal file
232
_js/src/push-state.js
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { fromEvent, merge, timer, zip } from "rxjs";
|
||||
import {
|
||||
tap,
|
||||
exhaustMap,
|
||||
map,
|
||||
mapTo,
|
||||
mergeMap,
|
||||
pairwise,
|
||||
share,
|
||||
startWith,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import { animate, empty, importTemplate, webComponentsReady } from "./common";
|
||||
import { CrossFader } from "./cross-fader";
|
||||
import { setupFLIP } from "./flip";
|
||||
|
||||
(async () => {
|
||||
await Promise.all([
|
||||
...("fetch" in window
|
||||
? []
|
||||
: [import(/* webpackChunkName: "fetch" */ "./polyfills/fetch")]),
|
||||
...("customElements" in window
|
||||
? []
|
||||
: [
|
||||
import(
|
||||
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents"
|
||||
),
|
||||
]),
|
||||
...("animate" in Element.prototype
|
||||
? []
|
||||
: [import(/* webpackChunkName: "webanimations" */ "web-animations-js")]),
|
||||
...("IntersectionObserver" in window
|
||||
? []
|
||||
: [
|
||||
import(
|
||||
/* webpackChunkName: "intersection-observer" */ "intersection-observer"
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
await webComponentsReady;
|
||||
|
||||
const NAVBAR_SEL = "#_navbar > .content > .nav-btn-bar";
|
||||
|
||||
const CROSS_FADE_DURATION = 2000;
|
||||
|
||||
const FADE_OUT = [{ opacity: 1 }, { opacity: 0 }];
|
||||
const FADE_IN = [
|
||||
{ opacity: 0, transform: "translateY(-3rem)" },
|
||||
{ opacity: 1, transform: "translateY(0)" },
|
||||
];
|
||||
|
||||
function setupAnimationMain(pushStateEl) {
|
||||
pushStateEl?.parentNode?.insertBefore(
|
||||
importTemplate("_animation-template"),
|
||||
pushStateEl,
|
||||
);
|
||||
return pushStateEl?.previousElementSibling;
|
||||
}
|
||||
|
||||
function setupLoading(navbarEl) {
|
||||
navbarEl?.appendChild(importTemplate("_loading-template"));
|
||||
return navbarEl?.lastElementChild;
|
||||
}
|
||||
|
||||
function setupErrorPage(main, url) {
|
||||
const { pathname } = url;
|
||||
const error = importTemplate("_error-template");
|
||||
const anchor = error?.querySelector(".this-link");
|
||||
if (anchor) {
|
||||
anchor.href = pathname;
|
||||
anchor.textContent = pathname;
|
||||
}
|
||||
main?.appendChild(error);
|
||||
return main?.lastElementChild;
|
||||
}
|
||||
|
||||
function getFlipType(el) {
|
||||
if (el?.classList.contains("flip-title")) return "title";
|
||||
if (el?.classList.contains("flip-project")) return "project";
|
||||
return el?.getAttribute?.("data-flip");
|
||||
}
|
||||
|
||||
const pushStateEl = document.querySelector("hy-push-state");
|
||||
if (!pushStateEl) return;
|
||||
|
||||
const duration = Number(pushStateEl.getAttribute("duration")) || 350;
|
||||
const settings = {
|
||||
duration: duration,
|
||||
easing: "ease",
|
||||
};
|
||||
|
||||
const animateFadeOut = ({ main }) =>
|
||||
animate(main, FADE_OUT, { ...settings, fill: "forwards" }).pipe(
|
||||
mapTo({ main }),
|
||||
);
|
||||
|
||||
const animateFadeIn = ({ replaceEls: [main], flipType }) =>
|
||||
animate(main, FADE_IN, settings).pipe(mapTo({ main, flipType }));
|
||||
|
||||
const drawerEl = document.querySelector("hy-drawer");
|
||||
const navbarEl = document.querySelector(NAVBAR_SEL);
|
||||
|
||||
const animationMain = setupAnimationMain(pushStateEl);
|
||||
const loadingEl = setupLoading(navbarEl);
|
||||
|
||||
const fromEventX = (eventName, mapFn) =>
|
||||
fromEvent(pushStateEl, eventName).pipe(
|
||||
map(({ detail }) => detail),
|
||||
mapFn ? map(mapFn) : (_) => _,
|
||||
share(),
|
||||
);
|
||||
|
||||
const start$ = fromEventX("hy-push-state-start", (detail) =>
|
||||
Object.assign(detail, { flipType: getFlipType(detail.anchor) }),
|
||||
);
|
||||
const ready$ = fromEventX("hy-push-state-ready");
|
||||
const after$ = fromEventX("hy-push-state-after");
|
||||
const progress$ = fromEventX("hy-push-state-progress");
|
||||
const error$ = fromEventX("hy-push-state-networkerror");
|
||||
|
||||
const fadeOut$ = start$.pipe(
|
||||
map((context) =>
|
||||
Object.assign(context, { main: document.getElementById("_main") }),
|
||||
),
|
||||
tap(({ main }) => {
|
||||
main.style.pointerEvents = "none";
|
||||
}),
|
||||
window._noDrawer && drawerEl?.classList.contains("cover")
|
||||
? tap(() => {
|
||||
drawerEl.classList.remove("cover");
|
||||
drawerEl.parentNode?.appendChild(drawerEl);
|
||||
})
|
||||
: (_) => _,
|
||||
exhaustMap(animateFadeOut),
|
||||
tap(({ main }) => empty.call(main)),
|
||||
share(),
|
||||
);
|
||||
|
||||
if (loadingEl) {
|
||||
progress$.subscribe(() => {
|
||||
loadingEl.style.display = "flex";
|
||||
});
|
||||
ready$.subscribe(() => {
|
||||
loadingEl.style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
const fadeIn$ = after$.pipe(
|
||||
switchMap((context) => {
|
||||
const p = animateFadeIn(context).toPromise();
|
||||
context.transitionUntil(p);
|
||||
return p;
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
const flip$ = setupFLIP(start$, ready$, merge(fadeIn$, error$), {
|
||||
animationMain,
|
||||
settings: settings,
|
||||
}).pipe(share());
|
||||
|
||||
start$
|
||||
.pipe(
|
||||
switchMap((context) => {
|
||||
const promise = zip(timer(duration), fadeOut$, flip$).toPromise();
|
||||
context.transitionUntil(promise);
|
||||
return promise;
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// FIXME: Keeping permanent subscription? turn into hot observable?
|
||||
fadeOut$.subscribe();
|
||||
flip$.subscribe();
|
||||
|
||||
const sidebarBg = document.querySelector(".sidebar-bg");
|
||||
if (sidebarBg) {
|
||||
const crossFader = new CrossFader(CROSS_FADE_DURATION);
|
||||
after$
|
||||
.pipe(
|
||||
switchMap(({ document }) =>
|
||||
zip(crossFader.fetchImage(document), fadeIn$).pipe(
|
||||
map(([x]) => x),
|
||||
takeUntil(start$),
|
||||
),
|
||||
),
|
||||
startWith([sidebarBg]),
|
||||
pairwise(),
|
||||
mergeMap(([prev, curr]) => crossFader.fade(prev, curr)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
error$
|
||||
.pipe(
|
||||
switchMap(({ url }) => {
|
||||
if (loadingEl) loadingEl.style.display = "none";
|
||||
|
||||
const main = document.getElementById("_main");
|
||||
if (main) main.style.pointerEvents = "";
|
||||
empty.call(animationMain?.querySelector(".page"));
|
||||
empty.call(main);
|
||||
|
||||
setupErrorPage(main, url);
|
||||
|
||||
return animate(main, FADE_IN, { ...settings, fill: "forwards" });
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
import(/* webpackMode: "eager" */ "@hydecorp/push-state");
|
||||
|
||||
window._pushState = pushStateEl;
|
||||
})();
|
||||
138
_js/src/search.js
Normal file
138
_js/src/search.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import { fromEvent, merge, timer, zip } from "rxjs";
|
||||
import {
|
||||
tap,
|
||||
exhaustMap,
|
||||
map,
|
||||
mapTo,
|
||||
mergeMap,
|
||||
pairwise,
|
||||
share,
|
||||
startWith,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
filter,
|
||||
} from "rxjs/operators";
|
||||
import { webComponentsReady, importTemplate, stylesheetReady } from "./common";
|
||||
|
||||
(async () => {
|
||||
await Promise.all([
|
||||
...("customElements" in window
|
||||
? []
|
||||
: [
|
||||
import(
|
||||
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents.js"
|
||||
).then(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "shadydom" */ "./polyfills/shadydom.js"
|
||||
),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
await webComponentsReady;
|
||||
await stylesheetReady;
|
||||
|
||||
const SEL_NAVBAR_BTN_BAR = "#_navbar > .content > .nav-btn-bar";
|
||||
|
||||
const search = importTemplate("_search-template");
|
||||
const pushStateEl = document.querySelector("hy-push-state");
|
||||
|
||||
if (!pushStateEl || !search) return;
|
||||
|
||||
await import(/* webpackMode: "eager" */ "@honeymachine/search");
|
||||
|
||||
const navbarEl = document.querySelector(SEL_NAVBAR_BTN_BAR);
|
||||
navbarEl?.insertBefore(search, navbarEl.querySelector(".nav-insert-marker"));
|
||||
|
||||
const searchBtn = document.getElementById("_search");
|
||||
const searchEl = document.querySelector("hm-search");
|
||||
|
||||
const documents = await fetch("/search.json").then((r) => r.json());
|
||||
|
||||
searchEl.setAttribute("documents", JSON.stringify(documents));
|
||||
searchEl.setAttribute(
|
||||
"fields",
|
||||
JSON.stringify(["title", "description", "category", "tags"]),
|
||||
);
|
||||
|
||||
const start$ = fromEvent(pushStateEl, "hy-push-state-start");
|
||||
const ready$ = fromEvent(pushStateEl, "hy-push-state-after");
|
||||
const search$ = fromEvent(searchEl, "search");
|
||||
const click$ = fromEvent(searchBtn, "click");
|
||||
|
||||
let articles = (" " + document.getElementById("_main").innerHTML).slice(1);
|
||||
|
||||
const reset = () => {
|
||||
const result = document.getElementById("_main");
|
||||
result.classList.remove("search-results");
|
||||
result.innerHTML = articles;
|
||||
result.querySelectorAll("img, h1").forEach((article) => {
|
||||
article.setAttribute("style", "opacity: 1;");
|
||||
});
|
||||
};
|
||||
|
||||
start$.subscribe(() => {
|
||||
searchEl.clear();
|
||||
reset();
|
||||
});
|
||||
|
||||
ready$.subscribe(() => {
|
||||
const result = document.getElementById("_main");
|
||||
if (result.innerHTML) {
|
||||
articles = (" " + result.innerHTML).slice(1);
|
||||
}
|
||||
});
|
||||
|
||||
search$.subscribe((e) => {
|
||||
const result = document.getElementById("_main");
|
||||
const hits = e.detail;
|
||||
|
||||
if (hits.length === 0) {
|
||||
reset();
|
||||
} else {
|
||||
result.classList.add("search-results");
|
||||
result.innerHTML = "";
|
||||
hits.forEach((hit) => {
|
||||
const item = documents.find((doc) => doc.id === hit.ref);
|
||||
const articleEl = document.createElement("article");
|
||||
articleEl.classList.add("search-result", "page", "post", "mb6");
|
||||
articleEl.setAttribute("role", "article");
|
||||
articleEl.setAttribute("id", "post-" + item.id);
|
||||
articleEl.innerHTML = `
|
||||
<header>
|
||||
<h1 class="post-title flip-project-title">
|
||||
<a href="${item.url}" class="flip-title">${item.title}</a>
|
||||
</h1>
|
||||
<a
|
||||
href="${item.url}"
|
||||
class="no-hover no-print-link flip-project"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="img-wrapper lead aspect-ratio sixteen-nine flip-project-img">
|
||||
<img
|
||||
src="${item.image}"
|
||||
alt="${item.title}"
|
||||
width="864"
|
||||
height="486"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<p class="note-sm">
|
||||
${item.description}
|
||||
</p>
|
||||
</header>
|
||||
`;
|
||||
|
||||
result.appendChild(articleEl);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
click$.subscribe(() => {
|
||||
searchEl.hidden = !searchEl.hidden;
|
||||
!searchEl.active ? searchEl.focus() : searchEl.clear();
|
||||
searchEl.setAttribute("aria-expanded", !searchEl.hidden);
|
||||
});
|
||||
})();
|
||||
120
_js/src/sound.js
Normal file
120
_js/src/sound.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import { fromEvent } from "rxjs";
|
||||
import { webComponentsReady, stylesheetReady } from "./common.js";
|
||||
import { Wave } from "@foobar404/wave";
|
||||
|
||||
(async () => {
|
||||
await Promise.all([
|
||||
...("customElements" in window
|
||||
? []
|
||||
: [
|
||||
import(
|
||||
/* webpackChunkName: "webcomponents" */ "./polyfills/webcomponents.js"
|
||||
).then(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "shadydom" */ "./polyfills/shadydom.js"
|
||||
),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
await webComponentsReady;
|
||||
await stylesheetReady;
|
||||
|
||||
const sound = document.querySelector(".sound-player");
|
||||
|
||||
if (!sound) return;
|
||||
|
||||
const fallback = JSON.parse(sound.getAttribute("data-featured"));
|
||||
let featured = fallback;
|
||||
let currentIndex = undefined;
|
||||
|
||||
const randomTrackIndex = () =>
|
||||
Math.floor(Math.random() * (featured.tracks.length - 1));
|
||||
|
||||
const updateTrack = (index, active) => {
|
||||
if (index != null) {
|
||||
const track = featured.tracks[index];
|
||||
audioElement.setAttribute(
|
||||
"src",
|
||||
`/assets/sounds/${featured.slug}/${track}`,
|
||||
);
|
||||
currentIndex = index;
|
||||
}
|
||||
|
||||
if (active) {
|
||||
const track = featured.tracks[currentIndex];
|
||||
buttonElement.textContent = `[ ${track.substring(0, track.lastIndexOf("."))} ]`;
|
||||
audioElement.play();
|
||||
canvasElement.classList.remove("hidden");
|
||||
} else {
|
||||
buttonElement.textContent = "[ SOUND ON ]";
|
||||
audioElement.pause();
|
||||
canvasElement.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
const updateSound = () => {
|
||||
const sound = document.querySelector(".sound-wrapper");
|
||||
|
||||
if (!sound) return;
|
||||
|
||||
featured = JSON.parse(sound.getAttribute("data-featured"));
|
||||
updateTrack(0, !audioElement.paused);
|
||||
|
||||
const buttons = document.querySelectorAll(".sound-wrapper .sound-item");
|
||||
buttons.forEach((button, i) => {
|
||||
fromEvent(button, "click").subscribe((event) => {
|
||||
event.preventDefault();
|
||||
updateTrack(i, true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const audioElement = document.querySelector(".sound-player audio");
|
||||
const buttonElement = document.querySelector(".sound-player a");
|
||||
const canvasElement = document.querySelector(".sound-player canvas");
|
||||
const pushStateEl = document.querySelector("hy-push-state");
|
||||
const wave = new Wave(audioElement, canvasElement);
|
||||
|
||||
wave.addAnimation(
|
||||
new wave.animations.Arcs({
|
||||
lineColor: {
|
||||
gradient: [
|
||||
"#d16ba5",
|
||||
"#c777b9",
|
||||
"#ba83ca",
|
||||
"#aa8fd8",
|
||||
"#9a9ae1",
|
||||
"#8aa7ec",
|
||||
"#79b3f4",
|
||||
"#69bff8",
|
||||
"#52cffe",
|
||||
"#41dfff",
|
||||
"#46eefa",
|
||||
"#5ffbf1",
|
||||
],
|
||||
rotate: 45,
|
||||
},
|
||||
lineWidth: 10,
|
||||
diameter: 4,
|
||||
count: 20,
|
||||
frequencyBand: "lows",
|
||||
}),
|
||||
);
|
||||
|
||||
fromEvent(buttonElement, "click").subscribe(() => {
|
||||
updateTrack(undefined, audioElement.paused);
|
||||
});
|
||||
|
||||
fromEvent(audioElement, "ended").subscribe(() => {
|
||||
updateTrack(randomTrackIndex(), true);
|
||||
});
|
||||
|
||||
fromEvent(pushStateEl, "hy-push-state-after").subscribe(() => {
|
||||
updateSound();
|
||||
});
|
||||
|
||||
updateTrack(randomTrackIndex(), false);
|
||||
updateSound();
|
||||
})();
|
||||
320
_js/src/upgrades.js
Normal file
320
_js/src/upgrades.js
Normal file
@@ -0,0 +1,320 @@
|
||||
import {
|
||||
importTemplate,
|
||||
intersectOnce,
|
||||
loadCSS,
|
||||
stylesheetReady,
|
||||
once,
|
||||
} from "./common";
|
||||
import { fromEvent } from "rxjs";
|
||||
import { concatMap } from "rxjs/operators";
|
||||
import { createElement } from "create-element-x/library";
|
||||
import tippy from "tippy.js";
|
||||
|
||||
// import LANG from './languages.json';
|
||||
|
||||
const toggleClass = (element, ...cls) => {
|
||||
element.classList.remove(...cls);
|
||||
void element.offsetWidth;
|
||||
element.classList.add(...cls);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await Promise.all([
|
||||
...("animate" in Element.prototype
|
||||
? []
|
||||
: [import(/* webpackChunkName: "webanimations" */ "web-animations-js")]),
|
||||
...("IntersectionObserver" in window
|
||||
? []
|
||||
: [
|
||||
import(
|
||||
/* webpackChunkName: "intersection-observer" */ "intersection-observer"
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
await stylesheetReady;
|
||||
|
||||
const FN_SEL = "li[id^='fn:']";
|
||||
const FN_LINK_SEL = "a[href^='#fn:']";
|
||||
const HORIZONTAL_SCROLL_SEL =
|
||||
'pre, table:not(.highlight), .katex-display, .break-layout, mjx-container[jax="CHTML"][display="true"]';
|
||||
const CODE_BLOCK_SEL = "pre.highlight > code";
|
||||
const CODE_TITLE_RE = /(?:title|file):\s*['"`](([^'"`\\]|\\.)*)['"`]/iu;
|
||||
const HEADING_SELECTOR = "h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]";
|
||||
|
||||
const IMG_FADE_DURATION = 500;
|
||||
const IMG_KEYFRAMES = [{ opacity: 0 }, { opacity: 1 }];
|
||||
const IMG_SETTINGS = {
|
||||
fill: "forwards",
|
||||
duration: IMG_FADE_DURATION,
|
||||
easing: "ease",
|
||||
};
|
||||
|
||||
const pushStateEl = document.querySelector("hy-push-state");
|
||||
|
||||
const CODE_LINE_HEIGHT = 1.5;
|
||||
|
||||
/** @param {(param0: HTMLElement|null) => void} fn
|
||||
* @param {any=} opts */
|
||||
function ready(fn, opts) {
|
||||
if (pushStateEl && !window._noPushState) {
|
||||
pushStateEl.addEventListener(
|
||||
"hy-push-state-ready",
|
||||
({
|
||||
detail: {
|
||||
replaceEls: [main],
|
||||
},
|
||||
}) => fn(main),
|
||||
opts,
|
||||
);
|
||||
}
|
||||
fn(document.getElementById("_main"));
|
||||
}
|
||||
|
||||
/** @param {(param0: HTMLElement|null) => void} fn
|
||||
* @param {any=} opts */
|
||||
function load(fn, opts) {
|
||||
if (pushStateEl && !window._noPushState) {
|
||||
pushStateEl.addEventListener(
|
||||
"hy-push-state-load",
|
||||
() => fn(document.getElementById("_main")),
|
||||
opts,
|
||||
);
|
||||
}
|
||||
fn(document.getElementById("_main"));
|
||||
}
|
||||
|
||||
let init = true;
|
||||
|
||||
ready((main) => {
|
||||
if (!main) return;
|
||||
|
||||
tippy(main.querySelectorAll(".post-date > .ellipsis"), {
|
||||
trigger: "click",
|
||||
touch: true,
|
||||
interactive: true,
|
||||
allowHTML: true,
|
||||
maxWidth: "none",
|
||||
placement: "bottom-start",
|
||||
offset: 0,
|
||||
content: (el) => el.innerHTML,
|
||||
onTrigger(instance, event) {
|
||||
if (event.target.tagName === "A") {
|
||||
instance._hideOnce = true;
|
||||
}
|
||||
},
|
||||
onShow(instance) {
|
||||
if (instance._hideOnce) {
|
||||
return (instance._hideOnce = false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
tippy(main.querySelectorAll("abbr[title]"), {
|
||||
trigger: "click",
|
||||
touch: true,
|
||||
maxWidth: 500,
|
||||
content: (el) => el.getAttribute("title"),
|
||||
});
|
||||
|
||||
tippy(main.querySelectorAll(".sidebar-social [title]"), {
|
||||
touch: true,
|
||||
content: (el) => el.getAttribute("title"),
|
||||
});
|
||||
|
||||
main.querySelectorAll(HEADING_SELECTOR).forEach((h) => {
|
||||
const df = importTemplate("_permalink-template");
|
||||
const a = df.querySelector(".permalink");
|
||||
a.href = `#${h.id}`;
|
||||
h.appendChild(df);
|
||||
});
|
||||
|
||||
const toc = main.querySelector("#markdown-toc");
|
||||
if (toc) toc.classList.add("toc-hide");
|
||||
|
||||
if ("clipboard" in navigator && "ClipboardItem" in window) {
|
||||
Array.from(main.querySelectorAll(CODE_BLOCK_SEL)).forEach((el) => {
|
||||
const container = el?.parentNode?.parentNode;
|
||||
const writeText = async () => {
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
"text/plain": new Blob([el.textContent], { type: "text/plain" }),
|
||||
}),
|
||||
]);
|
||||
toggleClass(copyBtn, "copy-success");
|
||||
};
|
||||
const copyBtn = createElement(
|
||||
"button",
|
||||
{ onClick: writeText },
|
||||
createElement("small", { class: "icon-copy", title: "Copy" }),
|
||||
createElement("small", { class: "icon-checkmark", title: "Done" }),
|
||||
);
|
||||
container?.appendChild(copyBtn);
|
||||
});
|
||||
}
|
||||
|
||||
Array.from(main.querySelectorAll(CODE_BLOCK_SEL))
|
||||
.map((code) => code.children[0])
|
||||
.forEach((el) => {
|
||||
const result = CODE_TITLE_RE.exec(el?.innerText);
|
||||
if (!result) return;
|
||||
const [, fileName] = result;
|
||||
|
||||
const code = el.parentNode;
|
||||
|
||||
// Remove the first line
|
||||
const child0 = el.childNodes[0];
|
||||
const nli = child0.wholeText.indexOf("\n");
|
||||
if (nli > -1) {
|
||||
const restNode = child0.splitText(nli);
|
||||
code.insertBefore(restNode, code.firstChild);
|
||||
}
|
||||
|
||||
// Remove element before making changes
|
||||
code.removeChild(el);
|
||||
|
||||
// Remove newline
|
||||
code.childNodes[0].splitText(1);
|
||||
code.removeChild(code.childNodes[0]);
|
||||
|
||||
const container = code.parentNode.parentNode;
|
||||
|
||||
// Language
|
||||
// const highlighter = container.parentNode;
|
||||
// const [, lang] = highlighter.classList.value.match(/language-(\w*)/) ?? [];
|
||||
// const language = LANG[lang];
|
||||
|
||||
const header = createElement(
|
||||
"div",
|
||||
{ class: "pre-header break-layout" },
|
||||
createElement(
|
||||
"span",
|
||||
{},
|
||||
createElement("small", { class: "icon-file-empty" }),
|
||||
" ",
|
||||
fileName,
|
||||
),
|
||||
// !language ? null : createElement('small', { class: 'fr lang' }, language),
|
||||
);
|
||||
|
||||
container.insertBefore(header, container.firstChild);
|
||||
});
|
||||
|
||||
if ("complete" in HTMLImageElement.prototype) {
|
||||
main
|
||||
.querySelectorAll("img[width][height][loading=lazy]")
|
||||
.forEach((el) => {
|
||||
if (init && el.complete) return;
|
||||
el.style.opacity = "0";
|
||||
// TODO: replace with loading spinner
|
||||
el.addEventListener(
|
||||
"load",
|
||||
() => el.animate(IMG_KEYFRAMES, IMG_SETTINGS),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
init = false;
|
||||
}
|
||||
|
||||
// main.querySelectorAll(pushStateEl.linkSelector).forEach(anchor => {
|
||||
// caches.match(anchor.href).then(m => {
|
||||
// if (m) requestAnimationFrame(() => anchor.classList.add("visited"));
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
/** @type {Promise<{}>|null} */
|
||||
let katexPromise = null;
|
||||
|
||||
load(() => {
|
||||
const main = document.getElementById("_main");
|
||||
if (!main) return;
|
||||
|
||||
const toc = main.querySelector("#markdown-toc");
|
||||
if (toc) {
|
||||
toc.classList.remove("toc-hide");
|
||||
toc.classList.add("toc-show");
|
||||
}
|
||||
|
||||
main.querySelectorAll(FN_SEL).forEach((li) => (li.tabIndex = 0));
|
||||
|
||||
main
|
||||
.querySelectorAll(FN_LINK_SEL)
|
||||
.forEach((a) =>
|
||||
a.addEventListener("click", (e) =>
|
||||
document
|
||||
.getElementById(e.currentTarget.getAttribute("href").substr(1))
|
||||
?.focus(),
|
||||
),
|
||||
);
|
||||
|
||||
main
|
||||
.querySelectorAll(HORIZONTAL_SCROLL_SEL)
|
||||
.forEach((el) =>
|
||||
el.addEventListener(
|
||||
"touchstart",
|
||||
(e) => el.scrollLeft > 0 && e.stopPropagation(),
|
||||
{ passive: false },
|
||||
),
|
||||
);
|
||||
|
||||
// Array.from(main.querySelectorAll(CODE_BLOCK_SEL)).forEach((code) => {
|
||||
// Array.from(code.querySelectorAll('span[class^="c"]'))
|
||||
// .filter((c1) => c1.innerText.includes('!!'))
|
||||
// .forEach((c1) => {
|
||||
// const [, n] = c1.innerText.match(/!!\s*(\d+)/) || [, '1'];
|
||||
// const hl = createElement('span', { class: '__hl', style: `height: ${Number(n) * CODE_LINE_HEIGHT}em` });
|
||||
// c1.innerText = c1.innerText.replace(`!!${n}`, '!!');
|
||||
// const hasContent = c1.innerText?.match(/[\p{L}|\d]/u);
|
||||
// if (!hasContent) {
|
||||
// c1.parentElement?.replaceChild(hl, c1);
|
||||
// } else {
|
||||
// c1.innerText = c1.innerText.replace('!!', '');
|
||||
// c1.parentElement?.insertBefore(hl, c1);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
const katexHref = document.getElementById("_katexPreload")?.href;
|
||||
if (!katexPromise && katexHref) {
|
||||
intersectOnce(main.querySelectorAll(".katex"), {
|
||||
rootMargin: "1440px",
|
||||
}).then(() => {
|
||||
katexPromise = loadCSS(katexHref);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const mathJaxEl = document.getElementById("_MathJax");
|
||||
if (pushStateEl && mathJaxEl) {
|
||||
const mathJax2To3 = ({
|
||||
detail: {
|
||||
replaceEls: [mainEl],
|
||||
},
|
||||
}) => {
|
||||
mainEl
|
||||
.querySelectorAll('script[type="math/tex; mode=display"]')
|
||||
.forEach((el) => {
|
||||
el.outerHTML = el.innerText
|
||||
.replace("% <![CDATA[", "\\[")
|
||||
.replace("%]]>", "\\]");
|
||||
});
|
||||
mainEl.querySelectorAll('script[type="math/tex"]').forEach((el) => {
|
||||
el.outerHTML = `\\(${el.innerText}\\)`;
|
||||
});
|
||||
};
|
||||
|
||||
mathJax2To3({ detail: { replaceEls: [document] } });
|
||||
|
||||
if (!("MathJax" in window)) await once(mathJaxEl, "load");
|
||||
|
||||
await MathJax.typesetPromise();
|
||||
|
||||
if (!window._noPushState) {
|
||||
pushStateEl.addEventListener("ready", mathJax2To3);
|
||||
fromEvent(pushStateEl, "after")
|
||||
.pipe(concatMap(() => MathJax.typesetPromise()))
|
||||
.subscribe();
|
||||
}
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user