1import './styles/component-terminal.css'; 2import './styles/packages.css'; 3import './component-terminal.js'; 4import Clipboard from 'clipboard'; 5import TomSelect from 'tom-select'; 6 7window.addEventListener('DOMContentLoaded', () => { 8 new Packages(); 9}); 10 11class Packages { 12 #searchCriteria; 13 14 constructor() { 15 this.#searchCriteria = { 16 type: 'all', 17 query: '', 18 sortField: 'daily-downloads', 19 sortDirection: 'DESC', 20 }; 21 22 this.#createPackageSelector(); 23 this.#createCopyToClipboardButtons(); 24 this.#createSearchControls(); 25 } 26 27 #createPackageSelector() { 28 const packageSelector = document.querySelector('#package-selector'); 29 if (null === packageSelector) { 30 return; 31 } 32 33 new TomSelect(packageSelector, { 34 plugins: ['dropdown_input'], 35 maxOptions: null, 36 maxItems: 1, 37 }); 38 } 39 40 #createCopyToClipboardButtons() { 41 const wrapperElement = document.querySelector('.package-installation'); 42 if (null === wrapperElement) { 43 return; 44 } 45 46 const copyButton = wrapperElement.querySelector('button'); 47 const clipboard = new Clipboard(copyButton, { 48 text: () => { 49 let texToCopy = copyButton.getAttribute('data-text-to-copy'); 50 51 if (null === texToCopy) { 52 const listOfPackages = wrapperElement.querySelector('select'); 53 const selectedPackage = listOfPackages.options[listOfPackages.selectedIndex].value; 54 55 texToCopy = `composer require ${selectedPackage}`; 56 } 57 58 return texToCopy; 59 }, 60 }); 61 62 const originalCopyButtonContents = copyButton.innerHTML; 63 clipboard.on('success', (_event) => { 64 copyButton.innerHTML = 'Copied!'; 65 setTimeout(() => { 66 copyButton.innerHTML = originalCopyButtonContents; 67 }, 1500); 68 }); 69 } 70
71 #createSearchControls() { 72 const searchElement = document.querySelector('#package-search-control'); 73 if (null === searchElement) { 74 return; 75 } 76 77 searchElement.addEventListener('input', () => { 78 this.#searchCriteria.query = searchElement.value.trim().toLowerCase(); 79 this.#updatePackageList(); 80 }); 81 82 const filterElements = document.querySelectorAll( 83 '#package-search-type-filter input[name="package-search-type"]', 84 ); 85 filterElements.forEach((filterElement) => { 86 filterElement.addEventListener('click', () => { 87 document.querySelectorAll('#package-search-type-filter label').forEach((filterLabel) => { 88 filterLabel.classList.remove('active'); 89 }); 90 filterElement.closest('label').classList.add('active'); 91 92 this.#searchCriteria.type = filterElement.value; 93 this.#updatePackageList(); 94 }); 95 }); 96 97 const sortElement = document.querySelector('#package-sort-control'); 98 sortElement.addEventListener('change', (event) => { 99 event.preventDefault(); 100 101 const selectedOption = sortElement.options[sortElement.selectedIndex]; 102 this.#searchCriteria.sortField = selectedOption.getAttribute('data-sort-field'); 103 this.#searchCriteria.sortDirection = selectedOption.getAttribute('data-sort-direction'); 104 this.#updatePackageList(); 105 }); 106 } 107 108 #updatePackageList() { 109 const otherTypes = ['polyfill', 'contract', 'pack']; 110 let numResults = 0; 111 // first, filter the list of packages based on the search criteria 112 document.querySelectorAll('#package-list .package').forEach((packageElement) => { 113 const packageType = packageElement.getAttribute('data-type'); 114 const packageTypes = packageType.split(' '); 115 const isRowVisibleAfterFilters = 116 'all' === this.#searchCriteria.type || 117 ('other' === this.#searchCriteria.type && packageTypes.some((t) => otherTypes.includes(t))) || 118 packageTypes.includes(this.#searchCriteria.type); 119 120 const packageName = packageElement.querySelector('.name').innerText.trim().toLowerCase(); 121 const packageDescription = packageElement.querySelector('.description').innerText.trim().toLowerCase(); 122 const isRowVisibleAfterQuerySearch = 123 '' === this.#searchCriteria.query || 124 this.#fuzzySearch(this.#searchCriteria.query, `${packageName} ${packageDescription}`); 125 126 const isPackageVisible = isRowVisibleAfterFilters && isRowVisibleAfterQuerySearch; 127 if (isPackageVisible) { 128 packageElement.classList.remove('d-none'); 129 numResults++; 130 } else { 131 packageElement.classList.add('d-none'); 132 } 133 }); 134 135 // second, sort the list of packages based on the search criteria 136 const sortField = this.#searchCriteria.sortField; 137 const sortDirection = this.#searchCriteria.sortDirection; 138 139 const packageListElement = document.querySelector('#package-list'); 140 const packages = Array.from(packageListElement.querySelectorAll('.package')); 141 142 packages.sort((a, b) => { 143 let valueA; 144 let valueB; 145 146 if ('name' === sortField) { 147 valueA = a.querySelector('.name').innerText.trim().toLowerCase(); 148 valueB = b.querySelector('.name').innerText.trim().toLowerCase(); 149 } else { 150 valueA = a.getAttribute(`data-${sortField}`); 151 valueB = b.getAttribute(`data-${sortField}`); 152 } 153 154 const comparison = Number.isNaN(valueB - valueA) 155 ? valueA.localeCompare(valueB) 156 : Number.parseFloat(valueA) - Number.parseFloat(valueB); 157
158 return 'ASC' === sortDirection ? comparison : -comparison; 159 }); 160 161 const fragment = document.createDocumentFragment(); 162 packages.forEach((packageElement) => fragment.appendChild(packageElement)); 163 164 packageListElement.replaceChildren(fragment); 165 166 // finally, update the number of results found 167 const numSearchResultsElement = document.querySelector('.num-search-results'); 168 numSearchResultsElement.innerHTML = 169 0 === numResults 170 ? `<b class="text-danger">Found ${numResults} ${1 === numResults ? 'package' : 'packages'}</b>` 171 : `Found ${numResults} ${1 === numResults ? 'package' : 'packages'}`; 172 } 173 174 #fuzzySearch(query, searchContent) { 175 let isFound = false; 176 query.split(' ').forEach((queryPart) => { 177 if (null !== searchContent && searchContent.includes(queryPart)) { 178 isFound = true; 179 } 180 }); 181 182 return isFound; 183 } 184}
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.