tags: js: Minor fixes and refactoring

- Drop changing the history as even without it, back/forward work just fine.

- Drop debouncing as there was a bug that prevented it from working. Since we
have a small number of tags, running the operations immediately seems to work
fine.

- Update incorrect docstring.

- Flatten and isolate the event handlers code further for readability.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Sunil Mohan Adapa 2024-10-16 15:28:03 -07:00
parent 5ce7385f60
commit 00a5377d9e
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2

View File

@ -24,23 +24,21 @@
*/ */
/** /**
* Update the URL path and history based on the selected tags. * Update the URL path based on the selected tags.
* *
* If no tags are selected, redirects to the base apps path. Otherwise, * If no tags are selected, redirects to the base apps path. Otherwise,
* constructs a new URL with query parameters for each tag and updates * constructs a new URL with query parameters for each tag.
* the browser history.
* *
* @param {string[]} tags - An array of selected tag names. * @param {string[]} tags - An array of selected tag names.
*/ */
function updatePath(tags) { function updatePathWithTags(tags) {
const appsPath = window.location.pathname; const appsPath = window.location.pathname;
if (tags.length === 0) { if (tags.length === 0) {
this.location.assign(appsPath); this.location.assign(appsPath);
} else { } else {
let queryParams = tags.map(tag => `tag=${tag}`).join('&'); const urlParams = new URLSearchParams();
let newPath = `${appsPath}?${queryParams}`; tags.forEach(tag => urlParams.append('tag', tag));
this.history.pushState({ tags: tags }, '', newPath); this.location.search = urlParams;
this.location.assign(newPath);
} }
} }
@ -56,8 +54,8 @@ function updatePath(tags) {
function getTags(tagToRemove) { function getTags(tagToRemove) {
const tagBadges = document.querySelectorAll('#selected-tags .tag-badge'); const tagBadges = document.querySelectorAll('#selected-tags .tag-badge');
return Array.from(tagBadges) return Array.from(tagBadges)
.map(tag => tag.dataset.tag) .map(tagBadge => tagBadge.dataset.tag)
.filter(tagName => tagName !== tagToRemove); .filter(tag => tag !== tagToRemove);
} }
/** /**
@ -67,9 +65,10 @@ function getTags(tagToRemove) {
* to match the user's input in the search box. It determines the best * to match the user's input in the search box. It determines the best
* matching item and marks it as active. * matching item and marks it as active.
* *
* @param {KeyboardEvent} event - The keyboard event that triggered the search. * @param {ElementList} [dropdownItems] - List of items in the tags dropdown.
*/ */
function findMatchingTag(addTagInput, dropdownItems) { function findMatchingTag(dropdownItems) {
const addTagInput = document.getElementById('add-tag-input');
const searchTerm = addTagInput.value.toLowerCase().trim(); const searchTerm = addTagInput.value.toLowerCase().trim();
// Remove highlighting from all items // Remove highlighting from all items
@ -81,7 +80,8 @@ function findMatchingTag(addTagInput, dropdownItems) {
if (text.includes(searchTerm)) { if (text.includes(searchTerm)) {
item.style.display = 'block'; item.style.display = 'block';
function matchesEarly () { function matchesEarly () {
return text.indexOf(searchTerm) < bestMatch.textContent.toLowerCase().indexOf(searchTerm); let bestMatchText = bestMatch.textContent.toLowerCase();
return text.indexOf(searchTerm) < bestMatchText.indexOf(searchTerm);
}; };
if (bestMatch === null || matchesEarly()) { if (bestMatch === null || matchesEarly()) {
bestMatch = item; bestMatch = item;
@ -97,6 +97,70 @@ function findMatchingTag(addTagInput, dropdownItems) {
} }
} }
/**
* Handle a key press event on that tag input field.
*
* As the user types in the input field, the dropdown list is filtered
* to show only matching items. The best matching item (first match if
* multiple match) is highlighted. Pressing Enter selects the
* highlighted item and adds it as a tag.
*
* @param {KeyboardEvent} [event] - The key press event.
*/
function onTagInputKeyUp(event) {
const dropdownItems = document.querySelectorAll('.tag-input li.dropdown-item');
// Select the active tag if the user pressed Enter
if (event.key === 'Enter') {
dropdownItems.forEach(item => {
if (item.classList.contains('active')) {
item.click();
}
});
}
findMatchingTag(dropdownItems);
}
/**
* When an item in the tags dropdown is clicked, navigate to a new URL with the
* added Tag.
*
* @param {PointerEvent} [event] - The click event.
*/
function onTagInputDropdownItemClicked(event) {
const item = event.currentTarget;
const selectedTag = item.dataset.value;
// Add the selected tag and update the path.
let tags = getTags('');
tags.push(selectedTag);
updatePathWithTags(tags);
// Reset the input field
const addTagInput = document.getElementById('add-tag-input');
addTagInput.value = '';
// Reset the dropdown
const dropdownItems = document.querySelectorAll('.tag-input li.dropdown-item');
dropdownItems.forEach(item => {
item.style.display = 'none';
item.classList.remove('active');
});
}
/**
* when an remove button next to a tag is clicked, navigate to a new URL without
* that Tag.
*
* @param {PointerEvent} [event] - The click event.
*/
function onRemoveTagClicked(event) {
const button = event.currentTarget;
const tag = button.parentElement.dataset.tag;
const tags = getTags(tag);
updatePathWithTags(tags);
}
/** /**
* Manage tag-related UI interactions for filtering and displaying apps. * Manage tag-related UI interactions for filtering and displaying apps.
* *
@ -106,65 +170,16 @@ function findMatchingTag(addTagInput, dropdownItems) {
* available tags in a searchable dropdown. * available tags in a searchable dropdown.
*/ */
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Remove Tag handler.
document.querySelectorAll('.remove-tag').forEach(button => { document.querySelectorAll('.remove-tag').forEach(button => {
button.addEventListener('click', () => { button.addEventListener('click', onRemoveTagClicked);
let tag = button.parentElement.dataset.tag;
let tags = getTags(tag);
updatePath(tags);
});
}); });
/**
* Searchable dropdown for selecting tags.
*
* As the user types in the input field, the dropdown list is filtered
* to show only matching items. The best matching item (first match if
* multiple match) is highlighted. Pressing Enter selects the
* highlighted item and adds it as a tag.
*/
const addTagInput = document.getElementById('add-tag-input'); const addTagInput = document.getElementById('add-tag-input');
const dropdownItems = document.querySelectorAll('li.dropdown-item'); addTagInput.addEventListener('keyup', onTagInputKeyUp);
var timeoutId;
addTagInput.addEventListener('keyup', (event) => {
clearTimeout(timeoutId);
// Select the active tag if the user pressed Enter
if (event.key === 'Enter') {
dropdownItems.forEach(item => {
if (item.classList.contains('active')) {
item.click();
}
});
}
// Debounce the user input for search with 300ms delay.
timeoutId = setTimeout(findMatchingTag(addTagInput, dropdownItems), 300);
});
const dropdownItems = document.querySelectorAll('.tag-input li.dropdown-item');
dropdownItems.forEach(item => { dropdownItems.forEach(item => {
item.addEventListener('click', () => { item.addEventListener('click', onTagInputDropdownItemClicked);
const selectedTag = item.dataset.value;
// Add the selected tag and update the path.
let tags = getTags('');
tags.push(selectedTag);
updatePath(tags);
// Reset the input field and dropdown.
addTagInput.value = '';
dropdownItems.forEach(item => {
item.style.display = '';
item.classList.remove('active');
});
});
});
// Handle browser back/forward navigation.
window.addEventListener('popstate', function (event) {
if (event.state && event.state.tags) {
updatePath(event.state.tags);
}
}); });
}); });