Skip to content

Commit 386de3e

Browse files
committed
Deploy preview for PR 114 🛫
1 parent 8ef43a9 commit 386de3e

File tree

256 files changed

+71451
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

256 files changed

+71451
-0
lines changed

‎preview/pr-114/2019/01/29/infant-in-arms.html

Lines changed: 1196 additions & 0 deletions
Large diffs are not rendered by default.

‎preview/pr-114/2019/04/06/the-harassment-tax.html

Lines changed: 879 additions & 0 deletions
Large diffs are not rendered by default.

‎preview/pr-114/2019/05/01/why-do-we-have-application-fees.html

Lines changed: 897 additions & 0 deletions
Large diffs are not rendered by default.

‎preview/pr-114/2020/10/19/DL-workshop.html

Lines changed: 920 additions & 0 deletions
Large diffs are not rendered by default.

‎preview/pr-114/404.html

Lines changed: 742 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
creates link next to each heading that links to that section.
3+
*/
4+
5+
{
6+
const onLoad = () => {
7+
// for each heading
8+
const headings = document.querySelectorAll(
9+
"h1[id], h2[id], h3[id], h4[id]"
10+
);
11+
for (const heading of headings) {
12+
// create anchor link
13+
const link = document.createElement("a");
14+
link.classList.add("icon", "fa-solid", "fa-link", "anchor");
15+
link.href = "#" + heading.id;
16+
link.setAttribute("aria-label", "link to this section");
17+
heading.append(link);
18+
19+
// if first heading in the section, move id to parent section
20+
if (heading.matches("section > :first-child")) {
21+
heading.parentElement.id = heading.id;
22+
heading.removeAttribute("id");
23+
}
24+
}
25+
};
26+
27+
// scroll to target of url hash
28+
const scrollToTarget = () => {
29+
const id = window.location.hash.replace("#", "");
30+
const target = document.getElementById(id);
31+
32+
if (!target) return;
33+
const offset = document.querySelector("header").clientHeight || 0;
34+
window.scrollTo({
35+
top: target.getBoundingClientRect().top + window.scrollY - offset,
36+
behavior: "smooth",
37+
});
38+
};
39+
40+
// after page loads
41+
window.addEventListener("load", onLoad);
42+
window.addEventListener("load", scrollToTarget);
43+
window.addEventListener("tagsfetched", scrollToTarget);
44+
45+
// when hash nav happens
46+
window.addEventListener("hashchange", scrollToTarget);
47+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
manages light/dark mode.
3+
*/
4+
5+
{
6+
// save/load user's dark mode preference from local storage
7+
const loadDark = () => window.localStorage.getItem("dark-mode") === "true";
8+
const saveDark = (value) => window.localStorage.setItem("dark-mode", value);
9+
10+
// immediately load saved mode before page renders
11+
document.documentElement.dataset.dark = loadDark();
12+
13+
const onLoad = () => {
14+
// update toggle button to match loaded mode
15+
document.querySelector(".dark-toggle").checked =
16+
document.documentElement.dataset.dark === "true";
17+
};
18+
19+
// after page loads
20+
window.addEventListener("load", onLoad);
21+
22+
// when user toggles mode button
23+
window.onDarkToggleChange = (event) => {
24+
const value = event.target.checked;
25+
document.documentElement.dataset.dark = value;
26+
saveDark(value);
27+
};
28+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
fetches tags (aka "topics") from a given GitHub repo and adds them to row of
3+
tag buttons. specify repo in data-repo attribute on row.
4+
*/
5+
6+
{
7+
const onLoad = async () => {
8+
// get tag rows with specified repos
9+
const rows = document.querySelectorAll("[data-repo]");
10+
11+
// for each repo
12+
for (const row of rows) {
13+
// get props from tag row
14+
const repo = row.dataset.repo.trim();
15+
const link = row.dataset.link.trim();
16+
17+
// get tags from github
18+
if (!repo) continue;
19+
let tags = await fetchTags(repo);
20+
21+
// filter out tags already present in row
22+
let existing = [...row.querySelectorAll(".tag")].map((tag) =>
23+
window.normalizeTag(tag.innerText)
24+
);
25+
tags = tags.filter((tag) => !existing.includes(normalizeTag(tag)));
26+
27+
// add tags to row
28+
for (const tag of tags) {
29+
const a = document.createElement("a");
30+
a.classList.add("tag");
31+
a.innerHTML = tag;
32+
a.href = `${link}?search="tag: ${tag}"`;
33+
a.dataset.tooltip = `Show items with the tag "${tag}"`;
34+
row.append(a);
35+
}
36+
37+
// delete tags container if empty
38+
if (!row.innerText.trim()) row.remove();
39+
}
40+
41+
// emit "tags done" event for other scripts to listen for
42+
window.dispatchEvent(new Event("tagsfetched"));
43+
};
44+
45+
// after page loads
46+
window.addEventListener("load", onLoad);
47+
48+
// GitHub topics endpoint
49+
const api = "https://api.github.com/repos/REPO/topics";
50+
const headers = new Headers();
51+
headers.set("Accept", "application/vnd.github+json");
52+
53+
// get tags from GitHub based on repo name
54+
const fetchTags = async (repo) => {
55+
const url = api.replace("REPO", repo);
56+
try {
57+
const response = await (await fetch(url)).json();
58+
if (response.names) return response.names;
59+
else throw new Error(JSON.stringify(response));
60+
} catch (error) {
61+
console.groupCollapsed("GitHub fetch tags error");
62+
console.log(error);
63+
console.groupEnd();
64+
return [];
65+
}
66+
};
67+
}

‎preview/pr-114/_scripts/search.js

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
filters elements on page based on url or search box.
3+
syntax: term1 term2 "full phrase 1" "full phrase 2" "tag: tag 1"
4+
match if: all terms AND at least one phrase AND at least one tag
5+
*/
6+
{
7+
// elements to filter
8+
const elementSelector = ".card, .citation, .post-excerpt";
9+
// search box element
10+
const searchBoxSelector = ".search-box";
11+
// results info box element
12+
const infoBoxSelector = ".search-info";
13+
// tags element
14+
const tagSelector = ".tag";
15+
16+
// split search query into terms, phrases, and tags
17+
const splitQuery = (query) => {
18+
// split into parts, preserve quotes
19+
const parts = query.match(/"[^"]*"|\S+/g) || [];
20+
21+
// bins
22+
const terms = [];
23+
const phrases = [];
24+
const tags = [];
25+
26+
// put parts into bins
27+
for (let part of parts) {
28+
if (part.startsWith('"')) {
29+
part = part.replaceAll('"', "").trim();
30+
if (part.startsWith("tag:"))
31+
tags.push(normalizeTag(part.replace(/tag:\s*/, "")));
32+
else phrases.push(part.toLowerCase());
33+
} else terms.push(part.toLowerCase());
34+
}
35+
36+
return { terms, phrases, tags };
37+
};
38+
39+
// normalize tag string for comparison
40+
window.normalizeTag = (tag) =>
41+
tag.trim().toLowerCase().replaceAll(/-|\s+/g, " ");
42+
43+
// get data attribute contents of element and children
44+
const getAttr = (element, attr) =>
45+
[element, ...element.querySelectorAll(`[data-${attr}]`)]
46+
.map((element) => element.dataset[attr])
47+
.join(" ");
48+
49+
// determine if element should show up in results based on query
50+
const elementMatches = (element, { terms, phrases, tags }) => {
51+
// tag elements within element
52+
const tagElements = [...element.querySelectorAll(".tag")];
53+
54+
// check if text content exists in element
55+
const hasText = (string) =>
56+
(
57+
element.innerText +
58+
getAttr(element, "tooltip") +
59+
getAttr(element, "search")
60+
)
61+
.toLowerCase()
62+
.includes(string);
63+
// check if text matches a tag in element
64+
const hasTag = (string) =>
65+
tagElements.some((tag) => normalizeTag(tag.innerText) === string);
66+
67+
// match logic
68+
return (
69+
(terms.every(hasText) || !terms.length) &&
70+
(phrases.some(hasText) || !phrases.length) &&
71+
(tags.some(hasTag) || !tags.length)
72+
);
73+
};
74+
75+
// loop through elements, hide/show based on query, and return results info
76+
const filterElements = (parts) => {
77+
let elements = document.querySelectorAll(elementSelector);
78+
79+
// results info
80+
let x = 0;
81+
let n = elements.length;
82+
let tags = parts.tags;
83+
84+
// filter elements
85+
for (const element of elements) {
86+
if (elementMatches(element, parts)) {
87+
element.style.display = "";
88+
x++;
89+
} else element.style.display = "none";
90+
}
91+
92+
return [x, n, tags];
93+
};
94+
95+
// highlight search terms
96+
const highlightMatches = async ({ terms, phrases }) => {
97+
// make sure Mark library available
98+
if (typeof Mark === "undefined") return;
99+
100+
// reset
101+
new Mark(document.body).unmark();
102+
103+
// limit number of highlights to avoid slowdown
104+
let counter = 0;
105+
const filter = () => counter++ < 100;
106+
107+
// highlight terms and phrases
108+
new Mark(elementSelector)
109+
.mark(terms, { separateWordSearch: true, filter })
110+
.mark(phrases, { separateWordSearch: false, filter });
111+
};
112+
113+
// update search box based on query
114+
const updateSearchBox = (query = "") => {
115+
const boxes = document.querySelectorAll(searchBoxSelector);
116+
117+
for (const box of boxes) {
118+
const input = box.querySelector("input");
119+
const button = box.querySelector("button");
120+
const icon = box.querySelector("button i");
121+
input.value = query;
122+
icon.className = input.value.length
123+
? "icon fa-solid fa-xmark"
124+
: "icon fa-solid fa-magnifying-glass";
125+
button.disabled = input.value.length ? false : true;
126+
}
127+
};
128+
129+
// update info box based on query and results
130+
const updateInfoBox = (query, x, n) => {
131+
const boxes = document.querySelectorAll(infoBoxSelector);
132+
133+
if (query.trim()) {
134+
// show all info boxes
135+
boxes.forEach((info) => (info.style.display = ""));
136+
137+
// info template
138+
let info = "";
139+
info += `Showing ${x.toLocaleString()} of ${n.toLocaleString()} results<br>`;
140+
info += "<a href='./'>Clear search</a>";
141+
142+
// set info HTML string
143+
boxes.forEach((el) => (el.innerHTML = info));
144+
}
145+
// if nothing searched
146+
else {
147+
// hide all info boxes
148+
boxes.forEach((info) => (info.style.display = "none"));
149+
}
150+
};
151+
152+
// update tags based on query
153+
const updateTags = (query) => {
154+
const { tags } = splitQuery(query);
155+
document.querySelectorAll(tagSelector).forEach((tag) => {
156+
// set active if tag is in query
157+
if (tags.includes(normalizeTag(tag.innerText)))
158+
tag.setAttribute("data-active", "");
159+
else tag.removeAttribute("data-active");
160+
});
161+
};
162+
163+
// run search with query
164+
const runSearch = (query = "") => {
165+
const parts = splitQuery(query);
166+
const [x, n] = filterElements(parts);
167+
updateSearchBox(query);
168+
updateInfoBox(query, x, n);
169+
updateTags(query);
170+
highlightMatches(parts);
171+
};
172+
173+
// update url based on query
174+
const updateUrl = (query = "") => {
175+
const url = new URL(window.location);
176+
let params = new URLSearchParams(url.search);
177+
params.set("search", query);
178+
url.search = params.toString();
179+
window.history.replaceState(null, null, url);
180+
};
181+
182+
// search based on url param
183+
const searchFromUrl = () => {
184+
const query =
185+
new URLSearchParams(window.location.search).get("search") || "";
186+
runSearch(query);
187+
};
188+
189+
// return func that runs after delay
190+
const debounce = (callback, delay = 250) => {
191+
let timeout;
192+
return (...args) => {
193+
window.clearTimeout(timeout);
194+
timeout = window.setTimeout(() => callback(...args), delay);
195+
};
196+
};
197+
198+
// when user types into search box
199+
const debouncedRunSearch = debounce(runSearch, 1000);
200+
window.onSearchInput = (target) => {
201+
debouncedRunSearch(target.value);
202+
updateUrl(target.value);
203+
};
204+
205+
// when user clears search box with button
206+
window.onSearchClear = () => {
207+
runSearch();
208+
updateUrl();
209+
};
210+
211+
// after page loads
212+
window.addEventListener("load", searchFromUrl);
213+
// after tags load
214+
window.addEventListener("tagsfetched", searchFromUrl);
215+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
for site search component. searches site/domain via google.
3+
*/
4+
5+
{
6+
// when user submits site search form/box
7+
window.onSiteSearchSubmit = (event) => {
8+
event.preventDefault();
9+
const google = "https://www.google.com/search?q=site:";
10+
const site = window.location.origin;
11+
const query = event.target.elements.query.value;
12+
window.location = google + site + " " + query;
13+
};
14+
}

0 commit comments

Comments
 (0)