Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 197 additions & 31 deletions site/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
const REGISTERED_COMPOUNDS = "registered-compounds";
const WORD_FILTERS = ["f-pos", "f-domain", "f-module", "f-pillar"];
const HONORIFIC_GLOSSES = new Set(["HON.RESPECT", "HON.INTIM", "HON.ROLE"]);
let lexicon = [], compounds = [], words = new Set(), glosses = new Map(), shown = 0, current = [];
let lexicon = [], compounds = [], words = new Set(), glosses = new Map();
let currentResults = [], currentPage = 1, emptyStatus = "";
let resultCounts = { words: 0, compounds: 0, compoundOnly: false };

const PILLAR_NAMES = {
"solarpunk-values": "solarpunk",
Expand Down Expand Up @@ -75,8 +77,6 @@
fill($("f-domain"), opts.domain);
fill($("f-module"), opts.module, (v) => MODULE_NAMES[v] || v);
fill($("f-pillar"), opts.pillar, (v) => PILLAR_NAMES[v] || v);
const requestedModule = new URLSearchParams(window.location.search).get("module");
if (requestedModule === BASE_VOCABULARY || opts.module.has(requestedModule)) $("f-module").value = requestedModule;

const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));

Expand Down Expand Up @@ -155,13 +155,13 @@
: esc(t);
}).join(" ");

