1/** 2 * Get the status (checked/unchecked) for each filter. 3 * 4 * @returns {Object} Arrays of the name and status of each filter, grouped together into filter 5 * groups. 6 */ 7function getFilterStatuses() { 8 const useCases = Array.from( 9 document.querySelectorAll('#use-case-dropdown .checkbox-container'), 10 ).map((label) => { 11 return { 12 name: label.textContent.toLowerCase(), 13 isChecked: label.querySelector('input').checked, 14 }; 15 }); 16 const libraries = Array.from( 17 document.querySelectorAll('#library-dropdown .checkbox-container'), 18 ).map((label) => { 19 return { 20 name: label.textContent.toLowerCase(), 21 isChecked: label.querySelector('input').checked, 22 }; 23 }); 24 const frameworks = Array.from( 25 document.querySelectorAll('#framework-dropdown .checkbox-container'), 26 ).map((label) => { 27 return { 28 name: label.textContent.toLowerCase(), 29 isChecked: label.querySelector('input').checked, 30 }; 31 }); 32 const contributor = Array.from( 33 document.querySelectorAll('#all-examples-dropdown .checkbox-container'), 34 ).map((label) => { 35 const inputElement = label.querySelector('input'); 36 return { 37 name: inputElement.id.replace('-checkbox', ''), 38 isChecked: inputElement.checked, 39 }; 40 }); 41 return { 42 useCases, 43 libraries, 44 frameworks, 45 contributor, 46 }; 47} 48 49/** 50 * Test whether the tags of the given example panel match the requested filters. 51 * 52 * @param {any} tags Tags of the example panel 53 * @param {any} filters Filter statuses for all the filter groups; this should be the output of 54 * getFilterStatuses. 55 * @returns {bool} True if the example panel matches the filters, or not. 56 */ 57function panelMatchesFilters(tags, filters) { 58 return Object.entries(filters).every(([group, groupTags]) => { 59 // If there is no selection, consider the panel to be matched 60 if (groupTags.filter(({isChecked}) => isChecked).length === 0) { 61 return true; 62 } 63 64 // If "Any" is checked, consider the panel to be matched 65 if ( 66 groupTags.filter(({name, isChecked}) => name === 'any' && isChecked) 67 .length > 0 68 ) { 69 return true; 70 } 71 72 // Otherwise show the panel if any checked item matches the tags of the panel 73 return groupTags 74 .filter(({isChecked}) => isChecked) 75 .some(({name}) => tags.includes(name)); 76 }); 77} 78 79/** Apply the currently selected filters to the example gallery, showing only the relevant examples. */ 80function applyFilter() { 81 const noMatchesElement = document.getElementById('no-matches'); 82 const panels = document.querySelectorAll('.example'); 83 const filters = getFilterStatuses(); 84 const searchTerm = document 85 .getElementById('examples-search-input') 86 .value.toLowerCase(); 87 88 // Show all panels before hiding the ones that need to be hidden. 89 panels.forEach((panel) => panel.classList.remove('hidden')); 90 91 // Check the title and tags of each example panel. If the tags match and the search term matches, 92 // show the panel. 93 panels.forEach((panel) => { 94 const title = panel 95 .querySelector('.example-title') 96 .textContent.toLowerCase(); 97 const tags = panel 98 .querySelector('.example-tags') 99 .textContent.toLowerCase() 100 .concat(); 101 const other_keywords = panel 102 .querySelector('.example-other-keywords') 103 .textContent.toLowerCase(); 104 const keywords = `${tags} ${other_keywords}`; 105 const matchesSearch = 106 title.includes(searchTerm) || keywords.includes(searchTerm); 107 const matchesTags = panelMatchesFilters(keywords, filters); 108 109 // Hide panels that have no match 110 if (matchesSearch && matchesTags) { 111 panel.classList.remove('hidden'); 112 } else { 113 panel.classList.add('hidden'); 114 } 115 }); 116 117 // If none are shown, show the "no matches" graphic. 118 if (document.querySelectorAll('.example:not(.hidden)').length === 0) { 119 noMatchesElement.classList.remove('hidden'); 120 } else { 121 noMatchesElement.classList.add('hidden'); 122 } 123 124 // Set the URL to match the active filters using query parameters. 125 const selectedTags = Object.entries(filters).map(([group, groupTags]) => { 126 return { 127 group, 128 selected: groupTags 129 .filter(({isChecked}) => isChecked) 130 .map(({name}) => name), 131 }; 132 }); 133 134 const queryParam = selectedTags 135 .filter(({group, selected}) => selected.length > 0) 136 .map(({group, selected}) => `${group}=${selected.join(',')}`) 137 .join('&'); 138 139 history.replaceState( 140 null, 141 null, 142 queryParam.length === 0 ? location.pathname : `?${queryParam}`, 143 ); 144} 145 146window.addEventListener('load', () => { 147 // Listen for filter checkbox clicks. 148 document.querySelectorAll('.filter-checkbox').forEach((tag) => { 149 tag.addEventListener('click', () => applyFilter()); 150 }); 151 152 // Add event listener for keypresses in the search bar. 153 document 154 .getElementById('examples-search-input') 155 .addEventListener('keyup', (event) => { 156 event.preventDefault(); 157 applyFilter(); 158 }); 159 160 // Add the ability to provide URL query parameters to filter examples on page load.
161 const urlParams = new URLSearchParams(window.location.search); 162 if (urlParams.size > 0) { 163 urlParams.forEach((params) => { 164 params.split(',').forEach((param) => { 165 const element = document.getElementById(`${param}-checkbox`); 166 if (element) { 167 element.checked = true; 168 } 169 }); 170 }); 171 } 172 173 // Apply the filter in case there are URL query parameters. 174 applyFilter(); 175 176 const dropdowns = Array.from( 177 document.querySelectorAll('.dropdown-content'), 178 ).map((dropdown) => { 179 return { 180 dropdown, 181 input: dropdown.parentNode.querySelector('input'), 182 inputContainer: dropdown.parentNode, 183 }; 184 }); 185 document.addEventListener('click', (event) => { 186 let targetEl = event.target; // clicked element 187 188 do { 189 const unclicked = dropdowns.filter(({dropdown, inputContainer}) => { 190 return !(targetEl == dropdown || targetEl == inputContainer); 191 }); 192 if (unclicked.length !== dropdowns.length) { 193 // There has been a click inside one of the dropdowns. Close unclicked dropdowns and return. 194 unclicked.forEach(({input}) => { 195 input.checked = false; 196 }); 197 return; 198 } 199 200 // Go up the DOM. 201 targetEl = targetEl.parentNode; 202 } while (targetEl); 203 204 // This is a click outside. Close all dropdowns. 205 dropdowns.forEach(({dropdown, input}) => { 206 input.checked = false; 207 }); 208 }); 209});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.