123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
- let debounceTimeout;
- let timesDebounced = 0;
- return function () {
- if (timesDebounced == maxDebounceTimes) {
- clearTimeout(debounceTimeout);
- timesDebounced = 0;
- callback();
- return;
- }
- clearTimeout(debounceTimeout);
- timesDebounced++;
- debounceTimeout = setTimeout(() => {
- timesDebounced = 0;
- callback();
- }, debounceDelay);
- };
- };
- async function fetchPageContent(pageSlug) {
- // TODO: handle non 200 status codes/time outs
- // TODO: add retries
- const response = await fetch(`/api/pages/${pageSlug}/content/`);
- const content = await response.text();
- return content;
- }
- function setupCarousels() {
- const carouselElements = document.getElementsByClassName("carousel-container");
- if (carouselElements.length == 0) {
- return;
- }
- for (let i = 0; i < carouselElements.length; i++) {
- const carousel = carouselElements[i];
- carousel.classList.add("show-right-cutoff");
- const itemsContainer = carousel.getElementsByClassName("carousel-items-container")[0];
- const determineSideCutoffs = () => {
- if (itemsContainer.scrollLeft != 0) {
- carousel.classList.add("show-left-cutoff");
- } else {
- carousel.classList.remove("show-left-cutoff");
- }
- if (Math.ceil(itemsContainer.scrollLeft) + itemsContainer.clientWidth < itemsContainer.scrollWidth) {
- carousel.classList.add("show-right-cutoff");
- } else {
- carousel.classList.remove("show-right-cutoff");
- }
- }
- const determineSideCutoffsRateLimited = throttledDebounce(determineSideCutoffs, 20, 100);
- itemsContainer.addEventListener("scroll", determineSideCutoffsRateLimited);
- window.addEventListener("resize", determineSideCutoffsRateLimited);
- afterContentReady(determineSideCutoffs);
- }
- }
- const minuteInSeconds = 60;
- const hourInSeconds = minuteInSeconds * 60;
- const dayInSeconds = hourInSeconds * 24;
- const monthInSeconds = dayInSeconds * 30;
- const yearInSeconds = monthInSeconds * 12;
- function relativeTimeSince(timestamp) {
- const delta = Math.round((Date.now() / 1000) - timestamp);
- if (delta < minuteInSeconds) {
- return "1m";
- }
- if (delta < hourInSeconds) {
- return Math.floor(delta / minuteInSeconds) + "m";
- }
- if (delta < dayInSeconds) {
- return Math.floor(delta / hourInSeconds) + "h";
- }
- if (delta < monthInSeconds) {
- return Math.floor(delta / dayInSeconds) + "d";
- }
- if (delta < yearInSeconds) {
- return Math.floor(delta / monthInSeconds) + "mo";
- }
- return Math.floor(delta / yearInSeconds) + "y";
- }
- function updateRelativeTimeForElements(elements)
- {
- for (let i = 0; i < elements.length; i++)
- {
- const element = elements[i];
- const timestamp = element.dataset.dynamicRelativeTime;
- if (timestamp === undefined)
- continue
- element.textContent = relativeTimeSince(timestamp);
- }
- }
- function setupDynamicRelativeTime() {
- const elements = document.querySelectorAll("[data-dynamic-relative-time]");
- const updateInterval = 60 * 1000;
- let lastUpdateTime = Date.now();
- updateRelativeTimeForElements(elements);
- const updateElementsAndTimestamp = () => {
- updateRelativeTimeForElements(elements);
- lastUpdateTime = Date.now();
- };
- const scheduleRepeatingUpdate = () => setInterval(updateElementsAndTimestamp, updateInterval);
- if (document.hidden === undefined) {
- scheduleRepeatingUpdate();
- return;
- }
- let timeout = scheduleRepeatingUpdate();
- document.addEventListener("visibilitychange", () => {
- if (document.hidden) {
- clearTimeout(timeout);
- return;
- }
- const delta = Date.now() - lastUpdateTime;
- if (delta >= updateInterval) {
- updateElementsAndTimestamp();
- timeout = scheduleRepeatingUpdate();
- return;
- }
- timeout = setTimeout(() => {
- updateElementsAndTimestamp();
- timeout = scheduleRepeatingUpdate();
- }, updateInterval - delta);
- });
- }
- function setupLazyImages() {
- const images = document.querySelectorAll("img[loading=lazy]");
- if (images.length == 0) {
- return;
- }
- function imageFinishedTransition(image) {
- image.classList.add("finished-transition");
- }
- afterContentReady(() => {
- setTimeout(() => {
- for (let i = 0; i < images.length; i++) {
- const image = images[i];
- if (image.complete) {
- image.classList.add("cached");
- setTimeout(() => imageFinishedTransition(image), 1);
- } else {
- // TODO: also handle error event
- image.addEventListener("load", () => {
- image.classList.add("loaded");
- setTimeout(() => imageFinishedTransition(image), 400);
- });
- }
- }
- }, 1);
- });
- }
- function attachExpandToggleButton(collapsibleContainer) {
- const showMoreText = "Show more";
- const showLessText = "Show less";
- let expanded = false;
- const button = document.createElement("button");
- const icon = document.createElement("span");
- icon.classList.add("expand-toggle-button-icon");
- const textNode = document.createTextNode(showMoreText);
- button.classList.add("expand-toggle-button");
- button.append(textNode, icon);
- button.addEventListener("click", () => {
- expanded = !expanded;
- if (expanded) {
- collapsibleContainer.classList.add("container-expanded");
- button.classList.add("container-expanded");
- textNode.nodeValue = showLessText;
- return;
- }
- const topBefore = button.getClientRects()[0].top;
- collapsibleContainer.classList.remove("container-expanded");
- button.classList.remove("container-expanded");
- textNode.nodeValue = showMoreText;
- const topAfter = button.getClientRects()[0].top;
- if (topAfter > 0)
- return;
- window.scrollBy({
- top: topAfter - topBefore,
- behavior: "instant"
- });
- });
- collapsibleContainer.after(button);
- return button;
- };
- function setupCollapsibleLists() {
- const collapsibleLists = document.querySelectorAll(".list.collapsible-container");
- if (collapsibleLists.length == 0) {
- return;
- }
- for (let i = 0; i < collapsibleLists.length; i++) {
- const list = collapsibleLists[i];
- if (list.dataset.collapseAfter === undefined) {
- continue;
- }
- const collapseAfter = parseInt(list.dataset.collapseAfter);
- if (collapseAfter == -1) {
- continue;
- }
- if (list.children.length <= collapseAfter) {
- continue;
- }
- attachExpandToggleButton(list);
- for (let c = collapseAfter; c < list.children.length; c++) {
- const child = list.children[c];
- child.classList.add("collapsible-item");
- child.style.animationDelay = ((c - collapseAfter) * 20).toString() + "ms";
- }
- }
- }
- function setupCollapsibleGrids() {
- const collapsibleGridElements = document.querySelectorAll(".cards-grid.collapsible-container");
- if (collapsibleGridElements.length == 0) {
- return;
- }
- for (let i = 0; i < collapsibleGridElements.length; i++) {
- const gridElement = collapsibleGridElements[i];
- if (gridElement.dataset.collapseAfterRows === undefined) {
- continue;
- }
- const collapseAfterRows = parseInt(gridElement.dataset.collapseAfterRows);
- if (collapseAfterRows == -1) {
- continue;
- }
- const getCardsPerRow = () => {
- return parseInt(getComputedStyle(gridElement).getPropertyValue('--cards-per-row'));
- };
- const button = attachExpandToggleButton(gridElement);
- let cardsPerRow = 2;
- const resolveCollapsibleItems = () => {
- const hideItemsAfterIndex = cardsPerRow * collapseAfterRows;
- if (hideItemsAfterIndex >= gridElement.children.length) {
- button.style.display = "none";
- } else {
- button.style.removeProperty("display");
- }
- let row = 0;
- for (let i = 0; i < gridElement.children.length; i++) {
- const child = gridElement.children[i];
- if (i >= hideItemsAfterIndex) {
- child.classList.add("collapsible-item");
- child.style.animationDelay = (row * 40).toString() + "ms";
- if (i % cardsPerRow + 1 == cardsPerRow) {
- row++;
- }
- } else {
- child.classList.remove("collapsible-item");
- child.style.removeProperty("animation-delay");
- }
- }
- };
- afterContentReady(() => {
- cardsPerRow = getCardsPerRow();
- resolveCollapsibleItems();
- });
- window.addEventListener("resize", () => {
- const newCardsPerRow = getCardsPerRow();
- if (cardsPerRow == newCardsPerRow) {
- return;
- }
- cardsPerRow = newCardsPerRow;
- resolveCollapsibleItems();
- });
- }
- }
- const contentReadyCallbacks = [];
- function afterContentReady(callback) {
- contentReadyCallbacks.push(callback);
- }
- const weekDayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
- const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
- function makeSettableTimeElement(element, hourFormat) {
- const fragment = document.createDocumentFragment();
- const hour = document.createElement('span');
- const minute = document.createElement('span');
- const amPm = document.createElement('span');
- fragment.append(hour, document.createTextNode(':'), minute);
- if (hourFormat == '12h') {
- fragment.append(document.createTextNode(' '), amPm);
- }
- element.append(fragment);
- return (date) => {
- const hours = date.getHours();
- if (hourFormat == '12h') {
- amPm.textContent = hours < 12 ? 'AM' : 'PM';
- hour.textContent = hours % 12 || 12;
- } else {
- hour.textContent = hours < 10 ? '0' + hours : hours;
- }
- const minutes = date.getMinutes();
- minute.textContent = minutes < 10 ? '0' + minutes : minutes;
- };
- };
- function timeInZone(now, zone) {
- let timeInZone;
- try {
- timeInZone = new Date(now.toLocaleString('en-US', { timeZone: zone }));
- } catch (e) {
- // TODO: indicate to the user that this is an invalid timezone
- console.error(e);
- timeInZone = now
- }
- const diffInHours = Math.round((timeInZone.getTime() - now.getTime()) / 1000 / 60 / 60);
- return { time: timeInZone, diffInHours: diffInHours };
- }
- function setupClocks() {
- const clocks = document.getElementsByClassName('clock');
- if (clocks.length == 0) {
- return;
- }
- const updateCallbacks = [];
- for (var i = 0; i < clocks.length; i++) {
- const clock = clocks[i];
- const hourFormat = clock.dataset.hourFormat;
- const localTimeContainer = clock.querySelector('[data-local-time]');
- const localDateElement = localTimeContainer.querySelector('[data-date]');
- const localWeekdayElement = localTimeContainer.querySelector('[data-weekday]');
- const localYearElement = localTimeContainer.querySelector('[data-year]');
- const timeZoneContainers = clock.querySelectorAll('[data-time-in-zone]');
- const setLocalTime = makeSettableTimeElement(
- localTimeContainer.querySelector('[data-time]'),
- hourFormat
- );
- updateCallbacks.push((now) => {
- setLocalTime(now);
- localDateElement.textContent = now.getDate() + ' ' + monthNames[now.getMonth()];
- localWeekdayElement.textContent = weekDayNames[now.getDay()];
- localYearElement.textContent = now.getFullYear();
- });
- for (var z = 0; z < timeZoneContainers.length; z++) {
- const timeZoneContainer = timeZoneContainers[z];
- const diffElement = timeZoneContainer.querySelector('[data-time-diff]');
- const setZoneTime = makeSettableTimeElement(
- timeZoneContainer.querySelector('[data-time]'),
- hourFormat
- );
- updateCallbacks.push((now) => {
- const { time, diffInHours } = timeInZone(now, timeZoneContainer.dataset.timeInZone);
- setZoneTime(time);
- diffElement.textContent = (diffInHours <= 0 ? diffInHours : '+' + diffInHours) + 'h';
- });
- }
- }
- const updateClocks = () => {
- const now = new Date();
- for (var i = 0; i < updateCallbacks.length; i++)
- updateCallbacks[i](now);
- setTimeout(updateClocks, (60 - now.getSeconds()) * 1000);
- };
- updateClocks();
- }
- async function setupPage() {
- const pageElement = document.getElementById("page");
- const pageContentElement = document.getElementById("page-content");
- const pageContent = await fetchPageContent(pageData.slug);
- pageContentElement.innerHTML = pageContent;
- try {
- setupClocks()
- setupCarousels();
- setupCollapsibleLists();
- setupCollapsibleGrids();
- setupDynamicRelativeTime();
- setupLazyImages();
- } finally {
- pageElement.classList.add("content-ready");
- for (let i = 0; i < contentReadyCallbacks.length; i++) {
- contentReadyCallbacks[i]();
- }
- setTimeout(() => {
- document.body.classList.add("page-columns-transitioned");
- }, 300);
- }
- }
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", setupPage);
- } else {
- setupPage();
- }
|