forked from dict/alar.ink
280 lines
8.2 KiB
JavaScript
280 lines
8.2 KiB
JavaScript
(() => {
|
|
const elForm = document.querySelector("form.search-form");
|
|
const elQ = document.querySelector("#q");
|
|
const elSelDict = document.querySelector("form.search-form select");
|
|
const defaultLang = elSelDict.value;
|
|
|
|
|
|
// ===================================
|
|
// Helper functions.
|
|
|
|
function selectDict(dict) {
|
|
// dict is in the format '$fromLang/$toLang'.
|
|
const langs = dict.split("/");
|
|
Object.assign(localStorage, { dict, from_lang: langs[0], to_lang: langs[1] });
|
|
elSelDict.value = dict;
|
|
elForm.setAttribute("action", `${_ROOT_URL}/dictionary/${dict}`);
|
|
}
|
|
|
|
// Capture the form submit and send it as a canonical URL instead
|
|
// of the ?q query param.
|
|
function search(q) {
|
|
let val = q.trim();
|
|
|
|
const uri = elForm.getAttribute("action");
|
|
document.location.href = `${uri}/${encodeURIComponent(val).replace(/%20/g, "-")}`;
|
|
}
|
|
|
|
// ===================================
|
|
// Events
|
|
|
|
// On ~ press, focus search input.
|
|
document.onkeydown = (function (e) {
|
|
if (e.key !== "`" && e.key !== "~") {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
q.focus();
|
|
q.select();
|
|
});
|
|
|
|
// Bind to language change.
|
|
elSelDict.addEventListener("change", function (e) {
|
|
selectDict(e.target.value);
|
|
});
|
|
|
|
// Bind to form submit.
|
|
elForm.addEventListener("submit", function (e) {
|
|
e.preventDefault();
|
|
search(elQ.value);
|
|
});
|
|
|
|
// "More" link next to entries/defs that expands an entry.
|
|
document.querySelectorAll(".more-toggle").forEach((el) => {
|
|
el.onclick = (e) => {
|
|
e.preventDefault();
|
|
|
|
let state = "block";
|
|
if (el.dataset.open) {
|
|
delete (el.dataset.open);
|
|
state = "none";
|
|
} else {
|
|
el.dataset.open = true;
|
|
state = "block";
|
|
}
|
|
|
|
const container = document.getElementById(el.dataset.id);
|
|
container.style.display = state;
|
|
|
|
// fetch() content from the dataset data.guid from /api/entry/{guid}
|
|
// and populate the div.more innerHTML with the content.
|
|
if (state === "block" && !el.dataset.fetched) {
|
|
container.innerHTML = `<div aria-busy="true"></div>`;
|
|
|
|
fetch(`${window._ROOT_URL}/api/dictionary/entries/${el.dataset.entryGuid}`)
|
|
.then((resp) => resp.json())
|
|
.then((data) => {
|
|
const d = data.data;
|
|
container.innerHTML = "";
|
|
|
|
// If there's data.data.meta.synonyms, add them to the synonyms div.
|
|
if (d.meta.synonyms) {
|
|
const syns = document.createElement("div");
|
|
syns.classList.add("synonyms");
|
|
syns.innerHTML = d.meta.synonyms.map((s) => `<a href="${window._ROOT_URL}/dictionary/${el.dataset.fromLang}/${el.dataset.toLang}/${s}">${s}</a>`).join("");
|
|
container.appendChild(syns);
|
|
}
|
|
|
|
// Each data.content[] word, add to target.
|
|
const more = document.createElement("div");
|
|
more.classList.add("words");
|
|
more.innerHTML = d.content.slice(window._MAX_CONTENT_ITEMS).map((c) => `<span class="word">${c}</span>`).join("");
|
|
container.appendChild(more);
|
|
|
|
el.dataset.fetched = true;
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error fetching entry content:", err);
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
// ===================================
|
|
|
|
// Select a language based on the page URL.
|
|
let dict = localStorage.dict || defaultLang;
|
|
const uri = /(dictionary)\/((.+?)\/(.+?))\//i.exec(document.location.href);
|
|
if (uri && uri.length == 5) {
|
|
dict = uri[2];
|
|
}
|
|
|
|
selectDict(dict);
|
|
elQ.focus();
|
|
elQ.select();
|
|
})();
|
|
|
|
// Submission form.
|
|
(() => {
|
|
function filterTypes(e) {
|
|
// Filter the types select field with elements that are supported by the language.
|
|
const types = e.target.closest("[data-relation-controls]").querySelector("select[name=relation_type]");
|
|
types.querySelectorAll("option").forEach((o) => o.style.display = "none");
|
|
types.querySelectorAll(`option[data-lang=${e.target.value}]`).forEach((o) => o.style.display = "block");
|
|
types.selectedIndex = 1;
|
|
}
|
|
|
|
|
|
if (document.querySelector(".form-submit")) {
|
|
document.querySelectorAll("select[name=relation_lang]").forEach((e) => {
|
|
e.onchange = filterTypes;
|
|
});
|
|
|
|
// +definition button.
|
|
document.querySelector(".btn-add-relation").onclick = (e) => {
|
|
e.preventDefault();
|
|
|
|
if (document.querySelectorAll(".add-relations li").length >= 20) {
|
|
return false;
|
|
}
|
|
|
|
// Clone and add a relation fieldset.
|
|
const d = document.querySelector(".add-relations li").cloneNode(true);
|
|
d.dataset.added = true
|
|
d.querySelector("select[name=relation_lang]").onchange = filterTypes;
|
|
document.querySelector(".add-relations").appendChild(d);
|
|
|
|
// Remove definition link.
|
|
d.querySelector(".btn-remove-relation").onclick = (e) => {
|
|
e.preventDefault();
|
|
d.remove();
|
|
};
|
|
};
|
|
}
|
|
})();
|
|
|
|
// Edit form using ot-dropdown.
|
|
(() => {
|
|
const tpl = document.querySelector("#tpl-form-comments");
|
|
if (!tpl) return;
|
|
|
|
let counter = 0;
|
|
document.querySelectorAll("[data-edit-from]").forEach((btn) => {
|
|
const parent = btn.parentNode;
|
|
|
|
// Clone template content and give the popover a unique ID.
|
|
const popoverId = `form-comments-${counter++}`;
|
|
const form = tpl.content.firstElementChild.cloneNode(true);
|
|
form.id = popoverId;
|
|
|
|
// Wire the button as the popover trigger.
|
|
btn.setAttribute("popovertarget", popoverId);
|
|
|
|
// Build <ot-dropdown> with children fully assembled before inserting into DOM
|
|
// so that init() finds [popovertarget] and [popover].
|
|
const dropdown = document.createElement("ot-dropdown");
|
|
dropdown.appendChild(btn);
|
|
dropdown.appendChild(form);
|
|
parent.appendChild(dropdown);
|
|
|
|
const txt = form.querySelector("textarea");
|
|
|
|
// Handle submission.
|
|
form.querySelector("button.submit-comment").onclick = () => {
|
|
fetch(`${window._ROOT_URL}/api/submissions/comments`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
from_guid: btn.dataset.editFrom,
|
|
to_guid: btn.dataset.editTo,
|
|
comments: txt.value
|
|
})
|
|
}).catch((err) => {
|
|
alert(`Error submitting: ${err}`);
|
|
});
|
|
|
|
alert(form.dataset.success);
|
|
form.hidePopover();
|
|
};
|
|
|
|
form.querySelector("button.close").onclick = () => form.hidePopover();
|
|
});
|
|
})();
|
|
|
|
// Autocomplete.
|
|
(() => {
|
|
if (!autocomp) {
|
|
return;
|
|
}
|
|
|
|
const elForm = document.querySelector("form.search-form");
|
|
const elQ = document.querySelector("#q");
|
|
let debounce;
|
|
|
|
autocomp(elQ, {
|
|
autoSelect: false,
|
|
onQuery: async (val) => {
|
|
const langCode = localStorage.from_lang;
|
|
clearTimeout(debounce);
|
|
return new Promise(resolve => {
|
|
debounce = setTimeout(async () => {
|
|
const response = await fetch(`${_ROOT_URL}/api/autocomplete/${langCode}/${val}`);
|
|
const data = await response.json();
|
|
|
|
const suggestions = data.data.map(item => item.content[0]);
|
|
|
|
debounce = null;
|
|
resolve(suggestions);
|
|
}, 50);
|
|
});
|
|
},
|
|
|
|
onSelect: (val) => {
|
|
// autocomp search isn't complete. Use the user's input instead of autocomp selection.
|
|
if (val) {
|
|
elQ.value = val;
|
|
}
|
|
|
|
elForm.dispatchEvent(new Event("submit", { cancelable: true }));
|
|
return elQ.value;
|
|
}
|
|
});
|
|
})();
|
|
|
|
// Audio playback.
|
|
document.querySelectorAll("[data-audio]").forEach((el) => {
|
|
el.onclick = (e) => {
|
|
e.preventDefault();
|
|
const audio = new Audio(el.dataset.src);
|
|
audio.play().catch((err) => {
|
|
console.error("error playing audio:", err);
|
|
alert("error playing audio");
|
|
});
|
|
};
|
|
});
|
|
|
|
// Scroll to hash on load.
|
|
window.setTimeout(() => {
|
|
if (window.location.hash) {
|
|
document.querySelector(window.location.hash)?.scrollIntoView();
|
|
}
|
|
}, 100);
|
|
|
|
// Screenshot share.
|
|
document.querySelectorAll("[data-share-entry]").forEach((el) => {
|
|
el.onclick = async (e) => {
|
|
e.preventDefault();
|
|
const entryEl = document.getElementById(el.dataset.shareEntry);
|
|
if (!entryEl) return;
|
|
try {
|
|
await shareEntry(entryEl);
|
|
} catch (err) {
|
|
console.error("Error sharing entry:", err);
|
|
alert(`Error sharing: ${err?.message || err}`);
|
|
}
|
|
};
|
|
});
|