forked from mirrors/bookwyrm
787 lines
24 KiB
JavaScript
787 lines
24 KiB
JavaScript
/* exported BookWyrm */
|
|
/* globals TabGroup, Quagga */
|
|
|
|
let BookWyrm = new (class {
|
|
constructor() {
|
|
this.MAX_FILE_SIZE_BYTES = 10 * 1000000;
|
|
this.initOnDOMLoaded();
|
|
this.initReccuringTasks();
|
|
this.initEventListeners();
|
|
}
|
|
|
|
initEventListeners() {
|
|
document
|
|
.querySelectorAll("[data-controls]")
|
|
.forEach((button) => button.addEventListener("click", this.toggleAction.bind(this)));
|
|
|
|
document
|
|
.querySelectorAll(".interaction")
|
|
.forEach((button) => button.addEventListener("submit", this.interact.bind(this)));
|
|
|
|
document
|
|
.querySelectorAll(".hidden-form input")
|
|
.forEach((button) => button.addEventListener("change", this.revealForm.bind(this)));
|
|
|
|
document
|
|
.querySelectorAll("[data-hides]")
|
|
.forEach((button) => button.addEventListener("change", this.hideForm.bind(this)));
|
|
|
|
document
|
|
.querySelectorAll("[data-back]")
|
|
.forEach((button) => button.addEventListener("click", this.back));
|
|
|
|
document
|
|
.querySelectorAll('input[type="file"]')
|
|
.forEach((node) => node.addEventListener("change", this.disableIfTooLarge.bind(this)));
|
|
|
|
document
|
|
.querySelectorAll("[data-modal-open]")
|
|
.forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this)));
|
|
|
|
document
|
|
.querySelectorAll("details.dropdown")
|
|
.forEach((node) =>
|
|
node.addEventListener("toggle", this.handleDetailsDropdown.bind(this))
|
|
);
|
|
|
|
document
|
|
.querySelector("#barcode-scanner-modal")
|
|
.addEventListener("open", this.openBarcodeScanner.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Execute code once the DOM is loaded.
|
|
*/
|
|
initOnDOMLoaded() {
|
|
const bookwyrm = this;
|
|
|
|
window.addEventListener("DOMContentLoaded", function () {
|
|
document.querySelectorAll(".tab-group").forEach((tabs) => new TabGroup(tabs));
|
|
document
|
|
.querySelectorAll('input[type="file"]')
|
|
.forEach(bookwyrm.disableIfTooLarge.bind(bookwyrm));
|
|
document.querySelectorAll("[data-copytext]").forEach(bookwyrm.copyText.bind(bookwyrm));
|
|
document
|
|
.querySelectorAll(".modal.is-active")
|
|
.forEach(bookwyrm.handleActiveModal.bind(bookwyrm));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Execute recurring tasks.
|
|
*/
|
|
initReccuringTasks() {
|
|
// Polling
|
|
document.querySelectorAll("[data-poll]").forEach((liveArea) => this.polling(liveArea));
|
|
}
|
|
|
|
/**
|
|
* Go back in browser history.
|
|
*
|
|
* @param {Event} event
|
|
* @return {undefined}
|
|
*/
|
|
back(event) {
|
|
event.preventDefault();
|
|
history.back();
|
|
}
|
|
|
|
/**
|
|
* Update a counter with recurring requests to the API
|
|
* The delay is slightly randomized and increased on each cycle.
|
|
*
|
|
* @param {Object} counter - DOM node
|
|
* @param {int} delay - frequency for polling in ms
|
|
* @return {undefined}
|
|
*/
|
|
polling(counter, delay) {
|
|
const bookwyrm = this;
|
|
|
|
delay = delay || 10000;
|
|
delay += Math.random() * 1000;
|
|
|
|
setTimeout(
|
|
function () {
|
|
fetch("/api/updates/" + counter.dataset.poll)
|
|
.then((response) => response.json())
|
|
.then((data) => bookwyrm.updateCountElement(counter, data));
|
|
|
|
bookwyrm.polling(counter, delay * 1.25);
|
|
},
|
|
delay,
|
|
counter
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update a counter.
|
|
*
|
|
* @param {object} counter - DOM node
|
|
* @param {object} data - json formatted response from a fetch
|
|
* @return {undefined}
|
|
*/
|
|
updateCountElement(counter, data) {
|
|
let count = data.count;
|
|
|
|
if (count === undefined) {
|
|
return;
|
|
}
|
|
|
|
const currentCount = counter.innerText;
|
|
const hasMentions = data.has_mentions;
|
|
|
|
if (count != currentCount) {
|
|
this.addRemoveClass(counter.closest("[data-poll-wrapper]"), "is-hidden", count < 1);
|
|
counter.innerText = count;
|
|
this.addRemoveClass(counter.closest("[data-poll-wrapper]"), "is-danger", hasMentions);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show form.
|
|
*
|
|
* @param {Event} event
|
|
* @return {undefined}
|
|
*/
|
|
revealForm(event) {
|
|
let trigger = event.currentTarget;
|
|
let hidden = trigger.closest(".hidden-form").querySelectorAll(".is-hidden")[0];
|
|
|
|
if (hidden) {
|
|
this.addRemoveClass(hidden, "is-hidden", !hidden);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide form.
|
|
*
|
|
* @param {Event} event
|
|
* @return {undefined}
|
|
*/
|
|
hideForm(event) {
|
|
let trigger = event.currentTarget;
|
|
let targetId = trigger.dataset.hides;
|
|
let visible = document.getElementById(targetId);
|
|
|
|
this.addRemoveClass(visible, "is-hidden", true);
|
|
}
|
|
|
|
/**
|
|
* Execute actions on targets based on triggers.
|
|
*
|
|
* @param {Event} event
|
|
* @return {undefined}
|
|
*/
|
|
toggleAction(event) {
|
|
let trigger = event.currentTarget;
|
|
|
|
if (!trigger.dataset.allowDefault || event.currentTarget == event.target) {
|
|
event.preventDefault();
|
|
}
|
|
let pressed = trigger.getAttribute("aria-pressed") === "false";
|
|
let targetId = trigger.dataset.controls;
|
|
|
|
// Toggle pressed status on all triggers controlling the same target.
|
|
document
|
|
.querySelectorAll('[data-controls="' + targetId + '"]')
|
|
.forEach((otherTrigger) =>
|
|
otherTrigger.setAttribute(
|
|
"aria-pressed",
|
|
otherTrigger.getAttribute("aria-pressed") === "false"
|
|
)
|
|
);
|
|
|
|
// @todo Find a better way to handle the exception.
|
|
if (targetId && !trigger.classList.contains("pulldown-menu")) {
|
|
let target = document.getElementById(targetId);
|
|
|
|
this.addRemoveClass(target, "is-hidden", !pressed);
|
|
this.addRemoveClass(target, "is-active", pressed);
|
|
}
|
|
|
|
// Show/hide pulldown-menus.
|
|
if (trigger.classList.contains("pulldown-menu")) {
|
|
this.toggleMenu(trigger, targetId);
|
|
}
|
|
|
|
// Show/hide container.
|
|
let container = document.getElementById("hide_" + targetId);
|
|
|
|
if (container) {
|
|
this.toggleContainer(container, pressed);
|
|
}
|
|
|
|
// Check checkbox, if appropriate.
|
|
let checkbox = trigger.dataset.controlsCheckbox;
|
|
|
|
if (checkbox) {
|
|
this.toggleCheckbox(checkbox, pressed);
|
|
}
|
|
|
|
// Toggle form disabled, if appropriate
|
|
let disable = trigger.dataset.disables;
|
|
|
|
if (disable) {
|
|
this.toggleDisabled(disable, !pressed);
|
|
}
|
|
|
|
// Set focus, if appropriate.
|
|
let focus = trigger.dataset.focusTarget;
|
|
|
|
if (focus) {
|
|
this.toggleFocus(focus);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Show or hide menus.
|
|
*
|
|
* @param {Event} event
|
|
* @return {undefined}
|
|
*/
|
|
toggleMenu(trigger, targetId) {
|
|
let expanded = trigger.getAttribute("aria-expanded") == "false";
|
|
|
|
trigger.setAttribute("aria-expanded", expanded);
|
|
|
|
if (targetId) {
|
|
let target = document.getElementById(targetId);
|
|
|
|
this.addRemoveClass(target, "is-active", expanded);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show or hide generic containers.
|
|
*
|
|
* @param {object} container - DOM node
|
|
* @param {boolean} pressed - Is the trigger pressed?
|
|
* @return {undefined}
|
|
*/
|
|
toggleContainer(container, pressed) {
|
|
this.addRemoveClass(container, "is-hidden", pressed);
|
|
}
|
|
|
|
/**
|
|
* Check or uncheck a checkbox.
|
|
*
|
|
* @param {string} checkbox - id of the checkbox
|
|
* @param {boolean} pressed - Is the trigger pressed?
|
|
* @return {undefined}
|
|
*/
|
|
toggleCheckbox(checkbox, pressed) {
|
|
document.getElementById(checkbox).checked = !!pressed;
|
|
}
|
|
|
|
/**
|
|
* Enable or disable a form element or fieldset
|
|
*
|
|
* @param {string} form_element - id of the element
|
|
* @param {boolean} pressed - Is the trigger pressed?
|
|
* @return {undefined}
|
|
*/
|
|
toggleDisabled(form_element, pressed) {
|
|
document.getElementById(form_element).disabled = !!pressed;
|
|
}
|
|
|
|
/**
|
|
* Give the focus to an element.
|
|
* Only move the focus based on user interactions.
|
|
*
|
|
* @param {string} nodeId - ID of the DOM node to focus (button, link…)
|
|
* @return {undefined}
|
|
*/
|
|
toggleFocus(nodeId) {
|
|
let node = document.getElementById(nodeId);
|
|
|
|
node.focus();
|
|
|
|
setTimeout(function () {
|
|
node.selectionStart = node.selectionEnd = 10000;
|
|
}, 0);
|
|
}
|
|
|
|
/**
|
|
* Make a request and update the UI accordingly.
|
|
* This function is used for boosts, favourites, follows and unfollows.
|
|
*
|
|
* @param {Event} event
|
|
* @return {undefined}
|
|
*/
|
|
interact(event) {
|
|
event.preventDefault();
|
|
|
|
const bookwyrm = this;
|
|
const form = event.currentTarget;
|
|
const relatedforms = document.querySelectorAll(`.${form.dataset.id}`);
|
|
|
|
// Toggle class on all related forms.
|
|
relatedforms.forEach((relatedForm) =>
|
|
bookwyrm.addRemoveClass(
|
|
relatedForm,
|
|
"is-hidden",
|
|
relatedForm.className.indexOf("is-hidden") == -1
|
|
)
|
|
);
|
|
|
|
this.ajaxPost(form).catch((error) => {
|
|
// @todo Display a notification in the UI instead.
|
|
console.warn("Request failed:", error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Submit a form using POST.
|
|
*
|
|
* @param {object} form - Form to be submitted
|
|
* @return {Promise}
|
|
*/
|
|
ajaxPost(form) {
|
|
return fetch(form.action, {
|
|
method: "POST",
|
|
body: new FormData(form),
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add or remove a class based on a boolean condition.
|
|
*
|
|
* @param {object} node - DOM node to change class on
|
|
* @param {string} classname - Name of the class
|
|
* @param {boolean} add - Add?
|
|
* @return {undefined}
|
|
*/
|
|
addRemoveClass(node, classname, add) {
|
|
if (add) {
|
|
node.classList.add(classname);
|
|
} else {
|
|
node.classList.remove(classname);
|
|
}
|
|
}
|
|
|
|
disableIfTooLarge(eventOrElement) {
|
|
const { addRemoveClass, MAX_FILE_SIZE_BYTES } = this;
|
|
const element = eventOrElement.currentTarget || eventOrElement;
|
|
|
|
const submits = element.form.querySelectorAll('[type="submit"]');
|
|
const warns = element.parentElement.querySelectorAll(".file-too-big");
|
|
const isTooBig =
|
|
element.files && element.files[0] && element.files[0].size > MAX_FILE_SIZE_BYTES;
|
|
|
|
if (isTooBig) {
|
|
submits.forEach((submitter) => (submitter.disabled = true));
|
|
warns.forEach((sib) => addRemoveClass(sib, "is-hidden", false));
|
|
} else {
|
|
submits.forEach((submitter) => (submitter.disabled = false));
|
|
warns.forEach((sib) => addRemoveClass(sib, "is-hidden", true));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle the modal component with a button trigger.
|
|
*
|
|
* @param {Event} event - Event fired by an element
|
|
* with the `data-modal-open` attribute
|
|
* pointing to a modal by its id.
|
|
* @return {undefined}
|
|
*
|
|
* See https://github.com/bookwyrm-social/bookwyrm/pull/1633
|
|
* for information about using the modal.
|
|
*/
|
|
handleModalButton(event) {
|
|
const { handleFocusTrap } = this;
|
|
const modalButton = event.currentTarget;
|
|
const targetModalId = modalButton.dataset.modalOpen;
|
|
const htmlElement = document.querySelector("html");
|
|
const modal = document.getElementById(targetModalId);
|
|
|
|
if (!modal) {
|
|
return;
|
|
}
|
|
|
|
// Helper functions
|
|
function handleModalOpen(modalElement) {
|
|
event.preventDefault();
|
|
|
|
htmlElement.classList.add("is-clipped");
|
|
modalElement.classList.add("is-active");
|
|
modalElement.getElementsByClassName("modal-card")[0].focus();
|
|
|
|
const closeButtons = modalElement.querySelectorAll("[data-modal-close]");
|
|
|
|
closeButtons.forEach((button) => {
|
|
button.addEventListener("click", function () {
|
|
handleModalClose(modalElement);
|
|
});
|
|
});
|
|
|
|
document.addEventListener("keydown", function (event) {
|
|
if (event.key === "Escape") {
|
|
handleModalClose(modalElement);
|
|
}
|
|
});
|
|
|
|
modalElement.addEventListener("keydown", handleFocusTrap);
|
|
modalElement.dispatchEvent(new Event("open"));
|
|
}
|
|
|
|
function handleModalClose(modalElement) {
|
|
modalElement.dispatchEvent(new Event("close"));
|
|
modalElement.removeEventListener("keydown", handleFocusTrap);
|
|
htmlElement.classList.remove("is-clipped");
|
|
modalElement.classList.remove("is-active");
|
|
modalButton.focus();
|
|
}
|
|
|
|
// Open modal
|
|
handleModalOpen(modal);
|
|
}
|
|
|
|
/**
|
|
* Handle the modal component when opened at page load.
|
|
*
|
|
* @param {Element} modalElement - Active modal element
|
|
* @return {undefined}
|
|
*
|
|
*/
|
|
handleActiveModal(modalElement) {
|
|
if (!modalElement) {
|
|
return;
|
|
}
|
|
|
|
const { handleFocusTrap } = this;
|
|
|
|
modalElement.getElementsByClassName("modal-card")[0].focus();
|
|
|
|
const closeButtons = modalElement.querySelectorAll("[data-modal-close]");
|
|
|
|
closeButtons.forEach((button) => {
|
|
button.addEventListener("click", function () {
|
|
handleModalClose(modalElement);
|
|
});
|
|
});
|
|
|
|
document.addEventListener("keydown", function (event) {
|
|
if (event.key === "Escape") {
|
|
handleModalClose(modalElement);
|
|
}
|
|
});
|
|
|
|
modalElement.addEventListener("keydown", handleFocusTrap);
|
|
|
|
function handleModalClose(modalElement) {
|
|
modalElement.removeEventListener("keydown", handleFocusTrap);
|
|
history.back();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display pop up window.
|
|
*
|
|
* @param {string} url Url to open
|
|
* @param {string} windowName windowName
|
|
* @return {undefined}
|
|
*/
|
|
displayPopUp(url, windowName) {
|
|
window.open(url, windowName, "left=100,top=100,width=430,height=600");
|
|
}
|
|
|
|
/**
|
|
* Set up a "click-to-copy" component from a textarea element
|
|
* with `data-copytext`, `data-copytext-label`, `data-copytext-success`
|
|
* attributes.
|
|
*
|
|
* @param {object} node - DOM node of the text container
|
|
* @return {undefined}
|
|
*/
|
|
|
|
copyText(textareaEl) {
|
|
const text = textareaEl.textContent;
|
|
|
|
const copyButtonEl = document.createElement("button");
|
|
|
|
copyButtonEl.textContent = textareaEl.dataset.copytextLabel;
|
|
copyButtonEl.classList.add("button", "is-small", "is-primary", "is-light");
|
|
copyButtonEl.addEventListener("click", () => {
|
|
navigator.clipboard.writeText(text).then(function () {
|
|
textareaEl.classList.add("is-success");
|
|
copyButtonEl.classList.replace("is-primary", "is-success");
|
|
copyButtonEl.textContent = textareaEl.dataset.copytextSuccess;
|
|
});
|
|
});
|
|
|
|
textareaEl.parentNode.appendChild(copyButtonEl);
|
|
}
|
|
|
|
/**
|
|
* Handle the details dropdown component.
|
|
*
|
|
* @param {Event} event - Event fired by a `details` element
|
|
* with the `dropdown` class name, on toggle.
|
|
* @return {undefined}
|
|
*/
|
|
handleDetailsDropdown(event) {
|
|
const detailsElement = event.target;
|
|
const summaryElement = detailsElement.querySelector("summary");
|
|
const menuElement = detailsElement.querySelector(".dropdown-menu");
|
|
const htmlElement = document.querySelector("html");
|
|
|
|
if (detailsElement.open) {
|
|
// Focus first menu element
|
|
menuElement
|
|
.querySelectorAll("a[href]:not([disabled]), button:not([disabled])")[0]
|
|
.focus();
|
|
|
|
// Enable focus trap
|
|
menuElement.addEventListener("keydown", this.handleFocusTrap);
|
|
|
|
// Close on Esc
|
|
detailsElement.addEventListener("keydown", handleEscKey);
|
|
|
|
// Clip page if Mobile
|
|
if (this.isMobile()) {
|
|
htmlElement.classList.add("is-clipped");
|
|
}
|
|
} else {
|
|
summaryElement.focus();
|
|
|
|
// Disable focus trap
|
|
menuElement.removeEventListener("keydown", this.handleFocusTrap);
|
|
|
|
// Unclip page
|
|
if (this.isMobile()) {
|
|
htmlElement.classList.remove("is-clipped");
|
|
}
|
|
}
|
|
|
|
function handleEscKey(event) {
|
|
if (event.key !== "Escape") {
|
|
return;
|
|
}
|
|
|
|
summaryElement.click();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if windows matches mobile media query.
|
|
*
|
|
* @return {Boolean}
|
|
*/
|
|
isMobile() {
|
|
return window.matchMedia("(max-width: 768px)").matches;
|
|
}
|
|
|
|
/**
|
|
* Focus trap handler
|
|
*
|
|
* @param {Event} event - Keydown event.
|
|
* @return {undefined}
|
|
*/
|
|
handleFocusTrap(event) {
|
|
if (event.key !== "Tab") {
|
|
return;
|
|
}
|
|
|
|
const focusableEls = event.currentTarget.querySelectorAll(
|
|
[
|
|
"a[href]:not([disabled])",
|
|
"button:not([disabled])",
|
|
"textarea:not([disabled])",
|
|
'input:not([type="hidden"]):not([disabled])',
|
|
"select:not([disabled])",
|
|
"details:not([disabled])",
|
|
'[tabindex]:not([tabindex="-1"]):not([disabled])',
|
|
].join(",")
|
|
);
|
|
const firstFocusableEl = focusableEls[0];
|
|
const lastFocusableEl = focusableEls[focusableEls.length - 1];
|
|
|
|
if (event.shiftKey) {
|
|
/* Shift + tab */ if (document.activeElement === firstFocusableEl) {
|
|
lastFocusableEl.focus();
|
|
event.preventDefault();
|
|
}
|
|
} /* Tab */ else {
|
|
if (document.activeElement === lastFocusableEl) {
|
|
firstFocusableEl.focus();
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
|
|
openBarcodeScanner(event) {
|
|
const scannerNode = document.getElementById("barcode-scanner");
|
|
const statusNode = document.getElementById("barcode-status");
|
|
const cameraListNode = document.querySelector("#barcode-camera-list > select");
|
|
|
|
cameraListNode.addEventListener("change", onChangeCamera);
|
|
|
|
function onChangeCamera(event) {
|
|
initBarcodes(event.target.value);
|
|
}
|
|
|
|
function toggleStatus(status) {
|
|
for (const child of statusNode.children) {
|
|
BookWyrm.toggleContainer(child, !child.classList.contains(status));
|
|
}
|
|
}
|
|
|
|
function initBarcodes(cameraId = null) {
|
|
toggleStatus("grant-access");
|
|
|
|
if (!cameraId) {
|
|
cameraId = sessionStorage.getItem("preferredCam");
|
|
} else {
|
|
sessionStorage.setItem("preferredCam", cameraId);
|
|
}
|
|
|
|
scannerNode.replaceChildren();
|
|
Quagga.stop();
|
|
Quagga.init(
|
|
{
|
|
inputStream: {
|
|
name: "Live",
|
|
type: "LiveStream",
|
|
target: scannerNode,
|
|
constraints: {
|
|
facingMode: "environment",
|
|
deviceId: cameraId,
|
|
},
|
|
},
|
|
decoder: {
|
|
readers: [
|
|
"ean_reader",
|
|
{
|
|
format: "ean_reader",
|
|
config: {
|
|
supplements: ["ean_2_reader", "ean_5_reader"],
|
|
},
|
|
},
|
|
],
|
|
multiple: false,
|
|
},
|
|
},
|
|
(err) => {
|
|
if (err) {
|
|
scannerNode.replaceChildren();
|
|
console.log(err);
|
|
toggleStatus("access-denied");
|
|
|
|
return;
|
|
}
|
|
|
|
let activeId = null;
|
|
const track = Quagga.CameraAccess.getActiveTrack();
|
|
|
|
if (track) {
|
|
activeId = track.getSettings().deviceId;
|
|
}
|
|
|
|
Quagga.CameraAccess.enumerateVideoDevices().then((devices) => {
|
|
cameraListNode.replaceChildren();
|
|
|
|
for (const device of devices) {
|
|
const child = document.createElement("option");
|
|
|
|
child.value = device.deviceId;
|
|
child.innerText = device.label.slice(0, 30);
|
|
|
|
if (activeId === child.value) {
|
|
child.selected = true;
|
|
}
|
|
|
|
cameraListNode.appendChild(child);
|
|
}
|
|
});
|
|
|
|
toggleStatus("scanning");
|
|
Quagga.start();
|
|
}
|
|
);
|
|
}
|
|
|
|
function cleanup(clearDrawing = true) {
|
|
Quagga.stop();
|
|
cameraListNode.removeEventListener("change", onChangeCamera);
|
|
|
|
if (clearDrawing) {
|
|
scannerNode.replaceChildren();
|
|
}
|
|
}
|
|
|
|
Quagga.onProcessed((result) => {
|
|
const drawingCtx = Quagga.canvas.ctx.overlay;
|
|
const drawingCanvas = Quagga.canvas.dom.overlay;
|
|
|
|
if (result) {
|
|
if (result.boxes) {
|
|
drawingCtx.clearRect(
|
|
0,
|
|
0,
|
|
parseInt(drawingCanvas.getAttribute("width")),
|
|
parseInt(drawingCanvas.getAttribute("height"))
|
|
);
|
|
result.boxes
|
|
.filter((box) => box !== result.box)
|
|
.forEach((box) => {
|
|
Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
|
|
color: "green",
|
|
lineWidth: 2,
|
|
});
|
|
});
|
|
}
|
|
|
|
if (result.box) {
|
|
Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, {
|
|
color: "#00F",
|
|
lineWidth: 2,
|
|
});
|
|
}
|
|
|
|
if (result.codeResult && result.codeResult.code) {
|
|
Quagga.ImageDebug.drawPath(result.line, { x: "x", y: "y" }, drawingCtx, {
|
|
color: "red",
|
|
lineWidth: 3,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
let lastDetection = null;
|
|
let numDetected = 0;
|
|
|
|
Quagga.onDetected((result) => {
|
|
// Detect the same code 3 times as an extra check to avoid bogus scans.
|
|
if (lastDetection === null || lastDetection !== result.codeResult.code) {
|
|
numDetected = 1;
|
|
lastDetection = result.codeResult.code;
|
|
|
|
return;
|
|
} else if (numDetected++ < 3) {
|
|
return;
|
|
}
|
|
|
|
const code = result.codeResult.code;
|
|
|
|
statusNode.querySelector(".isbn").innerText = code;
|
|
toggleStatus("found");
|
|
|
|
const search = new URL("/search", document.location);
|
|
|
|
search.searchParams.set("q", code);
|
|
|
|
cleanup(false);
|
|
location.assign(search);
|
|
});
|
|
|
|
event.target.addEventListener("close", cleanup, { once: true });
|
|
|
|
initBarcodes();
|
|
}
|
|
})();
|