1import ApplicationController from 'controllers/application_controller'; 2import URLHashStore from 'utils/url_hash_store' 3import debounce from 'utils/debounce' 4 5export default class extends ApplicationController { 6 static targets = ['allCategories', 'category', 'term', 'range', 'item', 'joinUsCard'] 7 8 initialize () { 9 this.filter = debounce(this.filter, 25) 10 this.hashStore = new URLHashStore() 11 } 12 13 connect() { 14 this.termTarget.addEventListener('keyup', () => this.filter()) 15 this.element.filteredContentController = this 16 17 if (this.hasRangeTarget) { 18 this.updateRangeGradient() 19 this.rangeTarget.addEventListener('input', () => { 20 this.updateRangeGradient() 21 }) 22 this.rangeTarget.addEventListener('change', () => this.filter()) 23 } 24 25 this.allCategoriesTarget.addEventListener('change', () => { 26 this.categoryTargets.forEach((categoryTarget) => { 27 categoryTarget.checked = false; 28 }); 29 30 this.filter(); 31 }); 32 33 this.categoryTargets.forEach((categoryTarget) => { 34 categoryTarget.addEventListener('change', () => { 35 this.allCategoriesTarget.checked = false; 36 if (this.categories.length === 0) { 37 this.allCategoriesTarget.checked = true; 38 } 39 40 this.filter(); 41 }); 42 }); 43 44 this.loadFilters() 45 this.filter() 46 } 47 48 // private 49 50 get categories() { 51 if (this.allCategoriesTarget.checked) { 52 return []; 53 } else { 54 return this.categoryTargets 55 .filter((categoryTarget) => categoryTarget.checked) 56 .map((categoryTarget) => categoryTarget.value); 57 } 58 } 59 60 filter() { 61 this.itemTargets.forEach((itemTarget) => itemTarget.classList.remove('display-none')); 62 this.filterByName(); 63 this.filterByRange(); 64 this.filterByCategory(); 65 this.positionJoinUsCard(); 66 this.persistFilters() 67 } 68 69 positionJoinUsCard() { 70 if (!this.hasJoinUsCardTarget) return; 71 72 const MAX_INDEX = 5 73 const visibleAmount = this.itemTargets.filter((itemTarget) => { 74 return !itemTarget.classList.contains('display-none') 75 }).length 76 const newPosition = Math.floor(Math.random() * Math.min(visibleAmount, MAX_INDEX)) 77 $(this.itemTargets[newPosition]).after(this.joinUsCardTarget) 78 } 79 80 filterByName() { 81 this.itemTargets.forEach((itemTarget) => { 82 const name = itemTarget.dataset.name; 83 const matches = name.toLowerCase().includes(this.termTarget.value.toLowerCase()); 84 85 if (!matches) { 86 itemTarget.classList.add('display-none'); 87 } 88 }); 89 } 90 91 filterByRange() { 92 if (!this.hasRangeTarget) return; 93 94 this.itemTargets.forEach((itemTarget) => { 95 const since = itemTarget.dataset.joined; 96 const matches = +since <= +this.rangeTarget.value 97 98 if (!matches) { 99 itemTarget.classList.add('display-none'); 100 } 101 }); 102 } 103 104 filterByCategory() { 105 const categories = this.categories; 106 if (categories.length === 0) return; 107 108 this.itemTargets.forEach((itemTarget) => { 109 const itemCategories = itemTarget.dataset.categories.split(' '); 110 const intersection = categories.filter((category) => itemCategories.includes(category)); 111 const matches = intersection.length > 0 112 113 if (!matches) { 114 itemTarget.classList.add('display-none'); 115 } 116 }); 117 } 118 119 persistFilters () { 120 this.hashStore.setState((state) => { 121 if (this.termTarget.value) state.term = this.termTarget.value.toLowerCase(); 122 123 // Always persist category state - either specific categories or 'all' 124 if (this.allCategoriesTarget.checked) { 125 state.categories = 'all'; 126 } else if (this.categories.length) { 127 state.categories = this.categories; 128 } else { 129 // If no categories selected and not "all", default to "all" 130 state.categories = 'all'; 131 } 132 133 if (this.hasRangeTarget) state.range = +this.rangeTarget.value; 134 }) 135 } 136 137 loadFilters () { 138 const state = this.hashStore.getState() 139 140 // Term 141 if (state.term) this.termTarget.value = state.term 142 143 // Categories 144 if (state.categories) { 145 if (state.categories === 'all') { 146 // Set "All Beez" checkbox to checked 147 this.allCategoriesTarget.checked = true; 148 } else if (state.categories.length > 0) { 149 // Uncheck "All Beez" and check specific categories 150 this.allCategoriesTarget.checked = false;
151 this.categoryTargets.forEach((categoryTarget) => { 152 if (state.categories.includes(categoryTarget.value)) { 153 categoryTarget.checked = true 154 categoryTarget.dispatchEvent(new Event('change')) 155 } 156 }) 157 } 158 } 159 160 // Range 161 if (this.hasRangeTarget && state.range) { 162 this.rangeTarget.value = state.range 163 this.updateRangeGradient() 164 } 165 } 166 167 updateRangeGradient() { 168 if (!this.hasRangeTarget) return; 169 170 const min = parseInt(this.rangeTarget.min); 171 const max = parseInt(this.rangeTarget.max); 172 const value = parseInt(this.rangeTarget.value); 173 const percentage = ((value - min) / (max - min)) * 100; 174 175 this.rangeTarget.style.setProperty('--value-percent', `${percentage}%`); 176 } 177};
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.