Fixes: https://todo.code.netlandish.com/~netlandish/links/85
Changelog-fixed: tag autocomplete issues with selection and navigation
using the keyboard.
---
static/css/style.css | 7 ++
static/js/advancedsearch.js | 223 +++++++++++++++++++++++++++++++-----
static/js/autocomplete.js | 170 +++++++++++++++++++++++----
templates/link_list.html | 2 +-
4 files changed, 353 insertions(+), 49 deletions(-)
diff --git a/static/css/style.css b/static/css/style.css
index 36ba467..bd7d408 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -510,6 +510,13 @@ a.bullet-link:before {
cursor: pointer;
}
+.tag-suggested.highlighted, .suggested.highlighted {
+ background-color: var(--color-lightGrey);
+ border-radius: 2px;
+ padding: 2px 4px;
+ margin: 3px 0;
+}
+
.tour-section {
margin-bottom: 25px;
}
diff --git a/static/js/advancedsearch.js b/static/js/advancedsearch.js
index 45df0cf..4b228a4 100644
--- a/static/js/advancedsearch.js
+++ b/static/js/advancedsearch.js
@@ -5,6 +5,12 @@ var form = document.getElementById("advanced-search-form");
var btn = document.getElementById("advanced-search-btn")
var tagInputDiv = document.getElementById("advanced-search-div")
+// Track highlighted index for each autocomplete dropdown
+var highlightedIndexes = new WeakMap();
+
+// Track if we should allow form submission
+var allowFormSubmit = true;
+
function slugify(s) {
if (s === ", ") {
return ""
@@ -15,7 +21,77 @@ function slugify(s) {
return s.replace(/^-+|-+$/g, "");
}
-function autocomplete() {
+// Function to highlight a suggestion in a specific dropdown
+function highlightSuggestion(autocompleteTags, index) {
+ var items = autocompleteTags.querySelectorAll('.tag-suggested');
+ items.forEach(function(item, i) {
+ if (i === index) {
+ item.classList.add('highlighted');
+ } else {
+ item.classList.remove('highlighted');
+ }
+ });
+ highlightedIndexes.set(autocompleteTags, index);
+}
+
+// Function to select the highlighted suggestion
+function selectHighlightedSuggestion(tagSelector, autocompleteTags) {
+ var highlightedIndex = highlightedIndexes.get(autocompleteTags);
+ if (highlightedIndex === undefined) {
+ highlightedIndex = -1;
+ }
+ var items = autocompleteTags.querySelectorAll('.tag-suggested');
+ if (highlightedIndex >= 0 && highlightedIndex < items.length) {
+ var selectedTag = items[highlightedIndex].textContent;
+ var tags = tagSelector.value.split(",");
+ var newTag = selectedTag + ", ";
+ if (tags.length > 1) {
+ newTag = " " + newTag;
+ }
+ tags[tags.length - 1] = newTag;
+ tagSelector.value = tags.join(",");
+ autocompleteTags.classList.add("d-none");
+ highlightedIndexes.set(autocompleteTags, -1);
+ tagSelector.focus();
+ }
+}
+
+// Function to show suggestions
+function showSuggestions(autocompleteTags, data) {
+ if (data.length > 0) {
+ var currentHighlight = highlightedIndexes.get(autocompleteTags);
+ if (currentHighlight === undefined) {
+ currentHighlight = -1;
+ highlightedIndexes.set(autocompleteTags, -1); // Initialize it
+ }
+ var t = "";
+ for (var i = 0; i < data.length; i++) {
+ t += '<p class="tag-suggested" data-index="' + i + '">' + data[i].Name + '</p>';
+ }
+ autocompleteTags.innerHTML = t;
+ autocompleteTags.classList.remove("d-none");
+
+ // Preserve highlight if still valid
+ if (currentHighlight >= data.length) {
+ highlightedIndexes.set(autocompleteTags, -1);
+ } else if (currentHighlight >= 0) {
+ // Re-highlight the same index
+ highlightSuggestion(autocompleteTags, currentHighlight);
+ }
+ } else {
+ autocompleteTags.classList.add("d-none");
+ highlightedIndexes.set(autocompleteTags, -1);
+ }
+}
+
+function autocomplete(event) {
+ var tagSelector = this;
+
+ // Skip if it's a navigation key
+ if (event && ["ArrowDown", "ArrowUp", "Enter", "Tab", "Escape"].includes(event.key)) {
+ return;
+ }
+
tags = this.value.split(",")
tag = tags[tags.length -1].trim()
@@ -24,6 +100,9 @@ function autocomplete() {
autocompleteTags = this.nextElementSibling;
}
if (tag !== "") {
+ // Store the query we're searching for
+ var searchQuery = tag;
+
fetch(url + "&q=" + tag)
.then(function (response) {
if (!response.ok) {
@@ -32,44 +111,137 @@ function autocomplete() {
return response.json();
})
.then(function (data) {
- if (data.length > 0) {
- var t = "";
- for (var i=0; i < data.length; i++) {
- t += '<p class="tag-suggested">' + data[i].Name + '</p>';
- }
- if (autocompleteTags !== undefined) {
- autocompleteTags.innerHTML = t;
- autocompleteTags.classList.remove("d-none");
- }
+ // Check if the current input still matches what we searched for
+ var currentTags = tagSelector.value.split(",");
+ var currentTag = currentTags[currentTags.length - 1].trim();
+
+ if (autocompleteTags !== undefined && currentTag === searchQuery) {
+ showSuggestions(autocompleteTags, data);
+
+ // Set up click handler for this dropdown
+ autocompleteTags.onclick = function(event) {
+ if (event.target.classList.contains("tag-suggested")) {
+ var tags = tagSelector.value.split(",");
+ var newTag = event.target.textContent + ", ";
+ if (tags.length > 1) {
+ newTag = " " + newTag;
+ }
+ tags[tags.length - 1] = newTag;
+ tagSelector.value = tags.join(",");
+ autocompleteTags.classList.add("d-none");
+ highlightedIndexes.set(autocompleteTags, -1);
+ tagSelector.focus();
+ }
+ };
+
+ // Set up mouseover handler for this dropdown
+ autocompleteTags.onmouseover = function(event) {
+ if (event.target.classList.contains("tag-suggested")) {
+ var index = parseInt(event.target.getAttribute("data-index"));
+ highlightSuggestion(autocompleteTags, index);
+ }
+ };
+ } else if (autocompleteTags !== undefined) {
+ // Input has changed, hide the dropdown
+ autocompleteTags.classList.add("d-none");
+ highlightedIndexes.set(autocompleteTags, -1);
}
})
.catch(function (error) {
console.log("Fetch error:", error);
});
} else {
- autocompleteTags.classList.add("d-none");
- }
-
- tagSelector = this;
- autocompleteTags.addEventListener("click", function(event) {
- if (event.target.classList.contains("tag-suggested")) {
- newTag = event.target.textContent + ", ";
- if (tags.length > 1) {
- newTag = " " + newTag;
- }
- tags[tags.length - 1] = newTag;
- tagSelector.value = tags.join(",");
+ if (autocompleteTags !== undefined) {
autocompleteTags.classList.add("d-none");
- tagSelector.focus();
+ highlightedIndexes.set(autocompleteTags, -1);
}
- });
+ }
}
+
tagSelectors.forEach(function(tagSelector) {
tagSelector.addEventListener('keyup', autocomplete);
+
+ // Handle keyboard navigation - inline like autocomplete.js
+ tagSelector.addEventListener('keydown', function(event) {
+ var autocompleteTags = this.nextElementSibling;
+
+ if (!autocompleteTags || !autocompleteTags.classList.contains("autocomplete-tags")) {
+ return;
+ }
+
+ var items = autocompleteTags.querySelectorAll('.tag-suggested');
+ var isVisible = !autocompleteTags.classList.contains("d-none");
+ var highlightedIndex = highlightedIndexes.get(autocompleteTags);
+ if (highlightedIndex === undefined) {
+ highlightedIndex = -1;
+ }
+
+ if (!isVisible || items.length === 0) {
+ return;
+ }
+
+ switch(event.key) {
+ case "ArrowDown":
+ event.preventDefault();
+ if (highlightedIndex < items.length - 1) {
+ highlightSuggestion(autocompleteTags, highlightedIndex + 1);
+ } else {
+ highlightSuggestion(autocompleteTags, 0); // Wrap to first
+ }
+ break;
+
+ case "ArrowUp":
+ event.preventDefault();
+ if (highlightedIndex > 0) {
+ highlightSuggestion(autocompleteTags, highlightedIndex - 1);
+ } else {
+ highlightSuggestion(autocompleteTags, items.length - 1); // Wrap to last
+ }
+ break;
+
+ case "Enter":
+ if (isVisible) { // Prevent form submit if dropdown is visible
+ event.preventDefault();
+ event.stopPropagation();
+ allowFormSubmit = false;
+ if (highlightedIndex >= 0) {
+ selectHighlightedSuggestion(this, autocompleteTags);
+ }
+ // Reset the flag after a short delay
+ setTimeout(function() {
+ allowFormSubmit = true;
+ }, 100);
+ return false;
+ }
+ break;
+
+ case "Tab":
+ if (highlightedIndex >= 0) {
+ event.preventDefault();
+ event.stopPropagation();
+ selectHighlightedSuggestion(this, autocompleteTags);
+ return false;
+ }
+ break;
+
+ case "Escape":
+ event.preventDefault();
+ autocompleteTags.classList.add("d-none");
+ highlightedIndexes.set(autocompleteTags, -1);
+ break;
+ }
+ });
});
+// Prevent form submission when dropdown is active
form.addEventListener("submit", function(e) {
+ if (!allowFormSubmit) {
+ e.preventDefault();
+ e.stopPropagation();
+ allowFormSubmit = true; // Reset for next time
+ return false;
+ }
e.preventDefault();
var tagValue = form.elements.tag.value;
var excludeValue = form.elements.exclude.value;
@@ -127,5 +299,4 @@ form.addEventListener("submit", function(e) {
btn.addEventListener("click", function(e) {
e.preventDefault();
tagInputDiv.classList.toggle("d-none");
-})
-
+})
\ No newline at end of file
diff --git a/static/js/autocomplete.js b/static/js/autocomplete.js
index c94576c..5593795 100644
--- a/static/js/autocomplete.js
+++ b/static/js/autocomplete.js
@@ -1,12 +1,123 @@
var url = document.querySelector('body').getAttribute('data-autocomplete');
var tagSelector = document.getElementById("tag-selector");
var autocompleteTags = document.getElementById("autocomplete-tags");
+var highlightedIndex = -1;
+var suggestions = [];
+// Function to highlight a suggestion
+function highlightSuggestion(index) {
+ var items = autocompleteTags.querySelectorAll('.tag-suggested');
+ items.forEach(function(item, i) {
+ if (i === index) {
+ item.classList.add('highlighted');
+ } else {
+ item.classList.remove('highlighted');
+ }
+ });
+ highlightedIndex = index;
+}
+
+// Function to select the highlighted suggestion
+function selectHighlightedSuggestion() {
+ var items = autocompleteTags.querySelectorAll('.tag-suggested');
+ if (highlightedIndex >= 0 && highlightedIndex < items.length) {
+ var selectedTag = items[highlightedIndex].textContent;
+ var tags = tagSelector.value.split(",");
+ var newTag = selectedTag + ", ";
+ if (tags.length > 1) {
+ newTag = " " + newTag;
+ }
+ tags[tags.length - 1] = newTag;
+ tagSelector.value = tags.join(",");
+ autocompleteTags.classList.add("d-none");
+ highlightedIndex = -1;
+ tagSelector.focus();
+ }
+}
+
+// Function to show suggestions
+function showSuggestions(data) {
+ if (data.length > 0) {
+ suggestions = data;
+ var t = "";
+ for (var i = 0; i < data.length; i++) {
+ t += '<p class="tag-suggested" data-index="' + i + '">' + data[i].Name + '</p>';
+ }
+ autocompleteTags.innerHTML = t;
+ autocompleteTags.classList.remove("d-none");
+
+ // Preserve highlight if still valid
+ if (highlightedIndex >= data.length) {
+ highlightedIndex = -1;
+ } else if (highlightedIndex >= 0) {
+ // Re-highlight the same index
+ highlightSuggestion(highlightedIndex);
+ }
+ } else {
+ autocompleteTags.classList.add("d-none");
+ highlightedIndex = -1;
+ }
+}
+
+// Handle keyboard navigation
+tagSelector.addEventListener("keydown", function(event) {
+ var items = autocompleteTags.querySelectorAll('.tag-suggested');
+ var isVisible = !autocompleteTags.classList.contains("d-none");
+
+ if (!isVisible || items.length === 0) {
+ return;
+ }
+
+ switch(event.key) {
+ case "ArrowDown":
+ event.preventDefault();
+ if (highlightedIndex < items.length - 1) {
+ highlightSuggestion(highlightedIndex + 1);
+ } else {
+ highlightSuggestion(0); // Wrap to first
+ }
+ break;
+
+ case "ArrowUp":
+ event.preventDefault();
+ if (highlightedIndex > 0) {
+ highlightSuggestion(highlightedIndex - 1);
+ } else {
+ highlightSuggestion(items.length - 1); // Wrap to last
+ }
+ break;
+
+ case "Enter":
+ case "Tab":
+ if (highlightedIndex >= 0) {
+ event.preventDefault();
+ event.stopPropagation();
+ selectHighlightedSuggestion();
+ }
+ break;
+
+ case "Escape":
+ event.preventDefault();
+ autocompleteTags.classList.add("d-none");
+ highlightedIndex = -1;
+ break;
+ }
+});
+
+// Handle typing for autocomplete
tagSelector.addEventListener("keyup", function(event) {
- tags = this.value.split(",")
- tag = tags[tags.length -1].trim()
+ // Skip if it's a navigation key
+ if (["ArrowDown", "ArrowUp", "Enter", "Tab", "Escape"].includes(event.key)) {
+ return;
+ }
+
+ tags = this.value.split(",");
+ tag = tags[tags.length - 1].trim();
if (tag !== "") {
+ // Store the query we're searching for
+ var searchQuery = tag;
+
fetch(url + "&q=" + tag)
.then(function (response) {
if (!response.ok) {
@@ -15,13 +126,16 @@ tagSelector.addEventListener("keyup", function(event) {
return response.json();
})
.then(function (data) {
- if (data.length > 0) {
- var t = "";
- for (var i=0; i < data.length; i++) {
- t += '<p class="tag-suggested">' + data[i].Name + '</p>';
- }
- autocompleteTags.innerHTML = t;
- autocompleteTags.classList.remove("d-none");
+ // Check if the current input still matches what we searched for
+ var currentTags = tagSelector.value.split(",");
+ var currentTag = currentTags[currentTags.length - 1].trim();
+
+ if (currentTag === searchQuery) {
+ showSuggestions(data);
+ } else {
+ // Input has changed, hide the dropdown
+ autocompleteTags.classList.add("d-none");
+ highlightedIndex = -1;
}
})
.catch(function (error) {
@@ -29,18 +143,30 @@ tagSelector.addEventListener("keyup", function(event) {
});
} else {
autocompleteTags.classList.add("d-none");
+ highlightedIndex = -1;
}
-
- autocompleteTags.addEventListener("click", function(event) {
- if (event.target.classList.contains("tag-suggested")) {
- newTag = event.target.textContent + ", ";
- if (tags.length > 1) {
- newTag = " " + newTag;
- }
- tags[tags.length - 1] = newTag;
- tagSelector.value = tags.join(",");
- autocompleteTags.classList.add("d-none");
- tagSelector.focus();
- }
- });
});
+
+// Handle click selection
+autocompleteTags.addEventListener("click", function(event) {
+ if (event.target.classList.contains("tag-suggested")) {
+ var tags = tagSelector.value.split(",");
+ var newTag = event.target.textContent + ", ";
+ if (tags.length > 1) {
+ newTag = " " + newTag;
+ }
+ tags[tags.length - 1] = newTag;
+ tagSelector.value = tags.join(",");
+ autocompleteTags.classList.add("d-none");
+ highlightedIndex = -1;
+ tagSelector.focus();
+ }
+});
+
+// Handle mouse hover to update highlight
+autocompleteTags.addEventListener("mouseover", function(event) {
+ if (event.target.classList.contains("tag-suggested")) {
+ var index = parseInt(event.target.getAttribute("data-index"));
+ highlightSuggestion(index);
+ }
+});
\ No newline at end of file
diff --git a/templates/link_list.html b/templates/link_list.html
index 797285a..b67432e 100644
--- a/templates/link_list.html
+++ b/templates/link_list.html
@@ -19,7 +19,7 @@
</svg>
</button>
</div>
- {{ if not .hideNav }}<a href="#" class="text-right" id="advanced-search-btn">{{.pd.Data.advanced_search}}</a>{{ end }}
+ {{ if and (not .hideNav) .isOrgLink }}<a href="#" class="text-right" id="advanced-search-btn">{{.pd.Data.advanced_search}}</a>{{ end }}
</div>
</section>
{{if and (not .hideNav) (.advancedSearch)}}
--
2.49.0