UNPKG

@formkit/barcode

Version:
183 lines (173 loc) 7.03 kB
import { createSection, defaultIcon, outer, wrapper, label, inner, icon, prefix, textInput, $if, $attrs, suffix, help, messages, message } from '@formkit/inputs'; import { BrowserMultiFormatReader, BarcodeFormat } from '@zxing/library'; export { BarcodeFormat } from '@zxing/library'; /** * Dialog box where the video layer will appear. * * @public */ const dialog = createSection('dialog', () => ({ $el: 'dialog', attrs: { id: '$id + "-dialog"', } })); /** * The container for the scanner to live on. * * @public */ const scannerContainer = createSection("scannerContainer", () => ({ $el: "div", attrs: { class: "$classes.scannerContainer", }, })); /** * The scanner overlay for the scan bar. * * @public */ const overlay = createSection("overlay", () => ({ $el: "div", attrs: { class: "$classes.overlay", }, })); /** * The decorators for the overlay scanner. * * @public */ const overlayDecorators = createSection("overlayDecorators", () => ({ $el: "div", attrs: { class: "$classes.overlayDecorators", }, children: [ { $el: "div", attrs: { class: "$classes.overlayDecoratorTopLeft" } }, { $el: "div", attrs: { class: "$classes.overlayDecoratorTopRight" } }, { $el: "div", attrs: { class: "$classes.overlayDecoratorBottomLeft" } }, { $el: "div", attrs: { class: "$classes.overlayDecoratorBottomRight" } }, ], })); /** * The scanner laser for the scan bar. * * @public */ const laser = createSection("laser", () => ({ $el: "div", attrs: { class: "$classes.laser", }, })); /** * The video where the barcode reader will be attached. * * @public */ const video = createSection("video", () => ({ $el: "video", attrs: { class: "$classes.video", poster: "data:image/gif, AAAA", id: '$id + "-video"', }, })); const getFormats = (formats) => { if (!formats) return null; return formats.map((format) => BarcodeFormat[format]); }; const zxingMultiFormatReader = (node) => { if (node.props.type !== "barcode") return; // sets default icon while still allowing user to override it node.props.barcodeIcon = node.props.barcodeIcon || `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 50"><path fill="currentColor" d="m46.44,42.04c-1.1,0-2-.9-2-2V10.04c0-1.1.9-2,2-2s2,.9,2,2v30c0,1.1-.9,2-2,2Zm-32.88-2V10.04c0-1.1-.9-2-2-2s-2,.9-2,2v30c0,1.1.9,2,2,2s2-.9,2-2Zm3.26,1.5V8.54c0-.28-.22-.5-.5-.5s-.5.23-.5.5v32.99c0,.28.22.5.5.5s.5-.23.5-.5Zm19.49,0V8.54c0-.28-.22-.5-.5-.5s-.5.23-.5.5v32.99c0,.28.22.5.5.5s.5-.23.5-.5Zm-9-.5V9.04c0-.55-.45-1-1-1s-1,.45-1,1v32c0,.55.45,1,1,1s1-.45,1-1Zm14.06,0V9.04c0-.55-.45-1-1-1s-1,.45-1,1v32c0,.55.45,1,1,1s1-.45,1-1Zm-8.95,0V9.04c0-.55-.45-1-1-1s-1,.45-1,1v32c0,.55.45,1,1,1s1-.45,1-1Zm-9.57-.5V9.54c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5,1.5v31c0,.83.67,1.5,1.5,1.5s1.5-.67,1.5-1.5ZM3,11.5V3h8.5c.83,0,1.5-.67,1.5-1.5s-.67-1.5-1.5-1.5H1.5C.67,0,0,.67,0,1.5v10c0,.83.67,1.5,1.5,1.5s1.5-.67,1.5-1.5Zm55,0V1.5c0-.83-.67-1.5-1.5-1.5h-10c-.83,0-1.5.67-1.5,1.5s.67,1.5,1.5,1.5h8.5v8.5c0,.83.67,1.5,1.5,1.5s1.5-.67,1.5-1.5ZM13,48.53c0-.83-.67-1.5-1.5-1.5H3v-8.5c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5,1.5v10c0,.83.67,1.5,1.5,1.5h10c.83,0,1.5-.67,1.5-1.5Zm45,.05v-10c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5,1.5v8.5h-8.5c-.83,0-1.5.67-1.5,1.5s.67,1.5,1.5,1.5h10c.83,0,1.5-.67,1.5-1.5Z"></path>/svg>`; const codeReader = new BrowserMultiFormatReader(); function closeCamera() { const dialog = document.getElementById(`${node.props.id}-dialog`); codeReader.reset(); if (dialog) { dialog.close(); } } node.on("created", () => { node.context.scannerLoading = false; if (!codeReader.isMediaDevicesSuported) { node.context.scannerLoading = false; node.setErrors("Camera access not supported on your device."); return console.warn("Media Stream API is not supported in your device."); } node.context.handlers.openCamera = async () => { node.clearErrors(); const dialog = document.getElementById(`${node.props.id}-dialog`); node.context.scannerLoading = true; codeReader .decodeFromVideoDevice(null, `${node.props.id}-video`, (res) => { if (res) { const format = res.getBarcodeFormat(); const allowedFormats = getFormats(node.props.formats); // set an error if the format is not allowed if (allowedFormats && !allowedFormats.includes(format)) { return; } else { node.input(res.getText()); } closeCamera(); } }) .then(() => { dialog.showModal(); node.context.scannerLoading = false; // enable continuous autofocus of camera const videoElement = document.getElementById(`${node.props.id}-video`); if (videoElement && videoElement.srcObject instanceof MediaStream) { const track = videoElement.srcObject.getVideoTracks()[0]; if (track) { const constraints = { focusDistance: { ideal: 0 }, advanced: [{ zoom: { ideal: 0 } }], }; track .applyConstraints(constraints) .catch(() => { // do nothing }); } } }) .catch(() => { node.context.scannerLoading = false; node.setErrors("Camera access denied or not available."); codeReader.reset(); if (dialog) { dialog.close(); } }); }; node.context.handlers.closeCamera = closeCamera; }); node.on("destroying", () => { closeCamera(); }); }; const barcode = { type: "input", family: "text", props: ["formats"], features: [ zxingMultiFormatReader, defaultIcon("close", "close"), defaultIcon("loader", "spinner"), ], schema: outer(wrapper(label("$label"), inner(icon("prefix", "label"), prefix(), textInput(), // show loader or barcode icon depending on // loading state $if("$scannerLoading", icon("loader")), $if("$scannerLoading === false", $attrs({ onClick: "$handlers.openCamera" }, icon("barcode"))), suffix(), icon("suffix"))), dialog(scannerContainer($attrs({ onClick: "$handlers.closeCamera" }, icon("close")), video(), overlay(overlayDecorators(), laser()))), help("$help"), messages(message("$message.value"))), }; export { barcode };