PageSourceSearch

https://saddleback.com/scripts/Resources/ResourceFilterControls.js?v=517cdeafa93af7720f65d0a6f449ca34186eeedc

js saddleback.com collected 2026-09-24 17:18:08 UTC 8,293 bytes, 184 lines download raw bytes

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.