Skip to content

Commit

Permalink
chore: Add comments to search-and-filter.js
Browse files Browse the repository at this point in the history
  • Loading branch information
petesfrench committed Oct 24, 2024
1 parent fc9f750 commit 76e3045
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 39 deletions.
159 changes: 121 additions & 38 deletions static/js/search-and-filter.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,49 @@
// 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) {
const searchContainer = pattern.querySelector('.p-search-and-filter__search-container');
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')) {
Expand All @@ -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';
Expand All @@ -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)) {
Expand All @@ -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
Expand All @@ -109,16 +141,26 @@ 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);
showAndHideProductChips(result.map(item => item.item), query);
});
}

// 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) {
Expand All @@ -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) {
Expand All @@ -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() {
Expand Down Expand Up @@ -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 = '';
Expand All @@ -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');
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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');
Expand Down
1 change: 0 additions & 1 deletion templates/create-update.html
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ <h4 class="p-filter-panel-section__heading p-heading--5">Tags</h4>
<label class="u-hide" for="search">Search other tags</label>
<input autocomplete="off" class="p-search-and-filter__input js-input" type="text" value="" aria-labelledby="tags">
<input type="text" class="u-hide js-hidden-input" name="tags" value="">
<button alt="search" class="u-off-screen" type="submit">Search</button>
</div>
</div>
</div>
Expand Down

0 comments on commit 76e3045

Please sign in to comment.