Skip to content

Commit

Permalink
feat: added custom autocomplete to upload inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
jstucke committed Sep 9, 2024
1 parent 04fe945 commit a7c8f3b
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 37 deletions.
26 changes: 20 additions & 6 deletions src/web_interface/static/css/upload.css
Original file line number Diff line number Diff line change
@@ -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: 29px; /* dropdown button width */
font-family: monospace;
}
.autocomplete-items li {
cursor: pointer;
padding: 5px 5px 5px 10px;
border: none;
}
.btn-file {
position: relative;
overflow: hidden;
}
121 changes: 110 additions & 11 deletions src/web_interface/static/js/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ function add_device_class_options(selected_device_class, selected_vendor, data)
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) {
// also update the global variable for autocomplete but without overwriting it
deviceNames.length = 0; // empty existing array
deviceNames.push(...new Set(data[selected_device_class][selected_vendor])); // remove duplicates
if (deviceNames.length > 0) {
deviceNames.sort();
deviceNames.forEach(device => {
deviceNameList.append(`
<a class="dropdown-item" href="#" onClick="updateInput('device_name', this)">
${device_classes[index]}
${device}
</a>
`);
}
});
deviceClassButton.disabled = false;
return;
}
Expand All @@ -31,13 +31,13 @@ function add_device_class_options(selected_device_class, selected_vendor, data)
deviceClassButton.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);
add_device_class_options(device_class, vendor, allDeviceNames);
}
}

Expand All @@ -56,7 +56,7 @@ function updateInput(input_id, element, do_update = false) {
const input = document.getElementById(input_id);
input.value = element.innerText;
if (do_update) {
update_device_names();
updateDeviceNames();
}
}

Expand All @@ -70,3 +70,102 @@ function filterFunction(input) {
}
});
}

function autocompleteInput(inputId, options) {
const input = document.getElementById(inputId);
let currentFocus;
input.addEventListener("input", (_) => {
closeAll();
if (!input.value) {
return false;
}
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 (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
`<strong>${option.slice(index, index + input.value.length)}</strong>` +
option.slice(index + input.value.length)
);
listItem.__value = option;
listItem.__inputId = inputId;
listItem.addEventListener("click", (event) => {
// the user can either click the item or the bold text (which is inside the item)
target = event.target.tagName.toLowerCase() === "strong" ? event.target.parentNode : event.target;
input.value = target.__value;
closeAll();
if (["device_class", "vendor"].includes(target.__inputId)) {
updateDeviceNames();
}
});
list.appendChild(listItem);
}
});
});

input.addEventListener("keydown", (event) => {
if (event.ctrlKey && event.code === "Space") {
try {
// open dropdown menu on ctrl+space
const div = input.parentNode.getElementsByClassName("input-group-append")[0];
const dropdownButton = div.getElementsByTagName("button")[0];
dropdownButton.click();
} catch (Error) {
console.error(Error);
}
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 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 closeAll(currentElement) {
const elements = document.getElementsByClassName("autocomplete-items");
Array.from(elements).forEach((element) => {
if (currentElement !== element && currentElement !== input) {
element.parentNode.removeChild(element);
}
});
}

document.addEventListener("click", (event) => {
closeAll(event.target);
});
}
2 changes: 1 addition & 1 deletion src/web_interface/templates/macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ <h5 class="card-title mb-3"><i class="fas fa-{{ icon }}"></i> {{ panel_title }}<
{% macro upload_dropdown(input_id, options, update_device_list=False, include_filter=False) %}
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" tabindex="-1">
<span class="sr-only">Toggle Dropdown</span>
</button>
{% if include_filter %}
Expand Down
34 changes: 15 additions & 19 deletions src/web_interface/templates/upload/upload.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,28 @@

{%- set active_page = "Upload" -%}
{%- set is_reanalysis = True if firmware else False -%}
{%- set device_part_options = ["complete", "kernel", "bootloader", "root-fs"] -%}

{% block head %}

<script>
const device_names = {{ device_names | safe }};
const allDeviceNames = {{ device_names | safe }};
let deviceNames = [];
const plugin_dict = {{ analysis_plugin_dict | dict_to_json | safe }};
</script>
<script type="text/javascript" src="{{ url_for('static', filename='js/upload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/loading.js') }}"></script>
<script>
$(window).bind("pageshow", function (event) {
// if we come back to the page make sure that the loading gif is not covering up the form
// if we come back to the page, make sure that the loading gif is not covering up the form
hideImg();
});
$(document).ready(() => {
autocompleteInput('device_class', {{ device_classes | tojson | safe }});
autocompleteInput('vendor', {{ vendors | tojson | safe }});
autocompleteInput('device_part', {{ device_part_options | tojson | safe }});
autocompleteInput('device_name', deviceNames);
});
</script>
<style>
.upload-label {
justify-content: center;
width: 135px;
}

.upload-icon {
justify-content: center;
width: 45px;
}
</style>
{% endblock %}

{%- block styles -%}
Expand Down Expand Up @@ -80,7 +76,7 @@ <h3 class="mb-3">Upload Firmware</h3>
{%- call macros.upload_input("device_class", "Device Class") %}
<input type="text" name="device_class" class="form-control" id="device_class"
{%- if is_reanalysis %} value="{{ firmware.device_class }}" {%- endif -%}
placeholder="Select device class..." required>
placeholder="Select device class..." autocomplete="off" required>

{{ macros.upload_dropdown("device_class", device_classes, True, True) }}
{%- endcall %}
Expand All @@ -89,7 +85,7 @@ <h3 class="mb-3">Upload Firmware</h3>
{%- call macros.upload_input("vendor", "Vendor") %}
<input type="text" name="vendor" class="form-control" id="vendor"
{%- if is_reanalysis %} value="{{ firmware.vendor }}" {%- endif -%}
placeholder="Select vendor..." required>
placeholder="Select vendor..." autocomplete="off" required>

{{ macros.upload_dropdown("vendor", vendors, True, True) }}
{%- endcall %}
Expand All @@ -98,10 +94,10 @@ <h3 class="mb-3">Upload Firmware</h3>
{%- call macros.upload_input("device_name", "Device Name") %}
<input type="text" name="device_name" class="form-control" id="device_name"
{%- if is_reanalysis %} value="{{ firmware.device_name }}" {%- endif -%}
placeholder="Select device name..." required>
placeholder="Select device name..." autocomplete="off" required>

<div class="input-group-append">
<button type="button" id="device_class_select_button" disabled
<button type="button" id="device_class_select_button" tabindex="-1" disabled
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
Expand All @@ -117,7 +113,7 @@ <h3 class="mb-3">Upload Firmware</h3>
{%- if is_reanalysis and firmware.part %} value="{{ firmware.part }}" {%- endif -%}
value="complete" required>

{{ macros.upload_dropdown("device_part", ["complete", "kernel", "bootloader", "root-fs"]) }}
{{ macros.upload_dropdown("device_part", device_part_options) }}
{%- endcall %}

{# Version #}
Expand Down

0 comments on commit a7c8f3b

Please sign in to comment.