From 76e3045ad2d3750eacbcc3df2a434c4944758159 Mon Sep 17 00:00:00 2001 From: Pete Date: Thu, 24 Oct 2024 09:57:43 +0200 Subject: [PATCH] chore: Add comments to search-and-filter.js --- static/js/search-and-filter.js | 159 +++++++++++++++++++++++++-------- templates/create-update.html | 1 - 2 files changed, 121 insertions(+), 39 deletions(-) diff --git a/static/js/search-and-filter.js b/static/js/search-and-filter.js index 26e5a12..e5df7ce 100644 --- a/static/js/search-and-filter.js +++ b/static/js/search-and-filter.js @@ -1,4 +1,8 @@ -// Setup the search and filter patterns +/* + * Function that finds all the search and filter components on the page and + * sets up the event listeners for opening the panel and handling the chips. + * Also calls the specific setup functions for each type of search and filter component. + **/ (function() { const patternMap = new Map(); [].slice.call(document.querySelectorAll('.js-search-and-filter')).forEach(function(pattern) { @@ -6,34 +10,40 @@ const chipsContainer = pattern.querySelector('.p-filter-panel-section__chips'); const targetPanel = pattern.querySelector('.p-search-and-filter__panel'); const hiddenInput = pattern.querySelector('.js-hidden-input'); + // Here we ceate a map for each search and filter. This allow us to have only one document event listner. patternMap.set(pattern, { searchContainer, chipsContainer, targetPanel, hiddenInput, - isInSearchActiveComponent: (element, pattern) => { + // This function checks if the target is in an active search component. + isInActiveSearchComponent: (element, pattern) => { const isActive = pattern.classList.contains('js-active') return element.closest('.p-search-and-filter') === pattern && !isActive; } }); + // When you focus a specifc input, open the associated chips panel pattern.addEventListener('focusin', function(e) { openPanel(searchContainer, targetPanel, true); }); + // When you focus out of the input, close the chips panel. pattern.addEventListener('focusout', function(e) { - if (patternMap.get(pattern).isInSearchActiveComponent(e.target, pattern)) { + if (patternMap.get(pattern).isInActiveSearchComponent(e.target, pattern)) { return; } openPanel(searchContainer, targetPanel, false); }); }); + // Event delegation for clicks inside and outside the search and filter components. document.addEventListener('click', function(e) { patternMap.forEach((element, pattern) => { - const { searchContainer, targetPanel, isInSearchActiveComponent, hiddenInput } = element; - if (!isInSearchActiveComponent(e.target, pattern)) { + const { searchContainer, targetPanel, isInActiveSearchComponent, hiddenInput } = element; + if (!isInActiveSearchComponent(e.target, pattern)) { openPanel(searchContainer, targetPanel, false); return; } const targetChip = e.target.closest('.js-chip'); + // If the click is on a chip, handle the selection if (targetChip) { e.preventDefault(); if (targetChip.closest('.js-products')) { @@ -44,12 +54,19 @@ } }); }); + // Each of the search and filters have different data sources and act slightly differently. + // So we have to set up each one individually. setUpProductFilter(); setUpOtherTagsFilter(); setUpAuthorFilter(); })(); -// Generic function to show and hide the cips selection panel +/* + * Generic function to show and hide the chips selection panel. + * @param {HTMLElement} searchContainer - The whole pannel container. + * @param {HTMLElement} panel - Where the chips are displayed. + * @param {Boolean} isOpen - Whether the panel should end open or closed. + **/ function openPanel(searchContainer, panel, isOpen) { if (typeof isOpen === 'undefined') { isOpen = panel.getAttribute('aria-hidden') === 'false'; @@ -67,7 +84,12 @@ function openPanel(searchContainer, panel, isOpen) { } } -// Generic function to add chip value from hidden input +/* + * Generic function to add the value of a selected chip, to the value of a hidden input. + * We have to do this as the chips will not be submitted with the form. + * @param {String} value - The value of the chip. + * @param {HTMLElement} input - The hidden input to store the values. + **/ function addChipValueToHiddenInput(value, input) { let selectedChips = input.value.split(',').filter(Boolean); if (!selectedChips.includes(value)) { @@ -76,31 +98,41 @@ function addChipValueToHiddenInput(value, input) { input.setAttribute('value', selectedChips.join(',')); } -// Generic function to remove chip value from hidden input +/* + * Generic function to remove the value of a selected chip, from the value of a hidden input. + * @param {String} value - The value of the chip. + * @param {HTMLElement} input - The hidden input to store the values. + **/ function removeChipValueFromHiddenInput(value, input) { let selectedChips = input.value.split(',').filter(Boolean); selectedChips = selectedChips.filter(id => id !== value); input.setAttribute('value', selectedChips.join(',')); } -// Product specific setup +/* + * Specific setup for the products filter. + * Products comes from a predefined list (products.yaml) and are added to the page on load. + * By default they are all hidden and are shown and hidden based on the search input. + **/ function setUpProductFilter() { const productsPanel = document.querySelector('.js-products'); const fuse = setupFuse(productsPanel); - handleInputChange(fuse, productsPanel.querySelector('.js-search-input')); - // If there are existing chips, because you are updating, handle them + handleProductInputChange(fuse, productsPanel.querySelector('.js-search-input')); + // If we are on the /update route, the asset may have existing chips, we need to add them to the page. const existingChips = Array.from(productsPanel.querySelectorAll('.js-chip.is-inactive.u-hide')); existingChips.forEach(chip => handleProductsChipSelection(chip, productsPanel.querySelector('.js-hidden-input'))); } -// Setup for fuse.js, specific to products +/* + * Setup fuse.js, as fuzzy search specfically for the products and return the instance. + * @param {HTMLElement} productsPanel - The panel containing the products chips. + **/ function setupFuse(productsPanel) { + // Map the chips to an object with the name and id. const chipsObj = Array.from(productsPanel.querySelectorAll('.js-chip.is-inactive')).map(chip => ({ name: chip.dataset.name, id: chip.id - })); - const input = productsPanel.querySelector('.js-search-input'); const options = { keys: ['name'], threshold: 0.3 @@ -109,8 +141,11 @@ function setupFuse(productsPanel) { return fuse; } -// Function to handle input change for products -function handleInputChange(fuse, input) { +/* + * Specfic handling for the product search input. + * It calls the fuse.js instance and shows/hides the results. + **/ +function handleProductInputChange(fuse, input) { input.addEventListener('input', () => { const query = document.querySelector('.js-search-input').value; const result = fuse.search(query); @@ -118,7 +153,14 @@ function handleInputChange(fuse, input) { }); } -// Product specific handling for clicking chips +/* + * Specfic setup for product chips. + * When a chip is selected, it hides the one the selection panel and shows the one in the input. + * It adds/removes the selected chip to the hidden input. + * Attaches a dismiss handler to the active chip. + * @param {Array} chips - The chips to show. + * @param {HTMLElement} hiddenInput - The hidden input to store the selected chips values. + **/ function handleProductsChipSelection(chip, hiddenInput) { const activeChip = document.querySelector(`.js-${chip.id}.is-active`); activeChip.addEventListener('click', function dismissChip(e) { @@ -133,7 +175,13 @@ function handleProductsChipSelection(chip, hiddenInput) { activeChip.classList.remove('u-hide'); } -// Product specific handling for showing the results from fuse +/* + * Specfic handling for the product chips search results. + * Starts by hiding all chips, then shows the ones that match the search query. + * If there are no results, shows a message. + * @param {Array} chips - The chips to show. + * @param {String} query - The search query. + **/ function showAndHideProductChips(chips, query) { const allChips = document.querySelectorAll('.js-chip.is-inactive'); if (!query) { @@ -157,7 +205,10 @@ function showAndHideProductChips(chips, query) { } } -// Author specific setup +/* + * Sets up a query to the directory API to search for authors. + * Calls the function that shows the search results + **/ function setUpAuthorFilter() { const authorsInput = document.querySelector('.js-authors-input'); authorsInput.addEventListener('input', debounce(async function() { @@ -185,7 +236,12 @@ function setUpAuthorFilter() { } } -// Author specfic handling for chips selection dropdown +/* + * Adds and removes the author chips to the search panel. + * As this comes from an API call, we can not setupo the chips on load (like with products). + * We have to create the chips on the fly. Limited to 10 results. + * @param {Array} data - The data from the API call. + **/ function addAndRemoveAuthorsChips(data) { const chipContainer = document.querySelector('.js-authors-chip-container'); chipContainer.innerHTML = ''; @@ -205,26 +261,39 @@ function addAndRemoveAuthorsChips(data) { }); } -// Author specfic handling clicking author chip -function handleAuthorsChipSelection(chip, chipContainer) { +/* + * When a chip is selected, it adds the chip to the search panel. + * It also adds the chip value to three hidden inputs, as we have to pass the email, firstname and lastname. + * Attaches a dismiss handler to the active chip. Limited to 1 selected chip. + * @param {HTMLElement} chip - The chip to select. + * @param {HTMLElement} activeChipContainer - The container to add the active chip. + **/ +function handleAuthorsChipSelection(chip, activeChipContainer) { const template = document.querySelector('#author-active-chip-template'); + // Clone the chip template const activeChip = template.content.cloneNode(true); - chipContainer.querySelector('.js-chip')?.remove(); + // Remove the cuurent active chip as we only want one at a time + activeChipContainer.querySelector('.js-chip')?.remove(); activeChip.querySelector('.js-value').textContent = chip.dataset.firstname + ' ' + chip.dataset.lastname; activeChip.querySelector('.js-chip').addEventListener('click', function dismissChip(e) { e.preventDefault(); - removeChipValueFromHiddenInput(chip.dataset.email, chipContainer.querySelector('.js-hidden-input-email')); - removeChipValueFromHiddenInput(chip.dataset.firstname, chipContainer.querySelector('.js-hidden-input-firstname')); - removeChipValueFromHiddenInput(chip.dataset.lastname, chipContainer.querySelector('.js-hidden-input-lastname')); + // Reset the three hidden inputs, firstname, lastname and email + removeChipValueFromHiddenInput(chip.dataset.email, activeChipContainer.querySelector('.js-hidden-input-email')); + removeChipValueFromHiddenInput(chip.dataset.firstname, activeChipContainer.querySelector('.js-hidden-input-firstname')); + removeChipValueFromHiddenInput(chip.dataset.lastname, activeChipContainer.querySelector('.js-hidden-input-lastname')); this.remove(); }); - addChipValueToHiddenInput(chip.dataset.email, chipContainer.querySelector('.js-hidden-input-email')); - addChipValueToHiddenInput(chip.dataset.firstname, chipContainer.querySelector('.js-hidden-input-firstname')); - addChipValueToHiddenInput(chip.dataset.lastname, chipContainer.querySelector('.js-hidden-input-lastname')); - chipContainer.prepend(activeChip); + // Add the chip values to the three hidden inputs, firstname, lastname and email + addChipValueToHiddenInput(chip.dataset.email, activeChipContainer.querySelector('.js-hidden-input-email')); + addChipValueToHiddenInput(chip.dataset.firstname, activeChipContainer.querySelector('.js-hidden-input-firstname')); + addChipValueToHiddenInput(chip.dataset.lastname, activeChipContainer.querySelector('.js-hidden-input-lastname')); + activeChipContainer.prepend(activeChip); } -// Other tags specific setup +/* + * Specific setup for the other tags filter. + * We don't want to use the search here, but still want to add/remove chips. + **/ function setUpOtherTagsFilter() { const panel = document.querySelector('.js-other-tags'); const input = panel.querySelector('.js-input'); @@ -233,21 +302,26 @@ function setUpOtherTagsFilter() { const template = document.querySelector('#other-tag-chip-template'); const existingChips = Array.from(container.querySelectorAll('.js-chip')); handleExisitngOtherTagsChips(existingChips, hiddenInput); + // On 'enter' click, add the chip to the select panel. input.addEventListener('keydown', function(event) { if (event.key === 'Enter') { event.preventDefault(); const tagClone = template.content.cloneNode(true); tagClone.querySelector('.js-value').textContent = input.value; - attachDismissChipHandler(tagClone.querySelector('.js-chip'), hiddenInput); + attachDismissOtherTagChip(tagClone.querySelector('.js-chip'), hiddenInput); container.prepend(tagClone); + // Update the hidden input value addChipValueToHiddenInput(input.value, hiddenInput); + // Reset the input value input.value = ''; } }); } -// Dissmiss other tags chip handler -function attachDismissChipHandler(target, hiddenInput) { +/* + * Attach the dismiss handler for other tags chip. + **/ +function attachDismissOtherTagChip(target, hiddenInput) { target.addEventListener('click', function dismissChip(e) { e.preventDefault(); const value = this.querySelector('.js-value').textContent; @@ -256,16 +330,23 @@ function attachDismissChipHandler(target, hiddenInput) { }); } -// Handle exising chips +/* + * When editing a asset, check if it had existing other tags and handle them. + **/ function handleExisitngOtherTagsChips(chips, hiddenInput) { if (chips.length) { chips.forEach(chip => { - attachDismissChipHandler(chip, hiddenInput); + attachDismissOtherTagChip(chip, hiddenInput); addChipValueToHiddenInput(chip.dataset.value, hiddenInput) }); } } -// Debounce for directory api call + +/* + * Function to debounce a function call. + * @param {Function} func - The function to debounce. + * @param {Number} delay - The delay in ms. + **/ function debounce(func, delay) { let timer; return function (...args) { @@ -275,7 +356,9 @@ function debounce(func, delay) { }; } -// Presubmit actions +/* + * Function to handle multiselects as they were not submitting all the selected values. + **/ document.querySelector('#create-update-asset').addEventListener('submit', function(e) { e.preventDefault(); const multiSelects = document.querySelectorAll('.js-multiselect'); diff --git a/templates/create-update.html b/templates/create-update.html index d5c4940..fb9c164 100644 --- a/templates/create-update.html +++ b/templates/create-update.html @@ -144,7 +144,6 @@

Tags

-