PageSourceSearch

https://aopanet.org/wp-content/themes/mg-starter/blocks/directory/block-directory.js?ver=30

js aopanet.org collected 2026-10-03 23:47:59 UTC 33,708 bytes, 955 lines download raw bytes

1import { store, getElement } from "@wordpress/interactivity";
2import { scrollToAnchor } from "../../src/js/utils/scroll-to-anchor.js";
3import { sortBy } from "../../src/js/utils/sort-obj-array-by.js";
4import { isObject } from "../../src/js/utils/is.js";
5
6const svgPath = mgThemeVars ? mgThemeVars.svgPath : null;
7const orgPlaceholderImage = svgPath ? `${svgPath}/org_placeholder.png` : null;
8
9function parseHtmlText(text = "") {
10    const parser = new DOMParser();
11    const decoded = parser.parseFromString(text, "text/html").documentElement
12        .textContent;
13    return decoded;
14}
15
16function validateWebsite(websiteValue) {
17    // Handle empty or whitespace-only values
18    if (!websiteValue || websiteValue.trim() === "") {
19        return null;
20    }
21
22    // Split by pipe character to handle multiple URLs
23    const urls = websiteValue
24        .split("|")
25        .map((url) => url.trim())
26        .filter((url) => url !== "");
27
28    if (urls.length === 0) {
29        return null;
30    }
31
32    // Take the first valid URL
33    let validUrl = urls[0];
34
35    // Add protocol if missing
36    if (!validUrl.match(/^https?:\/\//i)) {
37        validUrl = "https://" + validUrl;
38    }
39
40    // Basic URL validation
41    try {
42        new URL(validUrl);
43        return validUrl;
44    } catch (e) {
45        // If URL is still invalid, return null
46        return null;
47    }
48}
49
50const searchUrl = mgThemeVars.restUrl + "directory";
51const facetsUrl = mgThemeVars.restUrl + "directory-facets";
52const searchCollectionId = 1;
53const tabClasses =
54    "uppercase text-sm lg:text-base border-b border-dividers bg-quaternary text-white px-6 lg:px-12 py-6 text-center font-black";
55const currentTabClasses =
56    "uppercase text-sm lg:text-base border border-dividers border-b-white px-6 lg:px-12 py-6 text-center font-black bg-white text-font-main";
57
58store("mg-store-directory", {
59    state: {
60        page: 1,
61        lastPage: 1, // last page of the directory
62        initialized: false,
63        posts: [],
64        mgPaginationListenerAdded: false,
65        loading: false,
66        mode: "company",
67        zip: "",
68        radius: "",
69        params: {
70            record_type: "",
71            record_number: "",
72            title: "",
73            categories: []
74        },
75        options: {
76            categories: [],
77            radius: [
78                {
79                    name: "5 miles",
80                    code: 5
81                },
82                {
83                    name: "10 miles",
84                    code: 10
85                },
86                {
87                    name: "15 miles",
88                    code: 15
89                },
90                {
91                    name: "25 miles",
92                    code: 25
93                },
94                {
95                    name: "50 miles",
96                    code: 50
97                },
98                {
99                    name: "100 miles",
100                    code: 100
101                }
102            ]
103        }
104    },
105
106    actions: {
107        updateName(e) {
108            const { state } = store("mg-store-directory");
109            state.params.title = e.target.value;
110        },
111
112        scrollToResults() {
113            const resultsAnchor = document.getElementById(
114                "mg-block-directory-feed"
115            );
116            scrollToAnchor(resultsAnchor);
117        },
118
119        prevPage() {
120            const { state, callbacks } = store("mg-store-directory");
121            state.page -= 1;
122
123            // fetchDirectory
124            callbacks.fetchDirectory();
125        },
126
127        nextPage() {
128            const { state, callbacks } = store("mg-store-directory");
129            state.page += 1;
130
131            // fetchDirectory
132            callbacks.fetchDirectory();
133        },
134
135        addPaginationNumber(pageNumber, container) {
136            const template = document.getElementById(
137                "directory-pagination-number"
138            );
139            const clone = template.content.cloneNode(true);
140            const button = clone.querySelector("button");
141            button.textContent = pageNumber;
142            button.dataset.pageNumber = pageNumber;
143            container.appendChild(clone);
144        },
145
146        addPaginationCurrent(pageNumber, container) {
147            const template = document.getElementById(
148                "directory-pagination-current"
149            );
150            const clone = template.content.cloneNode(true);
151            const span = clone.querySelector("span");
152            span.textContent = pageNumber;
153            container.appendChild(clone);
154        },
155
156        addPaginationDots(container) {
157            const template = document.getElementById(
158                "directory-pagination-dots"
159            );
160            const clone = template.content.cloneNode(true);
161            container.appendChild(clone);
162        },
163
164        setupPaginationDelegation() {
165            const { state, callbacks } = store("mg-store-directory");
166            const paginationNumbers = document.querySelector(
167                ".mg-directory-pagination-container"
168            );
169
170            // Use event delegation - single listener for all pagination buttons
171            // Only add the listener once to prevent duplicates
172            if (state.mgPaginationListenerAdded || !paginationNumbers) return;
173
174            paginationNumbers.addEventListener("click", (e) => {
175                if (e.target.matches("button.mg-directory-pagination-number")) {
176                    const pageNumber = parseInt(e.target.dataset.pageNumber);
177                    state.page = pageNumber;
178                    callbacks.fetchDirectory();
179                }
180            });
181
182            state.mgPaginationListenerAdded = true;
183        },
184
185        switchToCompany() {
186            const { state, actions, callbacks } = store("mg-store-directory");
187            state.mode = "company";
188
189            // Update URL parameters
190            actions.updateUrlParams();
191
192            callbacks.handleModeSwitch();
193        },
194
195        switchToIndividual() {
196            const { state, actions, callbacks } = store("mg-store-directory");
197            state.mode = "individual";
198
199            // Update URL parameters
200            actions.updateUrlParams();
201
202            callbacks.handleModeSwitch();
203        },
204
205        updateMode(event) {
206            const { state, actions, callbacks } = store("mg-store-directory");
207            state.mode = event.target.value;
208
209            // Update URL parameters
210            actions.updateUrlParams();
211
212            // Don't fetch here - wait for form submit
213            callbacks.getAndSetFacets();
214        },
215
216        updateName(event) {
217            const { state, actions } = store("mg-store-directory");
218            state.params.title = event.target.value;
219            actions.updateUrlParams();
220            // Don't fetch here - wait for form submit
221        },
222
223        updateZip(event) {
224            const { state, actions } = store("mg-store-directory");
225            state.zip = event.target.value;
226            actions.updateUrlParams();
227            // Don't fetch here - wait for form submit
228        },
229
230        updateRadius(event) {
231            const { state, actions } = store("mg-store-directory");
232            state.radius = event.target.value;
233            actions.updateUrlParams();
234            // Don't fetch here - wait for form submit
235        },
236
237        updateCategory(event) {
238            const { state, actions } = store("mg-store-directory");
239            state.params.categories = [event.target.value];
240            actions.updateUrlParams();
241            // Don't fetch here - wait for form submit
242        },
243
244        handleFormSubmit(event) {
245            event.preventDefault();
246            const { state, actions, callbacks } = store("mg-store-directory");
247            state.page = 1; // Reset to first page
248            callbacks.fetchDirectory();
249            actions.updateTabClasses();
250        },
251
252        clearForm() {
253            const { state, actions } = store("mg-store-directory");
254
255            // NOTE: dont change `state.mode` here
256            state.params = {
257                record_type: "",
258                record_number: "",
259                title: "",
260                categories: []
261            };
262            state.zip = "";
263            state.radius = "";
264            state.page = 1;
265
266            // Clear URL parameters
267            actions.updateUrlParams();
268        },
269
270        updateTabClasses() {
271            const { state } = store("mg-store-directory");
272            const tabs = document.querySelectorAll(".mg-directory-tabs button");
273            const companyTab = document.querySelector(
274                '[data-wp-on--click="actions.switchToCompany"]'
275            );
276            const individualTab = document.querySelector(
277                '[data-wp-on--click="actions.switchToIndividual"]'
278            );
279
280            tabs.forEach((tab) => {
281                tab.className = tabClasses;
282            });
283
284            if (state.mode == "company") {
285                companyTab.className = currentTabClasses;
286            } else if (state.mode == "individual") {
287                individualTab.className = currentTabClasses;
288            }
289        },
290
291        updateRadioButtons() {
292            const { state } = store("mg-store-directory");
293            const filterForm = document.querySelector(
294                ".mg-directory-filter-form"
295            );
296            const companyRadio = filterForm
297                ? filterForm.querySelector(
298                      'input[name="type"][value="company"]'
299                  )
300                : null;
301            const individualRadio = filterForm
302                ? filterForm.querySelector(
303                      'input[name="type"][value="individual"]'
304                  )
305                : null;
306
307            if (companyRadio && individualRadio) {
308                if (state.mode === "company") {
309                    companyRadio.checked = true;
310                    individualRadio.checked = false;
311                } else if (state.mode === "individual") {
312                    companyRadio.checked = false;
313                    individualRadio.checked = true;
314                }
315            }
316        },
317
318        updateUrlParams() {
319            const { state } = store("mg-store-directory");
320            const url = new URL(window.location);
321
322            // Clear existing search params
323            url.searchParams.delete("title");
324            url.searchParams.delete("category");
325            url.searchParams.delete("zip");
326            url.searchParams.delete("radius");
327            url.searchParams.delete("mode");
328
329            // Add current filter values to URL
330            if (state.params.title) {
331                url.searchParams.set("title", state.params.title);
332            }
333            if (state.params.categories && state.params.categories.length > 0) {
334                url.searchParams.set("category", state.params.categories[0]);
335            }
336            if (state.zip) {
337                url.searchParams.set("zip", state.zip);
338            }
339            if (state.radius) {
340                url.searchParams.set("radius", state.radius);
341            }
342            if (state.mode && state.mode !== "company") {
343                url.searchParams.set("mode", state.mode);
344            }
345
346            // Update browser URL without page reload
347            window.history.replaceState({}, "", url);
348        }
349    },
350
351    callbacks: {
352        init() {
353            const { state, actions, callbacks } = store("mg-store-directory");
354            console.log("MG STORE, page:", state.page);
355
356            // Only initialize once
357            if (state.initialized) {
358                console.log("MG STORE, already initialized");
359                return;
360            }
361
362            // init UI
363            actions.setupPaginationDelegation();
364
365            // fetch data
366            callbacks.getAndSetFacets();
367
368            // Update data based on search parameters from URL GET params
369            const urlParams = new URLSearchParams(window.location.search);
370            const searchTitle = urlParams.get("title") || "";
371            const searchCategory = urlParams.get("category") || "";
372            const searchZip = urlParams.get("zip") || "";
373            const searchRadius = urlParams.get("radius") || "";
374            const searchMode = urlParams.get("mode") || "company"; // Default to company
375            let hasInitUrlParams = false;
376
377            // Set initial search parameters if they exist
378            if (searchTitle) {
379                state.params.title = searchTitle;
380                hasInitUrlParams = true;
381            }
382            if (searchCategory) {
383                state.params.categories = [searchCategory];
384                hasInitUrlParams = true;
385
386                const catSelect = document.querySelector(
387                    ".mg-directory-filter-category"
388                );
389                if (catSelect) {
390                    // wait on second and update dom node
391                    // not sure how to properly do this with interactivity API
392                    setTimeout(() => {
393                        catSelect.value = searchCategory;
394                    }, 1000);
395                }
396            }
397            if (searchZip) {
398                state.zip = searchZip;
399                hasInitUrlParams = true;
400            }
401            if (searchRadius) {
402                state.radius = searchRadius;
403                hasInitUrlParams = true;
404            }
405            if (searchMode) {
406                state.mode = searchMode;
407            }
408
409            // Update radio buttons after setting mode from URL
410            actions.updateRadioButtons();
411
412            // Update tab classes after setting mode from URL
413            actions.updateTabClasses();
414
415            // then, fetch directory
416            callbacks.fetchDirectory(hasInitUrlParams);
417
418            // this is last, set initialized to true
419            state.initialized = true;
420        },
421
422        handlePostsUpdate() {
423            const { state } = store("mg-store-directory");
424            const docFragment = document.createDocumentFragment();
425            const feedGridContainer = document.getElementById(
426                "mg-block-directory-feed"
427            );
428            const prevButton = document.querySelector(
429                ".mg-directory-pagination-prev-button"
430            );
431            const nextButton = document.querySelector(
432                ".mg-directory-pagination-next-button"
433            );
434            const isIndividual = state.mode === "individual";
435
436            // console.log("Posts update triggered. Posts are now:", state.posts);
437
438            if (!feedGridContainer) return;
439
440            /**
441             * Not sure why but I could not get wp-bind to work here, so I'm doing it manually.
442             */
443            if (prevButton && state.page === 1) {
444                prevButton.classList.add("hidden");
445            } else {
446                prevButton.classList.remove("hidden");
447            }
448
449            if (nextButton && state.page === state.lastPage) {
450                nextButton.classList.add("hidden");
451            } else {
452                nextButton.classList.remove("hidden");
453            }
454
455            /**
456             * Warning, avoid event listeners on template items.
457             * If used, remove them when the item is removed from the DOM.
458             */
459            state.posts.forEach((post) => {
460                const postItem = document
461                    .getElementById("directory-item")
462                    .content.cloneNode(true);
463
464                // Populate the directory item with data
465                const contentElm = postItem.querySelector(
466                    ".mg-directory-item-content"
467                );
468                const heading = postItem.querySelector("h3");
469                const category = postItem.querySelector(".category");
470                const address = postItem.querySelector(".address");
471                const website = postItem.querySelector(".website a");
472                const websiteText = postItem.querySelector(".website b");
473                const phone = postItem.querySelector(".phone");
474                const ogImage = postItem.querySelector(".og-image");
475                const recordNumber = postItem.querySelector(".record-number");
476                const organization = postItem.querySelector(".organization");
477                const jobTitle = postItem.querySelector(".job-title");
478                const hasImage =
479                    !post["og:image"] ||
480                    (post["og:image"] &&
481                        !post["og:image"].includes("no-ind-image-available") &&
482                        !post["og:image"].includes("no-org-image-available"));
483
484                // Set the title
485                heading.insertAdjacentHTML("beforeend", post.title);
486
487                // set custom placeholder image
488                if (isIndividual && !hasImage) {
489                    post["og:image"] = "";
490                } else if (!hasImage) {
491                    post["og:image"] = orgPlaceholderImage;
492                }
493
494                if (isIndividual) {
495                    ogImage.setAttribute(
496                        "class",
497                        "og-image relative overflow-hidden shadow bg-center bg-cover bg-no-repeat bg-primary rounded-3xl w-[150px] h-[200px] mx-auto transform translate-y-[-10%]"
498                    );
499                } else {
500                    // border-t and pt-12
501                    contentElm.classList.add("pt-12", "border-t");
502                    ogImage.style.backgroundSize = "85%";
503                }
504
505                // Set category
506                if (!isIndividual && category && post.categories) {
507                    category.textContent = post.categories
508                        .split("|")
509                        .join(", ");
510                    category.classList.remove("hidden");
511                }
512
513                // Build address from components
514                if (address) {
515                    const addressLines = [];
516                    const line1Parts = [];
517                    const line2Parts = [];
518                    let line2PartsString = "";
519
520                    // First line: address_line1 and address_line2
521                    if (post.address_line1) line1Parts.push(post.address_line1);
522                    if (post.address_line2) line1Parts.push(post.address_line2);
523
524                    // Second line: city, state, zip
525                    if (post.address_city) line2Parts.push(post.address_city);
526                    if (post.address_state) line2Parts.push(post.address_state);
527
528                    // Build the address string
529                    if (line1Parts.length > 0) {
530                        addressLines.push(line1Parts.join(", "));
531                    }
532                    if (line2Parts.length > 0) {
533                        line2PartsString = line2Parts.join(", ");
534                        if (post.address_zip) {
535                            line2PartsString += ` ${post.address_zip}`;
536                        }
537                        addressLines.push(line2PartsString);
538                    }
539
540                    if (addressLines.length > 0) {
541                        address.innerHTML = addressLines.join("<br>");
542                        address.classList.remove("hidden");
543                    }
544                }
545
546                // Set website
547                if (website && websiteText && post.website) {
548                    const websiteUrl = validateWebsite(post.website);
549                    if (websiteUrl) {
550                        website.href = websiteUrl;
551                        websiteText.textContent = websiteUrl;
552                        website.parentElement.classList.remove("hidden");
553                    }
554                }
555
556                // Set phone
557                if (phone && post.phone) {
558                    phone.textContent = post.phone;
559                    phone.classList.remove("hidden");
560                }
561
562                // Set og:image as background
563                if (isIndividual && !hasImage) {
564                    // show nested img placeholder
565                    ogImage.querySelector("img").classList.remove("hidden");
566                } else if (ogImage && post["og:image"]) {
567                    ogImage.style.backgroundImage = `url(${post["og:image"]})`;
568                }
569
570                // Set record number
571                if (recordNumber && post.record_number) {
572                    recordNumber.textContent = post.record_number;
573                    recordNumber.classList.remove("hidden");
574                }
575
576                // Set organization (trusted HTML)
577                if (organization && post.organization) {
578                    organization.insertAdjacentHTML(
579                        "beforeend",
580                        post.organization
581                    );
582                    organization.classList.remove("hidden");
583                }
584
585                // Set job title
586                if (jobTitle && post.job_title) {
587                    jobTitle.textContent = post.job_title;
588                    jobTitle.classList.remove("hidden");
589                }
590
591                docFragment.appendChild(postItem);
592            });
593
594            document
595                .getElementById("mg-block-directory-feed")
596                .replaceChildren(docFragment);
597        },
598
599        watchPagination() {
600            const { state, actions } = store("mg-store-directory");
601            const paginationNumbers = document.querySelector(
602                ".mg-directory-pagination-container"
603            );
604
605            if (!paginationNumbers) return;
606
607            console.log(
608                "Pagination watcher triggered. Page:",
609                state.page,
610                "Last page:",
611                state.lastPage
612            );
613
614            // Clear existing pagination numbers
615            paginationNumbers.innerHTML = "";
616
617            const currentPage = state.page;
618            const lastPage = state.lastPage;
619            const maxVisiblePages = 5;
620
621            // Calculate which pages to show
622            let startPage = Math.max(
623                1,
624                currentPage - Math.floor(maxVisiblePages / 2)
625            );
626            let endPage = Math.min(lastPage, startPage + maxVisiblePages - 1);
627
628            // Adjust start page if we're near the end
629            if (endPage - startPage < maxVisiblePages - 1) {
630                startPage = Math.max(1, endPage - maxVisiblePages + 1);
631            }
632
633            // Add first page and dots if needed
634            if (startPage > 1) {
635                actions.addPaginationNumber(1, paginationNumbers);
636                if (startPage > 2) {
637                    actions.addPaginationDots(paginationNumbers);
638                }
639            }
640
641            // Add visible page numbers
642            for (let i = startPage; i <= endPage; i++) {
643                if (i === currentPage) {
644                    actions.addPaginationCurrent(i, paginationNumbers);
645                } else {
646                    actions.addPaginationNumber(i, paginationNumbers);
647                }
648            }
649
650            // Add last page and dots if needed
651            if (endPage < lastPage) {
652                if (endPage < lastPage - 1) {
653                    actions.addPaginationDots(paginationNumbers);
654                }
655                actions.addPaginationNumber(lastPage, paginationNumbers);
656            }
657        },
658
659        handleModeSwitch() {
660            const { state, actions, callbacks } = store("mg-store-directory");
661            const feedGridContainer = document.getElementById(
662                "mg-block-directory-feed"
663            );
664            const isIndividual = state.mode === "individual";
665
666            state.page = 1; // Reset to first page
667
668            // fetch data
669            callbacks.fetchDirectory();
670            callbacks.getAndSetFacets();
671
672            // Update UI
673            if (isIndividual) {
674                feedGridContainer.classList.remove("gap-y-12");
675                feedGridContainer.classList.add("gap-y-24", "mt-24");
676            } else {
677                feedGridContainer.classList.add("gap-y-12");
678                feedGridContainer.classList.remove("gap-y-24", "mt-24");
679            }
680
681            actions.updateTabClasses();
682            actions.updateRadioButtons();
683        },
684
685        fetchDirectory(scroll = true) {
686            const { state, actions, callbacks } = store("mg-store-directory");
687            const loader = document.querySelector(
688                ".mg-block-directory .pulsating-circle"
689            );
690            const feedContainer = document.getElementById(
691                "mg-block-directory-feed"
692            );
693
694            // log state name
695            console.log("State title params:", state.params.title);
696
697            // set state to loading
698            state.loading = true;
699
700            if (loader) {
701                loader.classList.remove("hidden");
702            }
703
704            if (feedContainer) {
705                feedContainer.classList.add("hidden");
706            }
707
708            // Add name search if provided
709            if (state.params.title && state.params.title.trim() !== "") {
710                state.params.title = state.params.title.trim();
711            }
712
713            const url = callbacks.buildRequestUrl();
714            wp.apiFetch({
715                url: url,
716                parse: false
717            })
718                .then((response) => {
719                    return response.json();
720                })
721                .then((data) => {
722                    const totalPages = data.lastPage;
723                    if (totalPages) {
724                        state.lastPage = parseInt(totalPages);
725                    }
726
727                    state.posts = data.result;
728                })
729                .catch((error) => {
730                    state.posts = [];
731                    console.error("Error fetching directory:", error);
732                })
733                .finally(() => {
734                    state.loading = false;
735
736                    if (loader) {
737                        loader.classList.add("hidden");
738                    }
739
740                    if (feedContainer) {
741                        feedContainer.classList.remove("hidden");
742                    }
743
744                    callbacks.handlePostsUpdate();
745                    callbacks.handleNoResults();
746
747                    if (scroll) {
748                        actions.scrollToResults();
749                    }
750                });
751        },
752
753        setOptions(payload = []) {
754            const { state } = store("mg-store-directory");
755
756            if (!Array.isArray(payload)) return;
757
758            for (const item of payload) {
759                if (!isObject(item) || !Array.isArray(item.facet)) continue;
760                const facet = item.facet.length ? item.facet[0] : "";
761                if (!isObject(facet)) continue;
762                if (facet.name in state.options) {
763                    const facetOptions = facet.int;
764
765                    if (Array.isArray(facetOptions)) {
766                        const filteredOptions = facetOptions.filter(
767                            (f_option) => {
768                                const optionName = f_option?.name;
769                                return (
770                                    "string" === typeof optionName &&
771                                    "none" !== optionName.toLowerCase()
772                                );
773                            }
774                        );
775
776                        state.options[facet.name] = sortBy(
777                            filteredOptions,
778                            "name"
779                        );
780                    }
781                }
782            }
783        },
784
785        buildRequestUrl() {
786            console.log("Building request url...");
787
788            const { state, actions } = store("mg-store-directory");
789            const urlObj = new URL(searchUrl);
790            const query = [];
791            const quoteValues = [
792                "address_country",
793                "address_state",
794                "categories",
795                "address_city"
796            ];
797            let zip = state.zip;
798            const radius = state.radius;
799            const page = state.page;
800            const mode = state.mode;
801
802            query.push(`record_type:"${mode}"`);
803
804            for (const key in state.params) {
805                const isLastName = "last_name" === key;
806                const isFirstName = "first_name" === key;
807                const isTitle = "title" === key;
808                let value = state.params[key];
809
810                if (isLastName && value) {
811                    value = value.replace(/-/g, " ");
812                }
813
814                if ((isLastName || isFirstName || isTitle) && value) {
815                    //if lastname contains ' (like o'malley) then wrap it in double quotes
816                    if (
817                        value.includes("'") &&
818                        !(value.startsWith("'") && value.endsWith("'"))
819                    ) {
820                        value = `"${value}"`;
821                    }
822                }
823
824                if (Array.isArray(value) && value.length) {
825                    const valueEncoded = value.map((v) => {
826                        return `"${v}"`;
827                    });
828                    query.push(`${key}:(${valueEncoded.join(" OR ")})`);
829                    continue;
830                }
831
832                let valueEncoded = encodeURIComponent(value);
833                const maybeNeedsAdjustment = [
834                    "first_name",
835                    "last_name",
836                    "organization",
837                    "title"
838                ];
839
840                if (maybeNeedsAdjustment.includes(key)) {
841                    const valueIsQuoted =
842                        "string" == typeof value &&
843                        ((value.startsWith(`"`) && value.endsWith(`"`)) ||
844                            (value.startsWith(`'`) && value.endsWith(`'`)));
845
846                    if (value && !value.endsWith("*") && !valueIsQuoted) {
847                        valueEncoded += "*";
848                    }
849                }
850
851                if (value && quoteValues.includes(key)) {
852                    query.push(`${key}:"${valueEncoded}"`);
853                } else if (value) {
854                    query.push(`${key}:${valueEncoded}`);
855                }
856            }
857
858            if (zip) {
859                zip = zip.substring(0, 5);
860
861                // if (!radius) {
862                //     query.push(`address_zip:${zip}`);
863                // }
864
865                urlObj.searchParams.append("zip", zip);
866            }
867
868            urlObj.searchParams.append("col", searchCollectionId);
869            urlObj.searchParams.append("pagesize", 9);
870            urlObj.searchParams.append("page", page);
871            urlObj.searchParams.append("filter", query.join(" AND "));
872
873            if (zip && radius) {
874                urlObj.searchParams.append("radius", radius);
875            }
876
877            const request = new Request(urlObj);
878            return request.url;
879        },
880
881        getAndSetFacets() {
882            console.log("Building request url for facets...");
883            const { state, callbacks } = store("mg-store-directory");
884            const urlObj = new URL(facetsUrl);
885            const query = [];
886            const mode = state.mode;
887
888            query.push(`record_type:"${mode}"`);
889            urlObj.searchParams.append("filter", query.join(" AND "));
890
891            const request = new Request(urlObj);
892            const url = request.url;
893
894            wp.apiFetch({
895                url: url,
896                parse: false
897            })
898                .then((response) => {
899                    return response.json();
900                })
901                .then((data) => {
902                    if (data.facets) {
903                        const facets = data.facets;
904                        callbacks.setOptions(facets);
905                    }
906                })
907                .catch((error) => {
908                    console.error("Error fetching facets:", error);
909                })
910                .finally(() => {
911                    // log state options
912                    // console.log("State options:", state.options);
913                });
914        },
915
916        handleNoResults() {
917            const { state } = store("mg-store-directory");
918            const pagination = document.querySelector(
919                ".wp-block-query-pagination"
920            );
921            const noResultsMessage = document.querySelector(
922                ".mg-directory-no-results-message"
923            );
924
925            // Check if we have results
926            const hasResults =
927                Array.isArray(state.posts) && state.posts.length > 0;
928
929            if (hasResults) {
930                // Show pagination, hide no results message
931                if (pagination) {
932                    pagination.classList.remove("hidden");
933                }
934                if (noResultsMessage) {
935                    noResultsMessage.classList.add("hidden");
936                }
937            } else {
938                // Hide pagination, show no results message
939                if (pagination) {
940                    pagination.classList.add("hidden");
941                }
942                if (noResultsMessage) {
943                    noResultsMessage.classList.remove("hidden");
944                }
945            }
946        },
947
948        parseMyText: () => {
949            const element = getElement();
950            const domNode = element.ref;
951            const html = parseHtmlText(domNode.textContent);
952            domNode.textContent = html;
953        }
954    }
955});

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.