1import MultiSelectDropdown from '../VueComponents/Inputs/MultiSelectDropdown.js'; 2import SaddlebackDropdown from '../VueComponents/SaddlebackDropdown.js'; 3 4/** 5 * ResourceFilterControls 6 * 7 * The Type, Campus and Speaker/Author filter controls for the Resources Search View, shared by 8 * both flavours: the 9 * desktop sidebar card and the mobile FilterPanelMobile popup. Same split as the weekend messages 10 * / courses filters (VueComponents/Cards: FilterControls inside FilterCard and FilterPanelMobile) â 11 * and the same reason for it, that the two layouts must not be able to drift apart. 12 * 13 * It renders as a fragment (header-section + dd-container) so it drops into either wrapper without 14 * adding a layout element, and reuses those same class names so it inherits the filter card styles. 15 * 16 * It doesn't own the selections: the view does, because it also restores them from the URL and 17 * refetches on change. Each group hides itself when its options fail to load, so a lookup outage 18 * costs one filter rather than the whole panel. 19 * 20 * Which filters exist, and why only these: of the companion app's six, its endpoint can act on 21 * Type, Topic, Campus and Speaker/Author (see ResourceSearchService.GetFilterOptionsAsync). Topic 22 * was dropped from the UI. 23 * 24 * Campus is SINGLE-select, because upstream's `CampusId` takes one value - so it uses 25 * SaddlebackDropdown rather than the MultiSelectDropdown the slug filters use, and carries an 26 * explicit "All Campuses" (0) option since it starts pre-selected rather than empty. 27 * 28 * Speaker/Author is multi-select like Type, but keyed by int id rather than by slug - so whatever 29 * it emits has to be coerced back to Number (select2 hands back strings). It is also the only 30 * filter that narrows BOTH halves of the results, which is what earns it that combined label: the 31 * person's resources AND the sermons they spoke at. It is EXPECTED to be absent on production 32 * today - HealthyChurch has yet to add author content, and an empty option list hides the group, 33 * so it stays invisible until the data lands and then appears on its own. 34 */ 35export default { 36 name: 'ResourceFilterControls', 37 components: { 38 MultiSelectDropdown, 39 SaddlebackDropdown, 40 }, 41 props: { 42 typeOptions: { 43 type: Array, 44 default: () => [], 45 }, 46 selectedTypeSlugs: { 47 type: Array, 48 default: () => [], 49 }, 50 // [{ id, name }] from /api/resourcesearch/filters. Rock campus ids, not slugs. 51 campusOptions: { 52 type: Array, 53 default: () => [], 54 }, 55 // A single Rock campus id, or 0 for "All Campuses". 56 selectedCampusId: { 57 type: Number, 58 default: 0, 59 }, 60 // [{ id, name }] from /api/resourcesearch/filters. Empty on production until HealthyChurch 61 // adds author content, which hides the group rather than showing a dead dropdown. 62 authorOptions: { 63 type: Array, 64 default: () => [], 65 }, 66 // Array of selected author ids (Numbers). 67 selectedAuthorIds: { 68 type: Array, 69 default: () => [], 70 }, 71 // The campus the page started on (the visitor's, or Online Community). Only used to decide 72 // whether Reset should appear â see isDirty. 73 defaultCampusId: { 74 type: Number, 75 default: 0, 76 }, 77 // Prefixes the underlying <select> ids. The two flavours are never mounted at the same 78 // time, but select2 keys its state off the element, so distinct ids keep a resize across 79 // the breakpoint from handing the new control the old one's leftovers. 80 idPrefix: { 81 type: String, 82 default: 'resourceFilters', 83 }, 84 // The mobile popup shows a close (X) in the header and listens for `close`; the desktop 85 // card leaves it off. 86 showClose: { 87 type: Boolean, 88 default: false, 89 }, 90 // Lets a wrapping dialog aria-labelledby the "Filters" heading. 91 headingId: { 92 type: String, 93 default: null, 94 }, 95 }, 96 emits: ['types-changed', 'campus-changed', 'authors-changed', 'reset', 'close'], 97 computed: { 98 // Drives the Reset link. Only the filters count â a search term is cleared from the search 99 // field, not from here, which is how the companion app scopes its "Clear all" too. 100 // 101 // Campus counts as dirty only when it differs from where the page STARTED (the visitor's 102 // campus, or Online Community). It is pre-selected rather than empty, so comparing it to 0 103 // would light up Reset on a page nobody has touched. 104 isDirty() { 105 return ( 106 this.selectedTypeSlugs.length > 0 || 107 this.selectedAuthorIds.length > 0 || 108 this.selectedCampusId !== this.defaultCampusId 109 ); 110 }, 111 // "All Campuses" first, matching every other campus dropdown on the site (FilterPanel.
111js 112 // prepends the same 0 sentinel). Without it there is no way back to the unfiltered list 113 // once the default has narrowed it. 114 campusDropdownOptions() { 115 return [ 116 { id: 0, text: 'All Campuses' }, 117 ...this.campusOptions.map((campus) => ({ id: campus.id, text: campus.name })), 118 ]; 119 }, 120 // MultiSelectDropdown's {id, text} shape. No "all" entry here: an empty multi-select 121 // already means "everyone", the way Type does. 122 authorDropdownOptions() { 123 return this.authorOptions.map((author) => ({ id: author.id, text: author.name })); 124 }, 125 }, 126 template: ` 127 <div class="header-section"> 128 <button 129 v-if="showClose" 130 type="button" 131 class="filter-close-btn" 132 aria-label="Close filters" 133 @click="$emit('close')" 134 > 135 <img src="/icons/closecircle.svg" alt="" aria-hidden="true" /> 136 </button> 137 <h3 :id="headingId">Filters</h3> 138 <Transition name="reset-cta"> 139 <button 140 v-if="isDirty" 141 type="button" 142 class="reset-cta" 143 @click="$emit('reset')" 144 > 145 Reset 146 </button> 147 </Transition> 148 </div> 149 <div class="dd-container"> 150 <div v-if="typeOptions.length" class="filter-group"> 151 <p class="bold">Type</p> 152 <MultiSelectDropdown 153 :selectId="idPrefix + '_type'" 154 placeholder="Select" 155 ariaLabel="Filter resources by type" 156 :options="typeOptions" 157 :modelValue="selectedTypeSlugs" 158 @update:modelValue="slugs => $emit('types-changed', slugs)" 159 /> 160 </div> 161 <div v-if="authorOptions.length" class="filter-group"> 162 <p class="bold">Speaker/Author</p> 163 <MultiSelectDropdown 164 :selectId="idPrefix + '_author'" 165 placeholder="Select" 166 ariaLabel="Filter by speaker or author" 167 :options="authorDropdownOptions" 168 :modelValue="selectedAuthorIds" 169 @update:modelValue="ids => $emit('authors-changed', ids.map(Number))" 170 /> 171 </div> 172 <div v-if="campusOptions.length" class="filter-group"> 173 <p class="bold">Campus</p> 174 <SaddlebackDropdown 175 :dropdownId="idPrefix + '_campus'" 176 ariaLabel="Filter weekend messages by campus" 177 :options="campusDropdownOptions" 178 :modelValue="selectedCampusId" 179 @update:modelValue="id => $emit('campus-changed', Number(id))" 180 /> 181 </div> 182 </div> 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.