diff --git a/assets/controllers/elements/nonprintable_char_input_controller.js b/assets/controllers/elements/nonprintable_char_input_controller.js index f93833ee..bd172f1b 100644 --- a/assets/controllers/elements/nonprintable_char_input_controller.js +++ b/assets/controllers/elements/nonprintable_char_input_controller.js @@ -19,12 +19,17 @@ import {Controller} from "@hotwired/stimulus"; +/** + * Purpose of this controller is to allow users to input non-printable characters like EOT, FS, etc. in a form field and submit them correctly with the form. + * The visible input field encodes non-printable characters via their Unicode Control picture representation, e.g. \n becomes ␊ and \t becomes ␉, so that they can be displayed in the input field without breaking the form submission. + * The actual value of the field, which is submitted with the form, is stored in a hidden input and contains the non-printable characters in their original form. + */ export default class extends Controller { _hiddenInput; connect() { - this.element.addEventListener("change", this._update.bind(this)); + this.element.addEventListener("input", this._update.bind(this)); // We use a hidden input to store the actual value of the field, which is submitted with the form. // The visible input is just for user interaction and can contain non-printable characters, which are not allowed in the hidden input. diff --git a/assets/controllers/helpers/scan_special_char_controller.js b/assets/controllers/helpers/scan_special_char_controller.js new file mode 100644 index 00000000..154b2a94 --- /dev/null +++ b/assets/controllers/helpers/scan_special_char_controller.js @@ -0,0 +1,136 @@ +/* + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Controller } from "@hotwired/stimulus" + +/** + * This controller listens for a special non-printable character (SOH / ASCII 1) to be entered anywhere on the page, + * which is then used as a trigger to submit the following characters as a barcode / scan input. + */ +export default class extends Controller { + connect() { + // Optional: Log to confirm global attachment + console.log("Scanner listener active") + + this.isCapturing = false + this.buffer = "" + + window.addEventListener("keypress", this.handleKeydown.bind(this)) + } + + initialize() { + this.isCapturing = false + this.buffer = "" + this.timeoutId = null + } + + handleKeydown(event) { + + // Ignore if the user is typing in a form field + const isInput = ["INPUT", "TEXTAREA", "SELECT"].includes(event.target.tagName) || + event.target.isContentEditable; + if (isInput) return + + // 1. Detect Start of Header (SOH / Ctrl+A) + if (event.key === "\x01" || event.keyCode === 1) { + this.startCapturing(event) + return + } + + // 2. Process characters if in capture mode + if (this.isCapturing) { + this.resetTimeout() // Push the expiration back with every keypress + + if (event.key === "Enter" || event.keyCode === 13) { + + this.finishCapturing(event) + } else if (event.key.length === 1) { + this.buffer += event.key + } + } + } + + startCapturing(event) { + this.isCapturing = true + this.buffer = "" + this.resetTimeout() + event.preventDefault() + console.debug("Scan character detected. Capture started...") + } + + finishCapturing(event) { + event.preventDefault() + const data = this.buffer; + this.stopCapturing() + this.processCapture(data) + } + + stopCapturing() { + this.isCapturing = false + this.buffer = "" + if (this.timeoutId) clearTimeout(this.timeoutId) + console.debug("Capture cleared/finished.") + } + + resetTimeout() { + if (this.timeoutId) clearTimeout(this.timeoutId) + + this.timeoutId = setTimeout(() => { + if (this.isCapturing) { + console.warn("Capture timed out. Resetting buffer.") + this.stopCapturing() + } + }, 500) + } + + processCapture(data) { + if (!data) return + + console.debug("Captured scan data: " + data) + + const scanInput = document.getElementById("scan_dialog_input"); + if (scanInput) { //When we are on the scan dialog page, submit the form there + this._submitScanForm(data); + } else { //Otherwise use our own form (e.g. on the part list page) + this.element.querySelector("input[name='input']").value = data; + this.element.requestSubmit(); + } + + + } + + _submitScanForm(data) { + const scanInput = document.getElementById("scan_dialog_input"); + if (!scanInput) { + console.error("Scan input field not found!") + return; + } + + scanInput.value = data; + scanInput.dispatchEvent(new Event('input', { bubbles: true })); + + const form = document.getElementById("scan_dialog_form"); + if (!form) { + console.error("Scan form not found!") + return; + } + + form.requestSubmit(); + } +} diff --git a/templates/base.html.twig b/templates/base.html.twig index 62f0ce53..4b49cb54 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -2,7 +2,7 @@ @@ -73,9 +73,17 @@ {{ encore_entry_script_tags('webauthn_tfa') }} {% endblock %} - + +{# Listen for the special #} +{% if is_granted("@tools.label_scanner") %} +
+ +
+{% endif %} + {% block body %}
@@ -121,13 +129,13 @@ {# Must be outside of the sidebar or it will be hidden too #}