function search() {
function search({ page = 1, historyMode = "replace", moveFocus = false } = {}) {
const q = $("q").value.trim().normalize("NFC").toLowerCase();
const field = $("f-field").value;
const compoundOnly = field === REGISTERED_COMPOUNDS;
const fp = $("f-pos").value, fd = $("f-domain").value;
const fm = $("f-module").value, fl = $("f-pillar").value;
current = lexicon
const wordMatches = lexicon
.map((e) => ({ e, s: q ? score(e, q, field) : (compoundOnly ? -1 : 9) }))
.filter(({ e, s }) =>
s >= 0 &&
Expand All @@ -181,31 +181,145 @@
.sort((a, b) => a.s - b.s || a.c.compound.localeCompare(b.c.compound))
.map(({ c }) => c)
: [];
const wordCount = `${current.length} ${current.length === 1 ? "word" : "words"}`;
if (compoundOnly) {
$("status").textContent = compMatches.length === 0
? "nothing found: the compound registry may simply not have it yet"
: `${compMatches.length} registered ${compMatches.length === 1 ? "compound" : "compounds"}`;
} else {

currentResults = [
...compMatches.map((value) => ({ kind: "compound", value })),
...wordMatches.map((value) => ({ kind: "word", value })),
];
resultCounts = {
words: wordMatches.length,
compounds: compMatches.length,
compoundOnly,
};
emptyStatus = compoundOnly
? "nothing found: the compound registry may simply not have it yet"
: "nothing found: the lexicon may simply not have it yet";
currentPage = clampPage(page);
renderPage();
syncUrl(historyMode);
if (moveFocus) moveToResults();
}

function totalPages() {
return Math.ceil(currentResults.length / PAGE);
}

function clampPage(page) {
const last = Math.max(1, totalPages());
const requested = Number.parseInt(page, 10);
return Number.isFinite(requested) ? Math.min(Math.max(requested, 1), last) : 1;
}

function renderPage() {
const start = (currentPage - 1) * PAGE;
const slice = currentResults.slice(start, start + PAGE);
const frag = document.createDocumentFragment();
for (const result of slice)
frag.appendChild(result.kind === "compound" ? compRow(result.value) : row(result.value));
$("results").replaceChildren(frag);
renderStatus(start, slice.length);
renderPagination($("pages-top"));
renderPagination($("pages-bottom"));
}

function renderStatus(start, count) {
if (!currentResults.length) {
$("status").textContent = emptyStatus;
return;
}
const first = start + 1;
const last = start + count;
const firstLabel = first.toLocaleString("en-US");
const lastLabel = last.toLocaleString("en-US");
const range = first === last ? firstLabel : `${firstLabel}-${lastLabel}`;
const total = currentResults.length.toLocaleString("en-US");
if (resultCounts.compoundOnly) {
$("status").textContent =
current.length === lexicon.length ? "" :
current.length === 0 && compMatches.length === 0 ? "nothing found: the lexicon may simply not have it yet" :
compMatches.length === 0 ? wordCount :
`${wordCount}, ${compMatches.length} ${compMatches.length === 1 ? "compound" : "compounds"}`;
`showing ${range} of ${total} registered ${resultCounts.compounds === 1 ? "compound" : "compounds"}`;
return;
}
$("results").innerHTML = "";
shown = 0;
for (const c of compMatches) $("results").appendChild(compRow(c));
renderMore();
if (!resultCounts.compounds) {
$("status").textContent =
`showing ${range} of ${total} ${resultCounts.words === 1 ? "word" : "words"}`;
return;
}
const wordsLabel = `${resultCounts.words.toLocaleString("en-US")} ${resultCounts.words === 1 ? "word" : "words"}`;
const compoundsLabel =
`${resultCounts.compounds.toLocaleString("en-US")} ${resultCounts.compounds === 1 ? "compound" : "compounds"}`;
$("status").textContent =
`showing ${range} of ${total} results: ${wordsLabel}, ${compoundsLabel}`;
}

function renderMore() {
const slice = current.slice(shown, shown + PAGE);
shown += slice.length;
const frag = document.createDocumentFragment();
for (const e of slice) frag.appendChild(row(e));
$("results").appendChild(frag);
$("more").hidden = shown >= current.length;
function pageWindow(last) {
const compact = window.matchMedia("(max-width: 560px)").matches;
if (last <= (compact ? 4 : 7)) return Array.from({ length: last }, (_, i) => i + 1);
const pages = new Set([1, last, currentPage]);
const radius = compact ? 0 : 1;
for (let page = currentPage - radius; page <= currentPage + radius; page++)
if (page > 1 && page < last) pages.add(page);
const edge = compact ? 2 : 4;
if (currentPage <= edge)
for (let page = 2; page <= edge; page++) pages.add(page);
if (currentPage >= last - edge + 1)
for (let page = last - edge + 1; page < last; page++) pages.add(page);
return [...pages].sort((a, b) => a - b);
}

function pageButton(text, label, page, disabled = false) {
const button = document.createElement("button");
button.type = "button";
button.className = "page-button";
button.textContent = text;
button.setAttribute("aria-label", label);
button.title = label;
button.disabled = disabled;
if (!disabled) button.addEventListener("click", () => goToPage(page));
return button;
}

function renderPagination(nav) {
const last = totalPages();
nav.replaceChildren();
nav.hidden = last <= 1;
if (last <= 1) return;

nav.appendChild(pageButton("←", "previous page", currentPage - 1, currentPage === 1));
let previous = 0;
for (const page of pageWindow(last)) {
if (previous && page - previous > 1) {
const ellipsis = document.createElement("span");
ellipsis.className = "page-ellipsis";
ellipsis.textContent = "…";
ellipsis.setAttribute("aria-hidden", "true");
nav.appendChild(ellipsis);
}
if (page === currentPage) {
const current = document.createElement("span");
current.className = "page-current";
current.textContent = page;
current.setAttribute("aria-current", "page");
current.setAttribute("aria-label", `page ${page}, current page`);
nav.appendChild(current);
} else {
nav.appendChild(pageButton(`${page}`, `page ${page}`, page));
}
previous = page;
}
nav.appendChild(pageButton("→", "next page", currentPage + 1, currentPage === last));
}

function goToPage(page) {
const next = clampPage(page);
if (next === currentPage) return;
currentPage = next;
renderPage();
syncUrl("push");
moveToResults();
}

function moveToResults() {
$("results").focus({ preventScroll: true });
$("results").scrollIntoView({ behavior: "smooth", block: "start" });
}

function row(e) {
Expand Down Expand Up @@ -334,15 +448,56 @@
}
}

function selectHasValue(id, value) {
return [...$(id).options].some((option) => option.value === value);
}

function applyUrlState() {
const params = new URLSearchParams(window.location.search);
$("q").value = params.get("q") || "";
const selections = [
["f-field", "field", "all"],
["f-pos", "pos", ""],
["f-domain", "domain", ""],
["f-module", "module", ""],
["f-pillar", "pillar", ""],
];
for (const [id, key, fallback] of selections) {
const requested = params.get(key);
$(id).value = requested && selectHasValue(id, requested) ? requested : fallback;
}
updateWordFilters();
const page = Number.parseInt(params.get("page"), 10);
return Number.isFinite(page) && page > 0 ? page : 1;
}

function syncUrl(mode) {
if (!mode) return;
const params = new URLSearchParams();
const q = $("q").value.trim();
if (q) params.set("q", q);
if ($("f-field").value !== "all") params.set("field", $("f-field").value);
if ($("f-pos").value) params.set("pos", $("f-pos").value);
if ($("f-domain").value) params.set("domain", $("f-domain").value);
if ($("f-module").value) params.set("module", $("f-module").value);
if ($("f-pillar").value) params.set("pillar", $("f-pillar").value);
if (currentPage > 1) params.set("page", currentPage);

const query = params.toString();
const next = `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`;
const here = `${window.location.pathname}${window.location.search}${window.location.hash}`;
if (next === here) return;
window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", next);
}

// cross-reference clicks
document.addEventListener("click", (ev) => {
const w = ev.target?.dataset?.w;
if (w) {
$("q").value = w;
$("f-field").value = "word";
updateWordFilters();
search();
window.scrollTo({ top: 0, behavior: "smooth" });
search({ historyMode: "push", moveFocus: true });
}
});

Expand All @@ -358,11 +513,22 @@
updateWordFilters();
search();
});
$("more").addEventListener("click", renderMore);
window.addEventListener("popstate", () => {
clearTimeout(t);
const page = applyUrlState();
search({ page, historyMode: null });
});
const compactPages = window.matchMedia("(max-width: 560px)");
const refreshPagination = () => {
renderPagination($("pages-top"));
renderPagination($("pages-bottom"));
};
if (compactPages.addEventListener) compactPages.addEventListener("change", refreshPagination);
else compactPages.addListener(refreshPagination);
document.addEventListener("keydown", (ev) => {
if (ev.key === "/" && document.activeElement !== $("q")) { ev.preventDefault(); $("q").focus(); }
});

updateWordFilters();
search();
const initialPage = applyUrlState();
search({ page: initialPage, historyMode: "replace" });
})();
15 changes: 4 additions & 11 deletions site/explore.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,10 @@ <h1 class="mark"><a href="index.html" style="color:inherit;text-decoration:none"
<button id="clear" type="button" title="clear search and filters">clear</button>
</div>

