diff --git a/src/test/unit/web_interface/test_app_re_analyze.py b/src/test/unit/web_interface/test_app_re_analyze.py
index 8d01f1e3d..abc231b9b 100644
--- a/src/test/unit/web_interface/test_app_re_analyze.py
+++ b/src/test/unit/web_interface/test_app_re_analyze.py
@@ -1,3 +1,5 @@
+import re
+
from helperFunctions.data_conversion import make_bytes
from test.common_helper import TEST_FW
@@ -12,11 +14,11 @@ def test_app_re_analyze_get_valid_firmware(self, test_client):
assert b'
update analysis of
' in rv.data
assert b'
TEST_FW_HID
' in rv.data
assert (
- b'value="default_plugin" checked' not in rv.data
+ re.search(rb'value="default_plugin"\s+checked>', rv.data) is None
), 'plugins that did not run for TEST_FW should not be checked'
assert b'value="mandatory_plugin"' not in rv.data, 'mandatory plugins should not be listed'
- assert (
- b'value="optional_plugin" checked' in rv.data
+ assert re.search(
+ rb'value="optional_plugin"\s+checked>', rv.data
), 'optional plugins that did run for TEST_FW should be checked'
def test_app_re_analyze_post_valid(self, test_client, intercom_task_list):
diff --git a/src/test/unit/web_interface/test_app_upload.py b/src/test/unit/web_interface/test_app_upload.py
index 6b7b68b0e..4f73de688 100644
--- a/src/test/unit/web_interface/test_app_upload.py
+++ b/src/test/unit/web_interface/test_app_upload.py
@@ -1,3 +1,4 @@
+import re
from io import BytesIO
@@ -5,9 +6,9 @@ class TestAppUpload:
def test_app_upload_get(self, test_client):
rv = test_client.get('/upload')
assert b'
Upload Firmware
' in rv.data
- assert b'value="default_plugin" checked' in rv.data
+ assert re.search(rb'value="default_plugin"\s+checked>', rv.data)
assert b'value="mandatory_plugin"' not in rv.data
- assert b'value="optional_plugin" >' in rv.data
+ assert re.search(rb'value="optional_plugin"\s+>', rv.data)
def test_app_upload_invalid_firmware(self, test_client, intercom_task_list):
rv = test_client.post(
diff --git a/src/web_interface/static/css/upload.css b/src/web_interface/static/css/upload.css
index 1a261ce0b..691568c4e 100644
--- a/src/web_interface/static/css/upload.css
+++ b/src/web_interface/static/css/upload.css
@@ -1,7 +1,21 @@
-.search {
- text-align: right;
+.upload-label {
+ justify-content: center;
+ width: 135px;
+}
+.upload-icon {
+ justify-content: center;
+ width: 45px;
+}
+.autocomplete-items {
+ position: absolute;
+ z-index: 99;
+ top: 100%;
+ left: 135px; /* upload-label width */
+ right: 35px; /* dropdown button width */
+ font-family: monospace;
+}
+.autocomplete-items li {
+ cursor: pointer;
+ padding: 5px 5px 5px 10px;
+ border: none;
}
-.btn-file {
- position: relative;
- overflow: hidden;
-}
\ No newline at end of file
diff --git a/src/web_interface/static/js/upload.js b/src/web_interface/static/js/upload.js
index 530b55f5e..fd5e71294 100644
--- a/src/web_interface/static/js/upload.js
+++ b/src/web_interface/static/js/upload.js
@@ -5,39 +5,30 @@ $(() => {
});
});
-function add_device_class_options(selected_device_class, selected_vendor, data) {
- const deviceClassButton = document.getElementById("device_class_select_button");
- let deviceNameList = $('#device_name_list');
- deviceNameList.empty();
- if (data.hasOwnProperty(selected_device_class)) {
- if (data[selected_device_class].hasOwnProperty(selected_vendor)) {
- let device_classes = data[selected_device_class][selected_vendor];
- // remove duplicates
- device_classes = [...new Set(device_classes)];
- device_classes.sort();
- if (device_classes.length > 0) {
- for (let index in device_classes) {
- deviceNameList.append(`
-
- ${device_classes[index]}
-
- `);
- }
- deviceClassButton.disabled = false;
+function addDeviceNameOptions(selected_device_class, selected_vendor) {
+ const dropdownButton = document.getElementById("device_name-button");
+ if (allDeviceNames.hasOwnProperty(selected_device_class)) {
+ if (allDeviceNames[selected_device_class].hasOwnProperty(selected_vendor)) {
+ // update the global variable but without overwriting it
+ deviceNames.length = 0; // empty existing array
+ deviceNames.push(...new Set(allDeviceNames[selected_device_class][selected_vendor])); // remove duplicates
+ if (deviceNames.length > 0) {
+ deviceNames.sort();
+ dropdownButton.disabled = false;
return;
}
}
}
- deviceClassButton.disabled = true;
+ dropdownButton.disabled = true;
}
-function update_device_names() {
+function updateDeviceNames() {
const deviceClassInput = document.getElementById("device_class");
const vendorInput = document.getElementById("vendor");
const vendor = vendorInput.value.trim();
const device_class = deviceClassInput.value.trim();
if (vendor.length > 0 && device_class.length > 0) {
- add_device_class_options(device_class, vendor, device_names);
+ addDeviceNameOptions(device_class, vendor);
}
}
@@ -52,21 +43,113 @@ function change_selected_plugins(preset_name) {
}
}
-function updateInput(input_id, element, do_update = false) {
- const input = document.getElementById(input_id);
- input.value = element.innerText;
- if (do_update) {
- update_device_names();
+function autocompleteInput(inputId, options) {
+ const input = document.getElementById(inputId);
+ const dropdownButton = document.getElementById(`${inputId}-button`);
+ let currentFocus;
+
+ input.addEventListener("input", populateAutocomplete);
+ dropdownButton.addEventListener("click", () => {
+ input.value = "";
+ populateAutocomplete();
+ });
+
+ input.addEventListener("keydown", (event) => {
+ if (event.ctrlKey && event.code === "Space") {
+ populateAutocomplete();
+ return;
+ } else if (event.code === "Tab") {
+ closeAll();
+ return;
+ }
+ let list = document.getElementById(input.id + "-autocomplete-list");
+ if (!list) return;
+ let listElements = list.getElementsByTagName("li");
+ if (event.code === "ArrowDown") {
+ currentFocus++;
+ setActive(listElements);
+ } else if (event.code === "ArrowUp") {
+ currentFocus--;
+ setActive(listElements);
+ } else if (event.code === "Enter") {
+ event.preventDefault(); // prevent the form from being submitted
+ if (currentFocus > -1) {
+ listElements[currentFocus].click(); // simulate a click on the item
+ }
+ }
+ });
+
+ function populateAutocomplete() {
+ closeAll();
+ currentFocus = -1;
+ const list = document.createElement("ul");
+ list.setAttribute("id", input.id + "-autocomplete-list");
+ list.setAttribute("class", "autocomplete-items list-group border");
+ input.parentNode.appendChild(list);
+
+ let listItem, target;
+ options.forEach(option => {
+ let index = option.toLowerCase().indexOf(input.value.toLowerCase());
+ if (!input.value || index !== -1) {
+ listItem = document.createElement("li");
+ listItem.setAttribute("class", "list-group-item list-group-item-action");
+ listItem.innerHTML = (
+ option.slice(0, index) +
+ // display the matched part in bold (with the class click-through, the event.target in the
+ // eventListener below will not be the element)
+ `${option.slice(index, index + input.value.length)}` +
+ option.slice(index + input.value.length)
+ );
+ listItem.__value = option;
+ listItem.__inputId = inputId;
+ listItem.addEventListener("click", (event) => {
+ input.value = event.target.__value;
+ closeAll();
+ if (["device_class", "vendor"].includes(event.target.__inputId)) {
+ updateDeviceNames();
+ }
+ });
+ list.appendChild(listItem);
+ }
+ });
+ }
+
+ function setActive(elements) {
+ if (elements === null || elements.length === 0) return false;
+ Array.from(elements).forEach(element => {
+ element.classList.remove("active");
+ });
+ if (currentFocus >= elements.length) {
+ currentFocus = 0;
+ } else if (currentFocus < 0) {
+ currentFocus = (elements.length - 1);
+ }
+ elements[currentFocus].classList.add("active");
}
}
-function filterFunction(input) {
- let filter = input.value.toLowerCase();
- input.parentElement.querySelectorAll(".dropdown-item").forEach(element => {
- if (!element.innerText.toLowerCase().includes(filter)) {
- element.style.display = "none";
- } else {
- element.style.display = "";
+function closeAll(currentElement) {
+ if (currentElement && currentElement.id.endsWith("-button")) {
+ // if we clicked on the dropdown button we don't want to close the autocomplete list (we just opened)
+ currentElement = getAutocompleteElement(currentElement.parentNode.parentNode);
+ } else if (currentElement && currentElement.tagName.toLowerCase() === "input") {
+ // also don't close the autocomplete that belongs to an input we just clicked
+ currentElement = getAutocompleteElement(currentElement.parentNode);
+ }
+ const elements = document.getElementsByClassName("autocomplete-items");
+ Array.from(elements).forEach((element) => {
+ if (currentElement !== element) {
+ element.parentNode.removeChild(element);
}
});
}
+
+function getAutocompleteElement(parentElement) {
+ const autocompletes = parentElement.querySelectorAll("ul");
+ if (autocompletes.length === 1) return autocompletes[0];
+ return null;
+}
+
+document.addEventListener("click", (event) => {
+ closeAll(event.target);
+});
\ No newline at end of file
diff --git a/src/web_interface/templates/macros.html b/src/web_interface/templates/macros.html
index 217d3684e..bcb9ea8f3 100644
--- a/src/web_interface/templates/macros.html
+++ b/src/web_interface/templates/macros.html
@@ -22,38 +22,24 @@