From 2660f4ee8276f3b54e94bb67455e633aeac0074c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 1 Mar 2026 13:36:52 +0100 Subject: [PATCH] Render non-printable chars in the scan input field --- .../nonprintable_char_input_controller.js | 73 +++++++++++++++++++ src/Form/LabelSystem/ScanDialogType.php | 1 + 2 files changed, 74 insertions(+) create mode 100644 assets/controllers/elements/nonprintable_char_input_controller.js diff --git a/assets/controllers/elements/nonprintable_char_input_controller.js b/assets/controllers/elements/nonprintable_char_input_controller.js new file mode 100644 index 00000000..eb99b297 --- /dev/null +++ b/assets/controllers/elements/nonprintable_char_input_controller.js @@ -0,0 +1,73 @@ +/* + * 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"; + +export default class extends Controller { + + _hiddenInput; + + connect() { + this.element.addEventListener("input", this._onInput.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. + this._hiddenInput = document.createElement("input"); + this._hiddenInput.type = "hidden"; + this._hiddenInput.name = this.element.name; + this.element.removeAttribute("name"); + this.element.parentNode.insertBefore(this._hiddenInput, this.element.nextSibling); + } + + _onInput(event) { + // Remove non-printable characters from the input value and store them in the hidden input + + const normalizedValue = this.decodeNonPrintableChars(this.element.value); + this._hiddenInput.value = normalizedValue; + + // Encode non-printable characters in the visible input to their Unicode Control picture representation + const encodedValue = this.encodeNonPrintableChars(normalizedValue); + if (encodedValue !== this.element.value) { + this.element.value = encodedValue; + } + } + + /** + * Encodes non-printable characters in the given string via their Unicode Control picture representation, e.g. \n becomes ␊ and \t becomes ␉. + * This allows us to display non-printable characters in the input field without breaking the form submission. + * @param str + */ + encodeNonPrintableChars(str) { + return str.replace(/[\x00-\x1F\x7F]/g, (char) => { + const code = char.charCodeAt(0); + return String.fromCharCode(0x2400 + code); + }); + } + + /** + * Decodes the Unicode Control picture representation of non-printable characters back to their original form, e.g. ␊ becomes \n and ␉ becomes \t. + * @param str + */ + decodeNonPrintableChars(str) { + return str.replace(/[\u2400-\u241F\u2421]/g, (char) => { + const code = char.charCodeAt(0) - 0x2400; + return String.fromCharCode(code); + }); + } +} diff --git a/src/Form/LabelSystem/ScanDialogType.php b/src/Form/LabelSystem/ScanDialogType.php index 42344ad1..5f9ce65f 100644 --- a/src/Form/LabelSystem/ScanDialogType.php +++ b/src/Form/LabelSystem/ScanDialogType.php @@ -62,6 +62,7 @@ class ScanDialogType extends AbstractType 'autofocus' => true, 'id' => 'scan_dialog_input', 'style' => 'font-family: var(--bs-font-monospace)', + 'data-controller' => 'elements--nonprintable-char-input', ], ]);