<div class="vine" aria-hidden="true">
<svg viewBox="0 0 400 14" preserveAspectRatio="xMidYMid meet">
<path d="M2 7 C 60 7, 90 2, 130 7 S 190 13, 200 7 S 260 1, 270 7 S 340 12, 398 7"
fill="none" stroke="currentColor" stroke-width="1"/>
<circle cx="200" cy="7" r="2.2" fill="currentColor"/>
</svg>
</div>

<p id="status" class="status"></p>
<ol id="results" class="results"></ol>
<button id="more" class="more" type="button" hidden>show more</button>
<p id="status" class="status" aria-live="polite" aria-atomic="true"></p>
<nav id="pages-top" class="pagination pagination-top" aria-label="pages above the lexicon results" aria-describedby="status" hidden></nav>
<ol id="results" class="results" tabindex="-1"></ol>
<nav id="pages-bottom" class="pagination pagination-bottom" aria-label="pages below the lexicon results" aria-describedby="status" hidden></nav>
</main>

<footer>
Expand Down
53 changes: 43 additions & 10 deletions site/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,7 @@ main { max-width: 44rem; margin: 0 auto; padding: 1.75rem 1.25rem 4rem; }
.filters select:disabled { color: var(--muted); cursor: not-allowed; opacity: .5; }
.filters button { color: var(--muted); cursor: pointer; margin-left: auto; }

.vine { color: var(--sage); margin: 1.6rem auto 1rem; max-width: 26rem; opacity: .8; }
.vine svg { display: block; width: 100%; height: 14px; }

.status { color: var(--muted); font-size: .85rem; text-align: center; min-height: 1.2em; margin: 0 0 .8rem; }
.status { color: var(--muted); font-size: .85rem; text-align: center; min-height: 1.2em; margin: 1.4rem 0 .8rem; }

.results { list-style: none; margin: 0; padding: 0; }
.results > li { border-bottom: 1px solid var(--hairline); }
Expand Down Expand Up @@ -203,19 +200,49 @@ main { max-width: 44rem; margin: 0 auto; padding: 1.75rem 1.25rem 4rem; }
margin: 0 .3rem .3rem 0;
}

.more {
display: block;
margin: 1.5rem auto 0;
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: .2rem;
min-height: 2.2rem;
}
.pagination[hidden] { display: none; }
.pagination-top { margin: 0 0 .8rem; }
.pagination-bottom { margin: 1.5rem 0 0; }
.page-button, .page-current {
display: inline-grid;
place-items: center;
width: 2rem;
height: 2rem;
font: inherit;
font-size: .9rem;
line-height: 1;
border-radius: 2px;
}
.page-button {
color: var(--sage);
background: none;
border: 1px solid var(--hairline);
border-radius: 2px;
padding: .4rem 1.4rem;
padding: 0;
cursor: pointer;
}
.more:hover { border-color: var(--sage); }
.page-button:hover { border-color: var(--sage); }
.page-button:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.page-button:disabled { color: var(--muted); cursor: default; opacity: .35; }
.page-button:disabled:hover { border-color: var(--hairline); }
.page-current {
color: var(--ink);
background: var(--paper-raised);
border: 1px solid var(--sage);
font-weight: 600;
}
.page-ellipsis {
width: 1.2rem;
color: var(--muted);
text-align: center;
}
.results:focus { outline: none; }

footer {
border-top: 1px solid var(--hairline);
Expand All @@ -227,6 +254,12 @@ footer {
footer a { color: var(--sage); }

@media (max-width: 560px) {
.search-field { width: 100%; }
.search-field select { min-width: 0; flex: 1; }
.filters > select { max-width: 100%; }
.pagination { gap: .15rem; }
.page-button, .page-current { width: 1.75rem; height: 1.9rem; }
.page-ellipsis { width: .85rem; }
.entry-head { grid-template-columns: 1fr auto; }
.entry-head .g { grid-column: 1 / -1; }
}
Expand Down
Loading